Monday, January 3, 2011

Advanced cloud-init custom handlers

I love cloud-init, the Ubuntu cloud technology that enables a cloud instance to bootstrap itself and customize itself into whatever you want it to be (coming from a generic image). I had previously created a screencast introducing cloud-init, and written an article on running cloud-init locally over KVM. This time however, I demonstrate some advanced cloud-init foo, namely how to write a custom content handler in python. Pass the code and data over the cloud's user-data, and watch your code crunch on the data. The possibilities are endless, using this technique you are basically writing plugins for cloud-init allowing you to do almost anything. Ok, enough babbling, let's do some cool stuff

We need the "write-mime-multipart" script from cloud-init. I would not recommend installing cloud-init on your own machine, since cloud-init is designed to be run on cloud instances (not physical nodes). If you do install it on your own machine, it blocks the boot process waiting for the cloud userdata service to appear (which it never does), so you end up waiting a lot! To get the script, we just check out the code directly
bzr branch lp:cloud-init

You'll find that script in the tools/ directory. Assuming you could run cloud-init on local KVM, we now need to replace the user-data file, which in the previous article was written using cloud-config syntax, with a new file. The new user-data file is a multipart file composed of custom python code and data you want your python code to chew on! Here is how you create the file
./write-mime-multipart --output user-data part-handler.txt one:text/plain two:text/plain

Let's take a look at the contents of those files. Files "one" and "two" are the data, while "part-handler.txt" is the python code adapted from cloud-init. In our case, I chose to let our part handler be a "user-creation" provider, i.e. you supply a list of user names in the data file, the code loops over them creating them. Simple enough for an example I hope. Let's check out the data files
$ cat one
jhonny
cash
$ cat two 
agent
smith

These two files hold the 4 users to be created! I split them into 2 files, just to demo you could have multiple input files. Now let's check out the code living in part-handler.txt
#part-handler
# vi: syntax=python ts=4

def list_types():
    # return a list of mime-types that are handled by this module
    return(["text/plain", "text/go-cubs-go"])

def handle_part(data,ctype,filename,payload):
    # data: the cloudinit object
    # ctype: '__begin__', '__end__', or the specific mime-type of the part
    # filename: the filename for the part, or dynamically generated part if
    #           no filename is given attribute is present
    # payload: the content of the part (empty for begin or end)
    if ctype == "__begin__":
       print "my handler is beginning"
       return
    if ctype == "__end__":
       print "my handler is ending"
       return

    print "==== received ctype=%s filename=%s ====" % (ctype,filename)
    import os
    for user in payload.splitlines():
        print " == Creating user %s" % (user)
        os.system('sudo useradd -p ubuntu -m %s' % (user) )
    print "==== end ctype=%s filename=%s" % (ctype, filename)

Most of the code is just boiler plate. The code needs to implement two functions "list_types()" that returns a list of content types that this code can handle. In our case, we return "text/plain" and "text/go-cubs-go". Note that when we ran "write-mime-multipart" we used the mime type text/plain, and that is the reason cloud-init would invoke this code to handle it. Because the code advertises it can handle text/plain types. The second function the code needs to implement is handle_part. This is called at the very beginning and very end for initialization and tear-down. It is also called on each input file (2 times in our case). A sample run looks like

qemu-cloud-init-advanced
and sure enough, the 4 users were created
qemu-cloud-init-users-created

That should be everything you need to know about writing custom mime type handlers as extensions to cloud-init. Indeed that is some pretty amazing stuff! Any questions or comments, leave a comment

2 comments:

threebadwheels said...

Hi ...

I have been reading your blog to learn more about cloud-init. Your examples are very good. Thank you.

I do have a question though.

I have created a combined multipart user-data-file. As follows:


Content-Type: multipart/mixed; boundary="===============6765312861905666463=="
MIME-Version: 1.0

--===============6765312861905666463==
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="setup.sh"

#!/bin/sh

sudo apt-get -y install libaio1 ksh libstdc++6-4.4-dev libstdc++6-4.4-dbg libstdc++6-4.4-pic lib32stdc++6-4.4-dbg
sudo ln -s /lib/x86_64-linux-gnu/libc.so.6 /lib/libc.so.6

sudo groupadd db2iadm
sudo groupadd db2fadm
sudo groupadd db2adm
sudo groupadd pwcadm

sudo useradd -g db2iadm -m -s /usr/bin/ksh small
sudo useradd -g db2fadm -m -s /usr/bin/ksh fence
sudo useradd -g db2adm -m -s /usr/bin/ksh db2as
sudo useradd -g pwcadm -m -s /usr/bin/ksh pcenter

sudo mkdir /opt/informatica
sudo chown pcenter:pwcadm /opt/informatica

sudo cat small.profile | tee -a /home/small/.profile
sudo cat pcenter.profile | tee -a /home/pcenter/.profile
sudo cat sysctl.db2 | tee -a /etc/sysctl.conf
sudo cat ubuntu.profile | tee -a /home/ubuntu/.profile

--===============6765312861905666463==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="sysctl.db2"

# DB2 v9.5. entries

kernel.shmall = 1895720140
kernel.shmmax = 2106355712
kernel.msgmax = 65535
kernel.msgmnb = 65535

kernel.sem = 250 1250000 32 5000

--===============6765312861905666463==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="small.profile"


set -o vi

alias "start"="db2start"
alias "stop"="db2stop"

alias "activate"="db2 activate db pcdom ; db2 activate db pcrepos"
alias "deactivate"="db2 deactivate db pcdom ; db2 deactivate db pcrepos"

--===============6765312861905666463==

This is a portion of the combined multipart.

My issue is when I create an instance with this cloud-init it cannot find some of the files used in the script portion. Here is the output from the console.


Setting up libstdc++6-4.4-dev (4.4.5-15ubuntu1) ...^M
Setting up lib32stdc++6-4.4-dbg (4.4.5-15ubuntu1) ...^M
Setting up libstdc++6-4.4-pic (4.4.5-15ubuntu1) ...^M
Processing triggers for libc-bin ...^M
ldconfig deferred processing now taking place^M
cat: small.profile: No such file or directory^M
cat: pcenter.profile: No such file or directory^M
cat: sysctl.db2: No such file or directory^M
cat: ubuntu.profile: No such file or directory^M
ec2: ^M
ec2: #############################################################^M
ec2: -----BEGIN SSH HOST KEY FINGERPRINTS-----^M
ec2: 2048 50:f3:55:1d:57:13:2c:13:0c:0b:27:bb:99:50:db:f6 /etc/ssh/ssh_host_rsa_key.pub (RSA)^M
ec2: 1024 0a:ff:fc:3b:7b:d5:58:ee:83:7c:9b:cd:5d:99:03:a6 /etc/ssh/ssh_host_dsa_key.pub (DSA)^M
ec2: -----END SSH HOST KEY FINGERPRINTS-----^M
ec2: #############################################################^M
cloud-init boot finished at Mon, 31 Oct 2011 13:58:17 +0000. Up 182.53 seconds^M

Any help would be most appreciated.

Thanks!

Thomas

jdswift said...

We had some trouble writing files using this method (we wanted to be able to store some types of user data as local files). Writing files to /tmp/ would silently fail.

The issue is how early in the boot the multi-part handler is run. To avoid this, make sure you write to a location that is available early (like /run/shm).