← Back to team overview

cloud-init-dev team mailing list archive

[Merge] lp:~vlastimil-holer/cloud-init/opennebula into lp:cloud-init

 

Vlastimil Holer has proposed merging lp:~vlastimil-holer/cloud-init/opennebula into lp:cloud-init.

Requested reviews:
  cloud init development team (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~vlastimil-holer/cloud-init/opennebula/+merge/127730

These patches are modified existing OpenStacks' ConfigDrive datasource to suite requirements of the OpenNebula (http://opennebula.org/). All copyrights are preserved.

In OpenNebula context variables aren't formalized as in OpenStack, people mostly write their own contextualization scripts and recommends image users what context variables should be exported. I have taken variables seen in most examples (SSH_KEY, HOSTNAME, PUBLIC_IP) to control cloud-init's behaviour.
-- 
https://code.launchpad.net/~vlastimil-holer/cloud-init/opennebula/+merge/127730
Your team cloud init development team is requested to review the proposed merge of lp:~vlastimil-holer/cloud-init/opennebula into lp:cloud-init.
=== modified file 'cloudinit/settings.py'
--- cloudinit/settings.py	2012-08-20 05:28:14 +0000
+++ cloudinit/settings.py	2012-10-03 11:57:23 +0000
@@ -31,6 +31,7 @@
     'datasource_list': [
         'NoCloud',
         'ConfigDrive',
+        'OpenNebula',
         'AltCloud',
         'OVF',
         'MAAS',

=== added file 'cloudinit/sources/DataSourceOpenNebula.py'
--- cloudinit/sources/DataSourceOpenNebula.py	1970-01-01 00:00:00 +0000
+++ cloudinit/sources/DataSourceOpenNebula.py	2012-10-03 11:57:23 +0000
@@ -0,0 +1,235 @@
+# vi: ts=4 expandtab
+#
+#    Copyright (C) 2012 Canonical Ltd.
+#    Copyright (C) 2012 Yahoo! Inc.
+#    Copyright (C) 2012 CERIT Scientific Cloud
+#
+#    Author: Scott Moser <scott.moser@xxxxxxxxxxxxx>
+#    Author: Joshua Harlow <harlowja@xxxxxxxxxxxxx>
+#    Author: Vlastimil Holer <xholer@xxxxxxxxxxxx>
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    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 General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import subprocess
+
+from cloudinit import log as logging
+from cloudinit import sources
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+
+DEFAULT_IID = "iid-dsopennebula"
+CONTEXT_DISK_FILES = ["context.sh"]
+VALID_DSMODES = ("local", "net", "disabled")
+
+class DataSourceOpenNebula(sources.DataSource):
+    def __init__(self, sys_cfg, distro, paths):
+        sources.DataSource.__init__(self, sys_cfg, distro, paths)
+        self.dsmode = 'local'
+        self.seed = None
+        self.seed_dir = os.path.join(paths.seed_dir, 'opennebula')
+
+    def __str__(self):
+        mstr = "%s [seed=%s][dsmode=%s]" % (util.obj_name(self),
+                                            self.seed, self.dsmode)
+        return mstr
+
+    def get_data(self):
+        defaults = {
+            "instance-id": DEFAULT_IID,
+            "dsmode": self.dsmode,
+        }
+
+        found = None
+        md = {}
+
+        results = {}
+        if os.path.isdir(self.seed_dir):
+            try:
+                results=read_on_context_device_dir(self.seed_dir)
+                found = self.seed_dir
+            except NonContextDeviceDir:
+                util.logexc(LOG, "Failed reading context device from %s",
+                            self.seed_dir)
+        if not found:
+            devlist = find_candidate_devs()
+            for dev in devlist:
+                try:
+                    results = util.mount_cb(dev, read_context_disk_dir)
+                    found = dev
+                    break
+                except (NonConfigDriveDir, util.MountFailedError):
+                    pass
+                except BrokenConfigDriveDir:
+                    util.logexc(LOG, "broken config drive: %s", dev)
+
+        if not found:
+            return False
+
+        md = results['metadata']
+        md = util.mergedict(md, defaults)
+
+        dsmode = results.get('dsmode', None)
+        if dsmode not in VALID_DSMODES + (None,):
+            LOG.warn("user specified invalid mode: %s" % dsmode)
+            dsmode = None
+
+        if (dsmode is None) and self.ds_cfg.get('dsmode'):
+            dsmode = self.ds_cfg.get('dsmode')
+        else:
+            dsmode = self.dsmode
+
+        if dsmode == "disabled":
+            # most likely user specified
+            return False
+
+        if dsmode != self.dsmode:
+            LOG.debug("%s: not claiming datasource, dsmode=%s", self, dsmode)
+            return False
+
+        self.seed = found
+        self.metadata = md
+        self.userdata_raw = results.get('userdata')
+
+        return True
+
+    def get_hostname(self, fqdn=False, resolve_ip=None):
+        if resolve_ip is None:
+            if self.dsmode == 'net':
+                resolve_ip = True
+            else:
+                resolve_ip = False
+        return sources.DataSource.get_hostname(self, fqdn, resolve_ip)
+
+
+class DataSourceOpenNebulaNet(DataSourceOpenNebula):
+    def __init__(self, sys_cfg, distro, paths):
+        DataSourceOpenNebula.__init__(self, sys_cfg, distro, paths)
+        self.dsmode = 'net'
+
+
+class NonContextDeviceDir(Exception):
+    pass
+
+
+def find_candidate_devs():
+    """
+    Return a list of devices that may contain the context disk.
+    """
+    by_fstype = util.find_devs_with("TYPE=iso9660")
+    by_label = util.find_devs_with("LABEL=CDROM")
+
+    by_fstype.sort()
+    by_label.sort()
+
+    # combine list of items by putting by-label items first
+    # followed by fstype items, but with dupes removed
+    combined = (by_label + [d for d in by_fstype if d not in by_label])
+
+    # We are looking for block device (sda, not sda1), ignore partitions
+    combined = [d for d in combined if d[-1] not in "0123456789"]
+
+    return combined
+
+
+def read_context_disk_dir(source_dir):
+    """
+    read_context_disk_dir(source_dir):
+    read source_dir and return a tuple with metadata dict and user-data
+    string populated.  If not a valid dir, raise a NonContextDeviceDir
+    """
+
+    found = {}
+    for af in CONTEXT_DISK_FILES:
+        fn = os.path.join(source_dir, af)
+        if os.path.isfile(fn):
+            found[af] = fn
+
+    if len(found) == 0:
+        raise NonContextDeviceDir("%s: %s" % (source_dir, "no files found"))
+
+    context_sh = {}
+    results = {
+        'userdata':None,
+        'metadata':{},
+    }
+
+    if "context.sh" in found:
+        # let bash process the contextualization script;
+        # write out data in normalized output NAME=\$?'?VALUE'?
+        # TODO: don't trust context.sh! parse manually !!!
+        try:
+            BASH_CMD='VARS=`set | sort -u `;' \
+                '. %s/context.sh;' \
+                'comm -23 <(set | sort -u) <(echo "$VARS") | egrep -v "^(VARS|PIPESTATUS|_)="'
+
+            (out,err) = util.subp(['bash',
+                '--noprofile',
+                '--norc',
+                '-c',
+                BASH_CMD % (source_dir) ])
+
+            for (key,value) in [ l.split('=',1) for l in out.rstrip().split("\n") ]:
+                # with backslash escapes
+                r=re.match("^\$'(.*)'$",value)
+                if r:
+                    context_sh[key.lower()]=r.group(1).\
+                        replace('\\\\','\\').\
+                        replace('\\t','\t').\
+                        replace('\\n','\n').\
+                        replace("\\'","'")
+                else:
+                    # multiword values
+                    r=re.match("^'(.*)'$",value)
+                    if r:
+                        context_sh[key.lower()]=r.group(1)
+                    else:
+                        # simple values
+                        context_sh[key.lower()]=value
+        except subprocess.CalledProcessError as exc:
+            LOG.warn("context script faled to read" % (exc.output[1]))
+        results['metadata']=context_sh
+
+    # process single or multiple SSH keys
+    if "ssh_key" in context_sh:
+        lines = context_sh.get('ssh_key').splitlines()
+        results['metadata']['public-keys'] = [l for l in lines
+            if len(l) and not l.startswith("#")]
+
+    # custom hostname
+    if 'hostname' in context_sh:
+        results['metadata']['local-hostname'] = context_sh['hostname']
+    elif 'public_ip'in context_sh:
+        results['metadata']['local-hostname'] = context_sh['public_ip']
+
+    # raw user data
+    if "user_data" in context_sh:
+        results['userdata'] = context_sh["user_data"]
+    elif "userdata" in context_sh:
+        results['userdata'] = context_sh["userdata"]
+
+    return results
+
+
+# Used to match classes to dependencies
+datasources = [
+    (DataSourceOpenNebula, (sources.DEP_FILESYSTEM, )),
+    (DataSourceOpenNebulaNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
+]
+
+
+# Return a list of data sources that match this set of dependencies
+def get_datasource_list(depends):
+    return sources.list_from_depends(depends, datasources)

=== modified file 'cloudinit/sources/__init__.py'
--- cloudinit/sources/__init__.py	2012-09-24 20:54:51 +0000
+++ cloudinit/sources/__init__.py	2012-10-03 11:57:23 +0000
@@ -149,7 +149,7 @@
             return "iid-datasource"
         return str(self.metadata['instance-id'])
 
-    def get_hostname(self, fqdn=False):
+    def get_hostname(self, fqdn=False, resolve_ip=False):
         defdomain = "localdomain"
         defhost = "localhost"
         domain = defdomain
@@ -173,7 +173,18 @@
             # make up a hostname (LP: #475354) in format ip-xx.xx.xx.xx
             lhost = self.metadata['local-hostname']
             if util.is_ipv4(lhost):
+<<<<<<< TREE
                 toks = ["ip-%s" % lhost.replace(".", "-")]
+=======
+                toks = []
+                if resolve_ip:
+                    toks = util.gethostbyaddr(lhost)
+
+                if toks:
+                    toks = toks.split('.')
+                else:
+                    toks = ["ip-%s" % lhost.replace(".", "-")]
+>>>>>>> MERGE-SOURCE
             else:
                 toks = lhost.split(".")
 

=== modified file 'cloudinit/util.py'
--- cloudinit/util.py	2012-09-28 21:21:02 +0000
+++ cloudinit/util.py	2012-10-03 11:57:23 +0000
@@ -905,6 +905,13 @@
     return hostname
 
 
+def gethostbyaddr(ip):
+    try:
+        return socket.gethostbyaddr(ip)[0]
+    except socket.herror:
+        return None
+
+
 def is_resolvable_url(url):
     """determine if this url is resolvable (existing or ip)."""
     return (is_resolvable(urlparse.urlparse(url).hostname))

=== added directory 'doc/sources/opennebula'
=== added file 'doc/sources/opennebula/README'
--- doc/sources/opennebula/README	1970-01-01 00:00:00 +0000
+++ doc/sources/opennebula/README	2012-10-03 11:57:23 +0000
@@ -0,0 +1,66 @@
+The 'OpenNebula' DataSource supports the OpenNebula contextualization disk.
+
+The following criteria are required to be identified by
+DataSourceOpenNebula as contextualization disk:
+  * must be formatted with iso9660 filesystem or labeled as CDROM
+  * must be un-partitioned block device (/dev/vdb, not /dev/vdb1)
+  * must contain
+     * context.sh
+
+== Content of config-drive ==
+  * context.sh
+    This is the only mandatory file on context disk, the rest content depends
+    on contextualization parameter FILES and thus is optional. It's
+    a shell script defining all context parameters. This script is
+    processed by bash (/bin/bash) to simulate behaviour of common
+    OpenNebula context scripts. Processed variables are handed over
+    back to cloud-init for further processing.
+
+== Configuration ==
+Cloud-init's behaviour can be modified by context variables found
+in the context.sh file in the folowing ways (variable names are
+case-insensitive):
+  * dsmode:
+    values: local, net, disabled
+    default: None
+
+    Tells if this datasource will be processed in local (pre-networking) or
+    net (post-networking) stage or even completely disabled.
+
+  * ssh_key:
+    default: None
+    If present, these key(s) will be used as the public key(s) for
+    the instance. More keys can be specified in this single context
+    variable, but each key must be on it's own line. I.e. keys must
+    be separated by newlines.
+
+  * hostname:
+    default: None
+    Custom hostname for the instance.
+
+  * public_ip:
+    default: None
+    If hostname not specified, public_ip is used to DNS resolve hostname.
+
+  * 'user_data' or 'userdata':
+    default: None
+    This provides cloud-init user-data. See other documentation for what
+    all can be present here.
+
+== Example OpenNebula's Virtual Machine template ==
+
+CONTEXT=[
+  PUBLIC_IP="$NIC[IP]",
+  SSH_KEY="$USER[SSH_KEY] 
+$USER[SSH_KEY1] 
+$USER[SSH_KEY2] ",
+  USER_DATA="#cloud-config
+# see https://help.ubuntu.com/community/CloudInit
+
+packages: []
+
+mounts:
+- [vdc,none,swap,sw,0,0]
+runcmd:
+- echo 'Instance has been configured by cloud-init.' | wall
+" ]