summaryrefslogtreecommitdiff
path: root/cloudinit/sources
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/sources')
-rw-r--r--cloudinit/sources/DataSourceAltCloud.py31
-rw-r--r--cloudinit/sources/DataSourceAzure.py485
-rw-r--r--cloudinit/sources/DataSourceCloudStack.py6
-rw-r--r--cloudinit/sources/DataSourceNoCloud.py6
-rw-r--r--cloudinit/sources/DataSourceSmartOS.py195
-rw-r--r--cloudinit/sources/__init__.py3
6 files changed, 705 insertions, 21 deletions
diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py
index 64548d43..a834f8eb 100644
--- a/cloudinit/sources/DataSourceAltCloud.py
+++ b/cloudinit/sources/DataSourceAltCloud.py
@@ -1,10 +1,11 @@
# vi: ts=4 expandtab
#
# Copyright (C) 2009-2010 Canonical Ltd.
-# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+# Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
# Copyright (C) 2012 Yahoo! Inc.
#
# Author: Joe VLcek <JVLcek@RedHat.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
#
# 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
@@ -79,7 +80,7 @@ def read_user_data_callback(mount_dir):
try:
user_data = util.load_file(user_data_file).strip()
except IOError:
- util.logexc(LOG, ('Failed accessing user data file.'))
+ util.logexc(LOG, 'Failed accessing user data file.')
return None
return user_data
@@ -178,7 +179,7 @@ class DataSourceAltCloud(sources.DataSource):
return False
# No user data found
- util.logexc(LOG, ('Failed accessing user data.'))
+ util.logexc(LOG, 'Failed accessing user data.')
return False
def user_data_rhevm(self):
@@ -205,12 +206,12 @@ class DataSourceAltCloud(sources.DataSource):
(cmd_out, _err) = util.subp(cmd)
LOG.debug(('Command: %s\nOutput%s') % (' '.join(cmd), cmd_out))
except ProcessExecutionError, _err:
- util.logexc(LOG, (('Failed command: %s\n%s') % \
- (' '.join(cmd), _err.message)))
+ util.logexc(LOG, 'Failed command: %s\n%s', ' '.join(cmd),
+ _err.message)
return False
except OSError, _err:
- util.logexc(LOG, (('Failed command: %s\n%s') % \
- (' '.join(cmd), _err.message)))
+ util.logexc(LOG, 'Failed command: %s\n%s', ' '.join(cmd),
+ _err.message)
return False
floppy_dev = '/dev/fd0'
@@ -222,12 +223,12 @@ class DataSourceAltCloud(sources.DataSource):
(cmd_out, _err) = util.subp(cmd)
LOG.debug(('Command: %s\nOutput%s') % (' '.join(cmd), cmd_out))
except ProcessExecutionError, _err:
- util.logexc(LOG, (('Failed command: %s\n%s') % \
- (' '.join(cmd), _err.message)))
+ util.logexc(LOG, 'Failed command: %s\n%s', ' '.join(cmd),
+ _err.message)
return False
except OSError, _err:
- util.logexc(LOG, (('Failed command: %s\n%s') % \
- (' '.join(cmd), _err.message)))
+ util.logexc(LOG, 'Failed command: %s\n%s', ' '.join(cmd),
+ _err.message)
return False
try:
@@ -236,8 +237,8 @@ class DataSourceAltCloud(sources.DataSource):
if err.errno != errno.ENOENT:
raise
except util.MountFailedError:
- util.logexc(LOG, ("Failed to mount %s"
- " when looking for user data"), floppy_dev)
+ util.logexc(LOG, "Failed to mount %s when looking for user data",
+ floppy_dev)
self.userdata_raw = return_str
self.metadata = META_DATA_NOT_SUPPORTED
@@ -272,8 +273,8 @@ class DataSourceAltCloud(sources.DataSource):
if err.errno != errno.ENOENT:
raise
except util.MountFailedError:
- util.logexc(LOG, ("Failed to mount %s"
- " when looking for user data"), cdrom_dev)
+ util.logexc(LOG, "Failed to mount %s when looking for user "
+ "data", cdrom_dev)
self.userdata_raw = return_str
self.metadata = META_DATA_NOT_SUPPORTED
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
new file mode 100644
index 00000000..d4863429
--- /dev/null
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -0,0 +1,485 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2013 Canonical Ltd.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+#
+# 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 base64
+import os
+import os.path
+import time
+from xml.dom import minidom
+
+from cloudinit import log as logging
+from cloudinit import sources
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+
+DS_NAME = 'Azure'
+DEFAULT_METADATA = {"instance-id": "iid-AZURE-NODE"}
+AGENT_START = ['service', 'walinuxagent', 'start']
+BOUNCE_COMMAND = ("i=$interface; x=0; ifdown $i || x=$?; "
+ "ifup $i || x=$?; exit $x")
+BUILTIN_DS_CONFIG = {
+ 'agent_command': AGENT_START,
+ 'data_dir': "/var/lib/waagent",
+ 'set_hostname': True,
+ 'hostname_bounce': {
+ 'interface': 'eth0',
+ 'policy': True,
+ 'command': BOUNCE_COMMAND,
+ 'hostname_command': 'hostname',
+ }
+}
+DS_CFG_PATH = ['datasource', DS_NAME]
+
+
+class DataSourceAzureNet(sources.DataSource):
+ def __init__(self, sys_cfg, distro, paths):
+ sources.DataSource.__init__(self, sys_cfg, distro, paths)
+ self.seed_dir = os.path.join(paths.seed_dir, 'azure')
+ self.cfg = {}
+ self.seed = None
+ self.ds_cfg = util.mergemanydict([
+ util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}),
+ BUILTIN_DS_CONFIG])
+
+ def __str__(self):
+ root = sources.DataSource.__str__(self)
+ return "%s [seed=%s]" % (root, self.seed)
+
+ def get_data(self):
+ # azure removes/ejects the cdrom containing the ovf-env.xml
+ # file on reboot. So, in order to successfully reboot we
+ # need to look in the datadir and consider that valid
+ ddir = self.ds_cfg['data_dir']
+
+ candidates = [self.seed_dir]
+ candidates.extend(list_possible_azure_ds_devs())
+ if ddir:
+ candidates.append(ddir)
+
+ found = None
+
+ for cdev in candidates:
+ try:
+ if cdev.startswith("/dev/"):
+ ret = util.mount_cb(cdev, load_azure_ds_dir)
+ else:
+ ret = load_azure_ds_dir(cdev)
+
+ except NonAzureDataSource:
+ continue
+ except BrokenAzureDataSource as exc:
+ raise exc
+ except util.MountFailedError:
+ LOG.warn("%s was not mountable" % cdev)
+ continue
+
+ (md, self.userdata_raw, cfg, files) = ret
+ self.seed = cdev
+ self.metadata = util.mergemanydict([md, DEFAULT_METADATA])
+ self.cfg = cfg
+ found = cdev
+
+ LOG.debug("found datasource in %s", cdev)
+ break
+
+ if not found:
+ return False
+
+ if found == ddir:
+ LOG.debug("using files cached in %s", ddir)
+
+ # now update ds_cfg to reflect contents pass in config
+ usercfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {})
+ self.ds_cfg = util.mergemanydict([usercfg, self.ds_cfg])
+ mycfg = self.ds_cfg
+
+ # walinux agent writes files world readable, but expects
+ # the directory to be protected.
+ write_files(mycfg['data_dir'], files, dirmode=0700)
+
+ # handle the hostname 'publishing'
+ try:
+ handle_set_hostname(mycfg.get('set_hostname'),
+ self.metadata.get('local-hostname'),
+ mycfg['hostname_bounce'])
+ except Exception as e:
+ LOG.warn("Failed publishing hostname: %s" % e)
+ util.logexc(LOG, "handling set_hostname failed")
+
+ try:
+ invoke_agent(mycfg['agent_command'])
+ except util.ProcessExecutionError:
+ # claim the datasource even if the command failed
+ util.logexc(LOG, "agent command '%s' failed.",
+ mycfg['agent_command'])
+
+ shcfgxml = os.path.join(mycfg['data_dir'], "SharedConfig.xml")
+ wait_for = [shcfgxml]
+
+ fp_files = []
+ for pk in self.cfg.get('_pubkeys', []):
+ bname = pk['fingerprint'] + ".crt"
+ fp_files += [os.path.join(mycfg['data_dir'], bname)]
+
+ start = time.time()
+ missing = wait_for_files(wait_for + fp_files)
+ if len(missing):
+ LOG.warn("Did not find files, but going on: %s", missing)
+ else:
+ LOG.debug("waited %.3f seconds for %d files to appear",
+ time.time() - start, len(wait_for))
+
+ if shcfgxml in missing:
+ LOG.warn("SharedConfig.xml missing, using static instance-id")
+ else:
+ try:
+ self.metadata['instance-id'] = iid_from_shared_config(shcfgxml)
+ except ValueError as e:
+ LOG.warn("failed to get instance id in %s: %s" % (shcfgxml, e))
+
+ pubkeys = pubkeys_from_crt_files(fp_files)
+
+ self.metadata['public-keys'] = pubkeys
+
+ return True
+
+ def get_config_obj(self):
+ return self.cfg
+
+
+def handle_set_hostname(enabled, hostname, cfg):
+ if not util.is_true(enabled):
+ return
+
+ if not hostname:
+ LOG.warn("set_hostname was true but no local-hostname")
+ return
+
+ apply_hostname_bounce(hostname=hostname, policy=cfg['policy'],
+ interface=cfg['interface'],
+ command=cfg['command'],
+ hostname_command=cfg['hostname_command'])
+
+
+def apply_hostname_bounce(hostname, policy, interface, command,
+ hostname_command="hostname"):
+ # set the hostname to 'hostname' if it is not already set to that.
+ # then, if policy is not off, bounce the interface using command
+ prev_hostname = util.subp(hostname_command, capture=True)[0].strip()
+
+ util.subp([hostname_command, hostname])
+
+ if util.is_false(policy):
+ return
+
+ if prev_hostname == hostname and policy != "force":
+ return
+
+ env = os.environ.copy()
+ env['interface'] = interface
+
+ if command == "builtin":
+ command = BOUNCE_COMMAND
+
+ util.subp(command, shell=(not isinstance(command, list)), capture=True)
+
+
+def crtfile_to_pubkey(fname):
+ pipeline = ('openssl x509 -noout -pubkey < "$0" |'
+ 'ssh-keygen -i -m PKCS8 -f /dev/stdin')
+ (out, _err) = util.subp(['sh', '-c', pipeline, fname], capture=True)
+ return out.rstrip()
+
+
+def pubkeys_from_crt_files(flist):
+ pubkeys = []
+ errors = []
+ for fname in flist:
+ try:
+ pubkeys.append(crtfile_to_pubkey(fname))
+ except util.ProcessExecutionError:
+ errors.extend(fname)
+
+ if errors:
+ LOG.warn("failed to convert the crt files to pubkey: %s" % errors)
+
+ return pubkeys
+
+
+def wait_for_files(flist, maxwait=60, naplen=.5):
+ need = set(flist)
+ waited = 0
+ while waited < maxwait:
+ need -= set([f for f in need if os.path.exists(f)])
+ if len(need) == 0:
+ return []
+ time.sleep(naplen)
+ waited += naplen
+ return need
+
+
+def write_files(datadir, files, dirmode=None):
+ if not datadir:
+ return
+ if not files:
+ files = {}
+ util.ensure_dir(datadir, dirmode)
+ for (name, content) in files.items():
+ util.write_file(filename=os.path.join(datadir, name),
+ content=content, mode=0600)
+
+
+def invoke_agent(cmd):
+ # this is a function itself to simplify patching it for test
+ if cmd:
+ LOG.debug("invoking agent: %s" % cmd)
+ util.subp(cmd, shell=(not isinstance(cmd, list)))
+ else:
+ LOG.debug("not invoking agent")
+
+
+def find_child(node, filter_func):
+ ret = []
+ if not node.hasChildNodes():
+ return ret
+ for child in node.childNodes:
+ if filter_func(child):
+ ret.append(child)
+ return ret
+
+
+def load_azure_ovf_pubkeys(sshnode):
+ # This parses a 'SSH' node formatted like below, and returns
+ # an array of dicts.
+ # [{'fp': '6BE7A7C3C8A8F4B123CCA5D0C2F1BE4CA7B63ED7',
+ # 'path': 'where/to/go'}]
+ #
+ # <SSH><PublicKeys>
+ # <PublicKey><Fingerprint>ABC</FingerPrint><Path>/ABC</Path>
+ # ...
+ # </PublicKeys></SSH>
+ results = find_child(sshnode, lambda n: n.localName == "PublicKeys")
+ if len(results) == 0:
+ return []
+ if len(results) > 1:
+ raise BrokenAzureDataSource("Multiple 'PublicKeys'(%s) in SSH node" %
+ len(results))
+
+ pubkeys_node = results[0]
+ pubkeys = find_child(pubkeys_node, lambda n: n.localName == "PublicKey")
+
+ if len(pubkeys) == 0:
+ return []
+
+ found = []
+ text_node = minidom.Document.TEXT_NODE
+
+ for pk_node in pubkeys:
+ if not pk_node.hasChildNodes():
+ continue
+ cur = {'fingerprint': "", 'path': ""}
+ for child in pk_node.childNodes:
+ if (child.nodeType == text_node or not child.localName):
+ continue
+
+ name = child.localName.lower()
+
+ if name not in cur.keys():
+ continue
+
+ if (len(child.childNodes) != 1 or
+ child.childNodes[0].nodeType != text_node):
+ continue
+
+ cur[name] = child.childNodes[0].wholeText.strip()
+ found.append(cur)
+
+ return found
+
+
+def single_node_at_path(node, pathlist):
+ curnode = node
+ for tok in pathlist:
+ results = find_child(curnode, lambda n: n.localName == tok)
+ if len(results) == 0:
+ raise ValueError("missing %s token in %s" % (tok, str(pathlist)))
+ if len(results) > 1:
+ raise ValueError("found %s nodes of type %s looking for %s" %
+ (len(results), tok, str(pathlist)))
+ curnode = results[0]
+
+ return curnode
+
+
+def read_azure_ovf(contents):
+ try:
+ dom = minidom.parseString(contents)
+ except Exception as e:
+ raise NonAzureDataSource("invalid xml: %s" % e)
+
+ results = find_child(dom.documentElement,
+ lambda n: n.localName == "ProvisioningSection")
+
+ if len(results) == 0:
+ raise NonAzureDataSource("No ProvisioningSection")
+ if len(results) > 1:
+ raise BrokenAzureDataSource("found '%d' ProvisioningSection items" %
+ len(results))
+ provSection = results[0]
+
+ lpcs_nodes = find_child(provSection,
+ lambda n: n.localName == "LinuxProvisioningConfigurationSet")
+
+ if len(results) == 0:
+ raise NonAzureDataSource("No LinuxProvisioningConfigurationSet")
+ if len(results) > 1:
+ raise BrokenAzureDataSource("found '%d' %ss" %
+ ("LinuxProvisioningConfigurationSet",
+ len(results)))
+ lpcs = lpcs_nodes[0]
+
+ if not lpcs.hasChildNodes():
+ raise BrokenAzureDataSource("no child nodes of configuration set")
+
+ md_props = 'seedfrom'
+ md = {'azure_data': {}}
+ cfg = {}
+ ud = ""
+ password = None
+ username = None
+
+ for child in lpcs.childNodes:
+ if child.nodeType == dom.TEXT_NODE or not child.localName:
+ continue
+
+ name = child.localName.lower()
+
+ simple = False
+ value = ""
+ if (len(child.childNodes) == 1 and
+ child.childNodes[0].nodeType == dom.TEXT_NODE):
+ simple = True
+ value = child.childNodes[0].wholeText
+
+ attrs = {k: v for k, v in child.attributes.items()}
+
+ # we accept either UserData or CustomData. If both are present
+ # then behavior is undefined.
+ if (name == "userdata" or name == "customdata"):
+ if attrs.get('encoding') in (None, "base64"):
+ ud = base64.b64decode(''.join(value.split()))
+ else:
+ ud = value
+ elif name == "username":
+ username = value
+ elif name == "userpassword":
+ password = value
+ elif name == "hostname":
+ md['local-hostname'] = value
+ elif name == "dscfg":
+ if attrs.get('encoding') in (None, "base64"):
+ dscfg = base64.b64decode(''.join(value.split()))
+ else:
+ dscfg = value
+ cfg['datasource'] = {DS_NAME: util.load_yaml(dscfg, default={})}
+ elif name == "ssh":
+ cfg['_pubkeys'] = load_azure_ovf_pubkeys(child)
+ elif name == "disablesshpasswordauthentication":
+ cfg['ssh_pwauth'] = util.is_false(value)
+ elif simple:
+ if name in md_props:
+ md[name] = value
+ else:
+ md['azure_data'][name] = value
+
+ defuser = {}
+ if username:
+ defuser['name'] = username
+ if password:
+ defuser['password'] = password
+ defuser['lock_passwd'] = False
+
+ if defuser:
+ cfg['system_info'] = {'default_user': defuser}
+
+ if 'ssh_pwauth' not in cfg and password:
+ cfg['ssh_pwauth'] = True
+
+ return (md, ud, cfg)
+
+
+def list_possible_azure_ds_devs():
+ # return a sorted list of devices that might have a azure datasource
+ devlist = []
+ for fstype in ("iso9660", "udf"):
+ devlist.extend(util.find_devs_with("TYPE=%s" % fstype))
+
+ devlist.sort(reverse=True)
+ return devlist
+
+
+def load_azure_ds_dir(source_dir):
+ ovf_file = os.path.join(source_dir, "ovf-env.xml")
+
+ if not os.path.isfile(ovf_file):
+ raise NonAzureDataSource("No ovf-env file found")
+
+ with open(ovf_file, "r") as fp:
+ contents = fp.read()
+
+ md, ud, cfg = read_azure_ovf(contents)
+ return (md, ud, cfg, {'ovf-env.xml': contents})
+
+
+def iid_from_shared_config(path):
+ with open(path, "rb") as fp:
+ content = fp.read()
+ return iid_from_shared_config_content(content)
+
+
+def iid_from_shared_config_content(content):
+ """
+ find INSTANCE_ID in:
+ <?xml version="1.0" encoding="utf-8"?>
+ <SharedConfig version="1.0.0.0" goalStateIncarnation="1">
+ <Deployment name="INSTANCE_ID" guid="{...}" incarnation="0">
+ <Service name="..." guid="{00000000-0000-0000-0000-000000000000}" />
+ """
+ dom = minidom.parseString(content)
+ depnode = single_node_at_path(dom, ["SharedConfig", "Deployment"])
+ return depnode.attributes.get('name').value
+
+
+class BrokenAzureDataSource(Exception):
+ pass
+
+
+class NonAzureDataSource(Exception):
+ pass
+
+
+# Used to match classes to dependencies
+datasources = [
+ (DataSourceAzureNet, (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)
diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 81c8cda9..08f661e4 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -4,11 +4,13 @@
# Copyright (C) 2012 Cosmin Luta
# Copyright (C) 2012 Yahoo! Inc.
# Copyright (C) 2012 Gerard Dethier
+# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Cosmin Luta <q4break@gmail.com>
# Author: Scott Moser <scott.moser@canonical.com>
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
# Author: Gerard Dethier <g.dethier@gmail.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
#
# 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
@@ -109,8 +111,8 @@ class DataSourceCloudStack(sources.DataSource):
int(time.time() - start_time))
return True
except Exception:
- util.logexc(LOG, ('Failed fetching from metadata '
- 'service %s'), self.metadata_address)
+ util.logexc(LOG, 'Failed fetching from metadata service %s',
+ self.metadata_address)
return False
def get_instance_id(self):
diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py
index 084abca7..4ef92a56 100644
--- a/cloudinit/sources/DataSourceNoCloud.py
+++ b/cloudinit/sources/DataSourceNoCloud.py
@@ -1,7 +1,7 @@
# vi: ts=4 expandtab
#
# Copyright (C) 2009-2010 Canonical Ltd.
-# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+# Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
# Copyright (C) 2012 Yahoo! Inc.
#
# Author: Scott Moser <scott.moser@canonical.com>
@@ -119,8 +119,8 @@ class DataSourceNoCloud(sources.DataSource):
if e.errno != errno.ENOENT:
raise
except util.MountFailedError:
- util.logexc(LOG, ("Failed to mount %s"
- " when looking for data"), dev)
+ util.logexc(LOG, "Failed to mount %s when looking for "
+ "data", dev)
# There was no indication on kernel cmdline or data
# in the seeddir suggesting this handler should be used.
diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
new file mode 100644
index 00000000..1ce20c10
--- /dev/null
+++ b/cloudinit/sources/DataSourceSmartOS.py
@@ -0,0 +1,195 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2013 Canonical Ltd.
+#
+# Author: Ben Howard <ben.howard@canonical.com>
+#
+# 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/>.
+#
+#
+# Datasource for provisioning on SmartOS. This works on Joyent
+# and public/private Clouds using SmartOS.
+#
+# SmartOS hosts use a serial console (/dev/ttyS1) on Linux Guests.
+# The meta-data is transmitted via key/value pairs made by
+# requests on the console. For example, to get the hostname, you
+# would send "GET hostname" on /dev/ttyS1.
+#
+
+
+from cloudinit import log as logging
+from cloudinit import sources
+from cloudinit import util
+import os
+import os.path
+import serial
+
+
+DEF_TTY_LOC = '/dev/ttyS1'
+DEF_TTY_TIMEOUT = 60
+LOG = logging.getLogger(__name__)
+
+SMARTOS_ATTRIB_MAP = {
+ #Cloud-init Key : (SmartOS Key, Strip line endings)
+ 'local-hostname': ('hostname', True),
+ 'public-keys': ('root_authorized_keys', True),
+ 'user-script': ('user-script', False),
+ 'user-data': ('user-data', False),
+ 'iptables_disable': ('iptables_disable', True),
+ 'motd_sys_info': ('motd_sys_info', True),
+}
+
+
+class DataSourceSmartOS(sources.DataSource):
+ def __init__(self, sys_cfg, distro, paths):
+ sources.DataSource.__init__(self, sys_cfg, distro, paths)
+ self.seed_dir = os.path.join(paths.seed_dir, 'sdc')
+ self.is_smartdc = None
+ self.seed = self.sys_cfg.get("serial_device", DEF_TTY_LOC)
+ self.seed_timeout = self.sys_cfg.get("serial_timeout",
+ DEF_TTY_TIMEOUT)
+
+ def __str__(self):
+ root = sources.DataSource.__str__(self)
+ return "%s [seed=%s]" % (root, self.seed)
+
+ def get_data(self):
+ md = {}
+ ud = ""
+
+ if not os.path.exists(self.seed):
+ LOG.debug("Host does not appear to be on SmartOS")
+ return False
+ self.seed = self.seed
+
+ dmi_info = dmi_data()
+ if dmi_info is False:
+ LOG.debug("No dmidata utility found")
+ return False
+
+ system_uuid, system_type = dmi_info
+ if 'smartdc' not in system_type.lower():
+ LOG.debug("Host is not on SmartOS")
+ return False
+ self.is_smartdc = True
+ md['instance-id'] = system_uuid
+
+ for ci_noun, attribute in SMARTOS_ATTRIB_MAP.iteritems():
+ smartos_noun, strip = attribute
+ md[ci_noun] = query_data(smartos_noun, self.seed,
+ self.seed_timeout, strip=strip)
+
+ if not md['local-hostname']:
+ md['local-hostname'] = system_uuid
+
+ if md['user-data']:
+ ud = md['user-data']
+ else:
+ ud = md['user-script']
+
+ self.metadata = md
+ self.userdata_raw = ud
+ return True
+
+ def get_instance_id(self):
+ return self.metadata['instance-id']
+
+
+def get_serial(seed_device, seed_timeout):
+ """This is replaced in unit testing, allowing us to replace
+ serial.Serial with a mocked class
+
+ The timeout value of 60 seconds should never be hit. The value
+ is taken from SmartOS own provisioning tools. Since we are reading
+ each line individually up until the single ".", the transfer is
+ usually very fast (i.e. microseconds) to get the response.
+ """
+ if not seed_device:
+ raise AttributeError("seed_device value is not set")
+
+ ser = serial.Serial(seed_device, timeout=seed_timeout)
+ if not ser.isOpen():
+ raise SystemError("Unable to open %s" % seed_device)
+
+ return ser
+
+
+def query_data(noun, seed_device, seed_timeout, strip=False):
+ """Makes a request to via the serial console via "GET <NOUN>"
+
+ In the response, the first line is the status, while subsequent lines
+ are is the value. A blank line with a "." is used to indicate end of
+ response.
+ """
+
+ if not noun:
+ return False
+
+ ser = get_serial(seed_device, seed_timeout)
+ ser.write("GET %s\n" % noun.rstrip())
+ status = str(ser.readline()).rstrip()
+ response = []
+ eom_found = False
+
+ if 'SUCCESS' not in status:
+ ser.close()
+ return None
+
+ while not eom_found:
+ m = ser.readline()
+ if m.rstrip() == ".":
+ eom_found = True
+ else:
+ response.append(m)
+
+ ser.close()
+ if not strip:
+ return "".join(response)
+ else:
+ return "".join(response).rstrip()
+
+ return None
+
+
+def dmi_data():
+ sys_uuid, sys_type = None, None
+ dmidecode_path = util.which('dmidecode')
+ if not dmidecode_path:
+ return False
+
+ sys_uuid_cmd = [dmidecode_path, "-s", "system-uuid"]
+ try:
+ LOG.debug("Getting hostname from dmidecode")
+ (sys_uuid, _err) = util.subp(sys_uuid_cmd)
+ except Exception as e:
+ util.logexc(LOG, "Failed to get system UUID", e)
+
+ sys_type_cmd = [dmidecode_path, "-s", "system-product-name"]
+ try:
+ LOG.debug("Determining hypervisor product name via dmidecode")
+ (sys_type, _err) = util.subp(sys_type_cmd)
+ except Exception as e:
+ util.logexc(LOG, "Failed to get system UUID", e)
+
+ return sys_uuid.lower(), sys_type
+
+
+# Used to match classes to dependencies
+datasources = [
+ (DataSourceSmartOS, (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)
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index d8fbacdd..974c0407 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -135,7 +135,8 @@ class DataSource(object):
@property
def availability_zone(self):
- return self.metadata.get('availability-zone')
+ return self.metadata.get('availability-zone',
+ self.metadata.get('availability_zone'))
def get_instance_id(self):
if not self.metadata or 'instance-id' not in self.metadata: