Custom storage layout configuration examples

@ack What we figured out eventually was that the absolute path of a template file did not work with the env that MAAS used to run the commissioning scripts. We decided to skip using a template storage.json configuration file and put it directly into the custom storage script. We output that to the $MAAS_STORAGE_CONFIG_FILE but the 50 script that looks for the custom information did not add the storage-extra things at the bottom of the MAAS_STORAGE_CONFIG_FILE so we wound up directly modifying the MAAS_STORAGE_CONFIG_FILE to achieve a custom storage layout.

Here is the end result.

#!/usr/bin/env python3
#
# 41-custom-storage-layout - set layout for ess controller
#
# Copyright (C) 2012-2020 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# --- Start MAAS 1.0 script metadata ---
# name: 41-custom-storage-layout
# title: Set layout for ESS_Controller
# description: Set layout for ESS_Controller
# script_type: commissioning
# timeout: 60
# --- End MAAS 1.0 script metadata ---

import json
import os
import sys

storage = '''{"layout": {"sda": {"type": "disk", "ptable": "gpt", "boot": true, "partitions": [{"name": "sda1", "fs": "fat32", "size": "4G", "bootable": true}, {"name": "sda2", "size": "23G", "fs": "ext4"}, {"name": "sda3", "size": "10G", "fs": "swap"}, {"name": "sda4", "size": "41G", "fs": "ext4"}, {"name": "sda5", "size": "1G", "fs": "ext4"}, {"name": "sda6", "size": "1G", "fs": "ext4"}]}}, "mounts": {"/": {"device": "sda2", "options": "noatime"}, "/boot/efi": {"device": "sda1"}, "/home": {"device": "sda4"}, "/var/log": {"device": "sda5"}, "/boot": {"device": "sda6"}, "none": {"device": "sda3"}}}'''

def read_json_file(path):
    try:
        with open(path) as fd:
            return json.load(fd)
    except OSError as e:
        sys.exit(f"Failed to read {path}: {e}")
    except json.JSONDecodeError as e:
        sys.exit(f"Failed to parse {path}: {e}")

data = read_json_file(os.environ["MAAS_RESOURCES_FILE"])

disk_type = data["resources"]["storage"]["disks"][0]["id"]

print (os.environ["MAAS_RESOURCES_FILE"])
print (os.getcwd())

layout_file = json.loads(storage)

layout_file["layout"][disk_type] = layout_file["layout"]["sda"]

if disk_type !="sda":
    del layout_file["layout"]["sda"]

for partition in layout_file["layout"][disk_type]["partitions"]:
    partition["name"] = partition["name"].replace("sda", disk_type)

for mount in layout_file["mounts"]:
    layout_file["mounts"][mount]["device"] = layout_file["mounts"][mount]["device"].replace("sda", disk_type)

data["storage-extra"] = layout_file

with open(os.environ["MAAS_RESOURCES_FILE"], 'w') as fd:
    json.dump(data, fd)

I noted that your output on the last 2 lines goes to MAAS_RESOURCES_FILE instead of MAAS_STORAGE_CONFIG_FILE…

I’m also working on a script of my own, and had another question:

  • Is it possible to interogate the Pool or Tags that are applied to a host to make decisions about which disks to select and perpaps set up different configurations based on those variables?

Thanks

~~ Charles

Hi @bedfordc, sorry for the late reply.

Currently, info about tags and pools are not available to the machine when executing the script.
The only information you can rely on is the the content of the $MAAS_RESOURCES_FILE, and whatever you can gather by inspecting the machine itself in your script.

Okay - I don’t see a way to specifiy a partition that takes up the rest of the disk… If a size is not specified for a partition will it consume the entire drive?

Also where would I find the MAAS_RESOURCES_FILE contents? Is that in the GUI somewhere so I can get a look at it to be better prepared how to code the script to take it apart?

Thanks

~~ Charles

The size is currently required, so you have to calculate the remaining size based on the disk size.

The content of the file is reported to maas as output of the 20-maas-03-machine-resources commissioning script.

After a few trials and tribulations, I have a well commented script that I think people might find useful. It adheres to a number of standards which our shop uses, and leaves a chunk of disk unused for whatever the purpose of the system may call for.

Here’s what I ended up with:

