Injecting files in Ironic
This post is a follow-up to the earlier deploy steps tutorial, where I showed how to create a deploy step for injecting arbitrary files. I promised to bring that that particular deploy step upstream, and I've delivered on that promise!
Deploy steps and templates
As explained in the deploy steps tutorial, the deployment process consistes of deploy steps, which are executed sequentially according to their priority. Deploy steps can be grouped into deploy templates, mapping a trait name to a list of steps. I explained traits in my post on bare metal scheduling.
Until Wallaby, deploy templates were the only way to use custom deploy steps. In the Wallaby release 16.2.0 Ironic gained an ability to accept deploy steps in the provisioning API, like this (using ironicclient 4.6.0 or newer):
Which approach to take depends on your use case. If you're using Ironic behind the Compute API (Nova), you have to go with deploy templates, there is no way to pass deploy steps directly from Nova to Ironic.
Standalone users have both options. The benefit of deploy templates is that they're linked with traits. For example, if your deploy step depends on the hardware vendor, or model, or architecture, go with deploy templates and use traits to specify which nodes are compatible. If you use a pretty universal deploy step, like the one discussed here, you can go with the explicit argument, although a deploy template could simplify applying the same step over and over.
It is worth noting that you can use both approaches together, just not for the same step.
Inject files
The new inject_files
deploy step is an in-band step and is documented in
the ironic-python-agent hardware manager documentation. The deploy step
accepts a files
argument that can come explicitly from the deployment
request, from the deploy template or from inject_files
in the node's
properties
field. Deploy steps were demonstrated in the deploy steps
tutorial, here I will use the newer explicit approach.
The files
argument is a list of file objects. Each of them define the
location of the file, its content (if any) and access rights (if needed). I'll
concentrate only on the most aspects:
- path
-
is the target file path. If the file is located on a non-root partition, this
path
may be different from the actual path on the final system!For example, if you need to write to the EFI partition, you may end up having
path
equal to/EFI/centos/my.conf
for it to end up in/boot/efi/EFI/centos/my.conf
.Any parent directories will be created on the way.
- partition
-
is the partition on which the file is located.
The most reliable way is to use
/dev/disk/by-uuid/<filesystem UUID>
, where<filesystem UUID>
is the UUID of the file system (not GUID of the partition, that would be/dev/disk/by-partuuid
). You can analyze your image to figure out this UUID, I'll show it later on.If your file systems or partitions have labels, you can use them. For example, my laptop has
/dev/disk/by-partlabel/EFI\x20System\x20Partition
(yes, exactly like this, with\x20
in the path).Of course you can use standard Linux device names, like
/dev/sda1
, but these depend on the bus (SATA, NVMe, virtio), as well as the partition ordering. On the root device you can simply use numbers starting with1
.If you don't provide a
partition
, ironic-python-agent will try a heuristic based onpath
: take the first component of the path and try to find a partition with it. For example, for/etc/sysctl.d/example.conf
, it will look for a partition withetc
directory on it.If you want to write a file to the root of any partition or if you need all parent directories created, you have to specify
partition
. - content
-
is the content of the file in one of the two forms:
The actual binary content as a base64 encoded string (recommended only for small files).
A URL to the content (without base64 encoding).
A cool thing about URLs is that you can use some simple formatting to extract information from the node of the ports. I'll show an example later on.
Hello world
Let us start with the case from the tutorial. Imaging we want to change the message of the day to something positive:
Hello Ironic World!
For the list of deploy steps we'll use this file (inject_files.json
):
[ { "interface": "deploy", "step": "inject_files", "args": { "files": [ { "path": "/etc/motd", "content": "SGVsbG8gSXJvbmljIFdvcmxkIQo=" } ] }, "priority": 50 } ]
What is the priority and why 50? Read the deploy steps tutorial! Everything else should be more or less self-explanatory.
The metalsmith CLI does not have support for deploy steps yet, so start
the deployment manually using the new --deploy-steps
argument:
$ SSH_KEY=$(cat ~/.ssh/id_ed25519.pub) $ baremetal node set <node> --instance-info image_source=file:///httpboot/centos8.qcow2 $ baremetal node deploy <node> \ --config-drive '{"meta_data": {"public_keys": {"0": "'"$SSH_KEY"'"}}}' \ --deploy-steps inject_files.json $ watch baremetal node show <node> --fields provision_state deploy_step last_error
Once the provision state is active
, we can figure out the IP address and
log into the instance:
Target partition
What if we need to specify where to put the file? I'm struggling to invent an actual real-life example for it, so let us write a file to the EFI partition just because we can!
But how do we know where it will be located? Your image will probably give you
a clue. We will use the virt-filesystems tool (which on my CentOS 8 system
comes from the libguestfs-tools-c
package):
$ virt-filesystems -a centos8-uefi.qcow2 --filesystems --long --uuids Name Type VFS Label Size Parent UUID /dev/sda1 filesystem vfat MKFS_ESP 576716800 - 93C3-C214 /dev/sda3 filesystem ext4 cloudimg-rootfs 3293577216 - 3ef8f8c2-439f-4aad-86e1-6d2457364c24
Looking great! We can use both labels and UUIDs. Labels will probably be the same for any image created with diskimage-builder (I haven't verified this statement), UUIDs are more reliable within the same image.
Let us update our inject_files.json
adding a new hello-world quality file
and some parent directories to create:
[ { "interface": "deploy", "step": "inject_files", "args": { "files": [ { "path": "/etc/motd", "content": "SGVsbG8gSXJvbmljIFdvcmxkIQo=" }, { "path": "nonEFI/hello", "partition": "/dev/disk/by-uuid/93C3-C214", "content": "SGVsbG8gSXJvbmljIFdvcmxkIQo=" } ] }, "priority": 50 } ]
After deploying and logging into the system, we see the file in place:
Node-specific content
Imagine you're using deploy templates, but still want the file content to be
node-specific. How can we achieve that? Through passing a URL to content
.
Not only does it allow to fetch content remotely, it also supports simple
Python formatting with node
and ports
parameters, for example:
Fetch file based on UUID:
http://host/{node[uuid]}.bin
.Access a remote service with the first MAC:
http://host/?mac={ports[0][address]}
.
We will generate static files based on node names (which on a Bifrost testing
environment look like testvm<N>
where <N>
is a number starting with
1):
$ find /httpboot/ -name testvm\* -print -exec cat {} \; /httpboot/testvm1.txt I am node 1 /httpboot/testvm2.txt And I am node 2
Update the deploy steps again:
[ { "interface": "deploy", "step": "inject_files", "args": { "files": [ { "path": "/etc/motd", "content": "http://192.168.122.1:8080/{node[name]}.txt" } ] }, "priority": 50 } ]
Then create a deploy template and declare all nodes compatible with it:
$ baremetal deploy template create CUSTOM_INJECT_FILES --steps inject_files.json --max-width 100 +------------+-------------------------------------------------------------------------------------+ | Field | Value | +------------+-------------------------------------------------------------------------------------+ | created_at | 2021-02-17T15:43:34.660443+00:00 | | extra | {} | | name | CUSTOM_INJECT_FILES | | steps | [{'interface': 'deploy', 'step': 'inject_files', 'args': {'files': [{'path': | | | '/etc/motd', 'content': 'http://192.168.122.1:8080/{node[name]}.txt'}]}, | | | 'priority': 50}] | | updated_at | None | | uuid | 3ebf8dac-896b-4633-b965-d6e2830ff5cc | +------------+-------------------------------------------------------------------------------------+ $ for node in $(baremetal node list -f value --fields uuid); do baremetal node add trait $node CUSTOM_INJECT_FILES; done Added trait CUSTOM_INJECT_FILES Added trait CUSTOM_INJECT_FILES
Now we can use metalsmith to pick any suitable node and deploy on it:
$ metalsmith deploy --resource-class baremetal --trait CUSTOM_INJECT_FILES \ --image file:///httpboot/centos8.qcow2 --ssh-public-key ~/.ssh/id_ed25519.pub +--------------------------------------+-----------+--------------------------------------+----------+--------+--------------+ | UUID | Node Name | Allocation UUID | Hostname | State | IP Addresses | +--------------------------------------+-----------+--------------------------------------+----------+--------+--------------+ | 4e41df61-84b1-5856-bfb6-6b5f2cd3dd11 | testvm1 | f1be9ff3-965b-4500-bb7b-59673faba68a | testvm1 | ACTIVE | | +--------------------------------------+-----------+--------------------------------------+----------+--------+--------------+
As you see, this is node number 1, and as expected it greets us with:
I am node 1
Conclusion
Deploy steps are incredibly powerful, and the new inject_files
deploy step
is capable of demonstrating this power, as well as enriching the Ironic
operator's toolset.