#!/usr/bin/env python3
####
#
#  Program: 42-custom-disk-layout.sh
#
#  Description:
#  This python script is to specify how to select and partition a disk in MAAS.
#  Inputs: the MAAS_RESOURCES_FILE from previous default scripts is a json
# construct detailing the hardware that is currently being "commissioned".
#  Outputs: the MAAS_STORAGE_CONFIG_FILE should contain a json structure
# that defines the selected hard disk and where to slice it up to the standard
# mountpoints for the OS installation, however that step continually failed.
# Instead this script loads all the data from the MAAS_RESOURCES_FILE, then
# adds the ['storage-extra'] block with the selected disk, partitions, and
# logical volumes.
#
#  Programmer:	Charles Bedford
#  History:
#    2022-09-30 - CHB - Initial revision
#
# --- Start MAAS 1.0 script metadata ---
# name: 42-custom-disk-layout
# title: Set layout for DELL Virtual disks
# description: Set layout for DELL Virtual disks
# script_type: commissioning
# timeout: 60
# --- End MAAS 1.0 script metadata ---
#
####
import json
import os
import sys

# Function definitions
def read_json_file(path):
    try:
        with open(path) as fd:
            return json.load(fd)
    except OSError as e:
        sys.exit(f"Failed to read {path}: {e}")
    except json.JSONDecodeError as e:
        sys.exit(f"Failed to parse {path}: {e}")

# Load the hardware from the json in the MAAS_RESOURCES_FILE
hardware = read_json_file(os.environ['MAAS_RESOURCES_FILE'])

####
#
#  This is the primary datastructure (in json format) with
# placeholders which are easily found and replaced below (DISK & MAX)
#
####
cfg = '''{
    "layout": {
        "DISK": {
            "type": "disk",
            "ptable": "gpt",
            "boot": true,
            "partitions": [
                { "name": "DISK1", "fs": "fat32", "size": "2G", "bootable": true },
                { "name": "DISK2", "size": "MAX" }
            ]
        },
        "vg": {
            "type": "lvm",
            "members": [ "DISK2" ],
            "volumes": [
                { "name": "lv1", "size": "50G", "fs": "ext4" },
                { "name": "lv2", "size": "16G", "fs": "swap" },
                { "name": "lv3", "size": "20G", "fs": "ext4" },
                { "name": "lv4", "size": "20G", "fs": "ext4" },
                { "name": "lv5", "size": "20G", "fs": "ext4" },
                { "name": "lv6", "size": "10G", "fs": "ext4" },
                { "name": "lv7", "size": "20G", "fs": "ext4" },
                { "name": "lv8", "size": "20G", "fs": "ext4" }
            ]
        }
    },
    "mounts": {
        "/": { "device": "lv1" },
        "/boot/efi": { "device": "DISK1" }, 
        "none": { "device": "lv2" },
        "/tmp": { "device": "lv3" },
        "/var": { "device": "lv4" },
        "/var/log": { "device": "lv5" },
        "/var/log/audit": { "device": "lv6" },
        "/var/tmp": { "device": "lv7" },
        "/home": { "device": "lv8" }
    }
}'''

disks = hardware['resources']['storage']['disks']

# Initialize a few variables so we have consistent values below
primary = ''
primaryId = 0
diskSize = 0
index=0

# Iterate through all the disks defined for this machine
for disk in disks:
    # skip virtual mounted drives from maas
    if 'Virtual' in disk["model"] or 'Virtual' in disk['device_id']:
        continue

    # find the PERC or DELLBOSS id to use...
    if 'PERC' in disk["model"] or 'DELLBOSS' in disk["model"]:
            primary = disk['id']
            primaryId = index
            diskSize = disk['size']

    index+=1

# if the loop didn't find a PERC or DELLBOSS drive to use
# barring that it will have to be #0
if primary == '':
    primary=disks[0]['id']
    primaryId = 0
    diskSize = disks[0]['size']

# The disk ID (sda or whatever) that we want to use for booting...
diskId = hardware["resources"]["storage"]["disks"][primaryId]["id"]

# Load layoutDetail from the above structure
layoutDetail = json.loads(cfg)

# copy the DISK temporary structure over the top of the new diskId name
layoutDetail["layout"][diskId] = layoutDetail["layout"]["DISK"]

# Now delete the DISK temporary structure
del layoutDetail["layout"]["DISK"]

####
#
#  Cleanup the structure details
#
####
# set the names and sizes on the partitions:
# Note: the sizes are NOT based on 1024, but 1000.
# subtract the size of the fat32 partition and a constant...
mySize = ( diskSize - 2000000000 ) / 1000000000
num = 1
for p in layoutDetail["layout"][diskId]["partitions"]:
	p["name"] = diskId + str(num)
	num += 1;
	if p["size"] == "MAX":
		p["size"] = str(int(mySize)) + "G"
		savedP = p

# The Members of the lvm
layoutDetail["layout"]["vg"]["members"] = [ savedP["name"] ]

# last but not least - fix the one mount detail
layoutDetail["mounts"]["/boot/efi"]["device"] = diskId + "1"

####
#
#  Output
#
####
hardware["storage-extra"] = layoutDetail

print('Saving custom storage layout to ' + os.environ['MAAS_RESOURCES_FILE'])
print(json.dumps(hardware))

with open(os.environ['MAAS_RESOURCES_FILE'], 'w') as fd:
    json.dump(hardware, fd)

I hope that’s helpful to someone.

Thanks

~~ Charles

2 Likes

I just came across a machine for which my script unexpectedly fails.

Apparently it’s getting an empty MAAS_RESOURCES_FILE, which of course fails to parse when I attempt to load_json the file.

Has something changed that wouldn’t load the resources file appropriately, or is this just a sole machine acting oddly?

I’m running the 3.2.4 snap.

I also tried to leave the host up so I could ssh in, but every time it powers off when commissionoing completes.

Thanks

~~ Charles

Does the 20-maas-03-machine-resources script succeed and reports output to maas, or is that one failing too?

1 Like

Hey folks. This is a great feature - working on kicking the tires on it now.

Question - I see that Custom is now (in the 3.3 beta) an option for the default storage type in the UI’s Settings page. Can you explain how that setting interacts with custom commissioning scripts?

For example, if I have something else set for storage default layout (let’s say, Flat), and I have a custom storage commissioning script, do I need to pick Custom as my default storage layout for that to trigger? If not, what is the use of picking the Custom storage default layout?

Also - maybe this is part of the updated docs push for 3.3, but is there a list of supported file system types?

Thanks!

I’ve been attempting to get this working too. I need to do a RAID with LVM and I can’t figure out how the scripts that others have posted. @bedfordc or @dadams-fg would it be possible to give me some idea’s on how to use the scripts you posted with a RAID1 config with LVM? Thanks

Okay - that’s a moderately complicated answer.

First of all - you’ll need an example of the json structure you’re trying to create. I’d suggest that you build a host the way you want then interrogate the output of the logs to get the final MAAS_RESOURCES_FILE and then build your python script to make it look like that.

The setup with variables and such you’ll have to figure out on your own tho :slight_smile:

Please post it here when you’re done - I might upgrade my script such that it can figure out when I need to use two disks in a raid configuration when hardware raid is inaccessible. (I think I have 3 hosts like that, so I just did them by hand, but if someone else does the work to figure it out I’ll gladly steal it!)

Thanks

~~ Charles

Late response here. I tried parsing the output of MAAS_RESOURCES_FILE and it really wasn’t usable. At least not in any way that I could easily understand and then build from. But, I’ve managed to come up with something that is ‘somewhat’ working. Although it’s such a hack I’m a bit embarassed to post what I have right now. I’m going to try and clean it up and post here what I’ve got working so far.

We’re currently looking at moving to MAAS from an in-house solution based (partially) on autoinstall. Autoinstall (which I’ll note is a also Canonical tool) has a really nice feature where you can specify a disk size of -1, which will use all of the remaining space. Is there any way to utilize autoinstall scripts in MAAS?

@bedfordc, this is indeed very useful! In fact, some of the questions you’ve asked in this post were the same questions I had, even after reading through a bunch of the documentation. (Googling for answers to this stuff is pretty fruitless)

1 Like

Most of the “documentation” is wrapped up in the code surrounding data manipulation. Just start with the details in the JSON files produced by MAAS for each host, and work backward from there.

If you know where you want to finish, that makes it an easy test case for your code :-p

Once I realized that, then the rest of my work to produce the configuration I required was a lot simpler - just looking up how to make Python do what I wanted to do with any data structures I maintained within the code as it ran.

I hope that answers some additional questions you may have :innocent:

Thanks

~~ Charles

1 Like

Is there any real documentation about this feature? Can dm_crypt devices be configured here? Is there no way to set a partition to 100% of remaining free space?

Resurrecting an older thread here, but an important one as I try and get MAAS working in my environment. It would be good to see some more documentation about how to define custom layouts, and moving further, it makes sense that all layouts would be editable from the MAAS UI, or even the CLI.

We are testing this procedure but so far it’s not working for us and in case we get it working it seems quite complex to assign different partition layouts to different machines, which is something we need.

Did anyone manage to define custom partition layouts using cloud-init ? cloud-init seems more flexible but we haven’t made it work either. Should it be possible to use cloud-init during the deployment step to define custom partitions layouts?

Hi @pescobar-scicore ,

nope, you can’t use cloud-init for that in MAAS.

1 Like

thanks @r00ta for confirming that we cannot use cloud-init. At least we won’t invest more time trying that approach

If I understand correctly the only approach for our use case (many different server models with different partitions layouts) is to write a commissioning script where we define the different partitions layout depending on the information in $MAAS_RESOURCES_FILE , right?

To be honest I don’t like the idea of “hiding” all this logic inside a commissioning script . I was expecting that MAAS would provide a simpler solution to assign different partition layouts to different nodes.