summaryrefslogtreecommitdiff
path: root/cloudinit/sources
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/sources')
-rw-r--r--cloudinit/sources/DataSourceAzure.py143
-rw-r--r--cloudinit/sources/DataSourceSmartOS.py244
2 files changed, 355 insertions, 32 deletions
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 0a5caebe..66d7728b 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -17,6 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
+import crypt
import os
import os.path
import time
@@ -31,9 +32,21 @@ LOG = logging.getLogger(__name__)
DS_NAME = 'Azure'
DEFAULT_METADATA = {"instance-id": "iid-AZURE-NODE"}
AGENT_START = ['service', 'walinuxagent', 'start']
-BUILTIN_DS_CONFIG = {'datasource': {DS_NAME: {
- 'agent_command': AGENT_START,
- 'data_dir': "/var/lib/waagent"}}}
+BOUNCE_COMMAND = ['sh', '-xc',
+ "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):
@@ -42,19 +55,19 @@ class DataSourceAzureNet(sources.DataSource):
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):
- ddir_cfgpath = ['datasource', DS_NAME, 'data_dir']
# 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 = util.get_cfg_by_path(self.sys_cfg, ddir_cfgpath)
- if ddir is None:
- ddir = util.get_cfg_by_path(BUILTIN_DS_CONFIG, ddir_cfgpath)
+ ddir = self.ds_cfg['data_dir']
candidates = [self.seed_dir]
candidates.extend(list_possible_azure_ds_devs())
@@ -91,44 +104,46 @@ class DataSourceAzureNet(sources.DataSource):
return False
if found == ddir:
- LOG.debug("using cached datasource in %s", ddir)
-
- fields = [('cmd', ['datasource', DS_NAME, 'agent_command']),
- ('datadir', ddir_cfgpath)]
- mycfg = {}
- for cfg in (self.cfg, self.sys_cfg, BUILTIN_DS_CONFIG):
- for name, path in fields:
- if name in mycfg:
- continue
- value = util.get_cfg_by_path(cfg, keyp=path)
- if value is not None:
- mycfg[name] = value
+ 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['datadir'], files, dirmode=0700)
+ 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['cmd'])
+ invoke_agent(mycfg['agent_command'])
except util.ProcessExecutionError:
# claim the datasource even if the command failed
- util.logexc(LOG, "agent command '%s' failed.", mycfg['cmd'])
+ util.logexc(LOG, "agent command '%s' failed.",
+ mycfg['agent_command'])
- shcfgxml = os.path.join(mycfg['datadir'], "SharedConfig.xml")
+ 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['datadir'], bname)]
+ fp_files += [os.path.join(mycfg['data_dir'], bname)]
- start = time.time()
- missing = wait_for_files(wait_for + fp_files)
+ missing = util.log_time(logfunc=LOG.debug, msg="waiting for files",
+ func=wait_for_files,
+ args=(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")
@@ -148,6 +163,56 @@ class DataSourceAzureNet(sources.DataSource):
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])
+
+ msg = ("phostname=%s hostname=%s policy=%s interface=%s" %
+ (prev_hostname, hostname, policy, interface))
+
+ if util.is_false(policy):
+ LOG.debug("pubhname: policy false, skipping [%s]", msg)
+ return
+
+ if prev_hostname == hostname and policy != "force":
+ LOG.debug("pubhname: no change, policy != force. skipping. [%s]", msg)
+ return
+
+ env = os.environ.copy()
+ env['interface'] = interface
+ env['hostname'] = hostname
+ env['old_hostname'] = prev_hostname
+
+ if command == "builtin":
+ command = BOUNCE_COMMAND
+
+ LOG.debug("pubhname: publishing hostname [%s]", msg)
+ shell = not isinstance(command, (list, tuple))
+ # capture=False, see comments in bug 1202758 and bug 1206164.
+ util.log_time(logfunc=LOG.debug, msg="publishing hostname",
+ get_uptime=True, func=util.subp,
+ kwargs={'args': command, 'shell': shell, 'capture': False,
+ 'env': env})
+
+
def crtfile_to_pubkey(fname):
pipeline = ('openssl x509 -noout -pubkey < "$0" |'
'ssh-keygen -i -m PKCS8 -f /dev/stdin')
@@ -319,15 +384,21 @@ def read_azure_ovf(contents):
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"):
- ud = base64.b64decode(''.join(value.split()))
+ if attrs.get('encoding') in (None, "base64"):
+ ud = base64.b64decode(''.join(value.split()))
+ else:
+ ud = value
elif name == "username":
username = value
elif name == "userpassword":
@@ -335,7 +406,11 @@ def read_azure_ovf(contents):
elif name == "hostname":
md['local-hostname'] = value
elif name == "dscfg":
- cfg['datasource'] = {DS_NAME: util.load_yaml(value, default={})}
+ 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":
@@ -350,7 +425,7 @@ def read_azure_ovf(contents):
if username:
defuser['name'] = username
if password:
- defuser['password'] = password
+ defuser['passwd'] = encrypt_pass(password)
defuser['lock_passwd'] = False
if defuser:
@@ -362,6 +437,10 @@ def read_azure_ovf(contents):
return (md, ud, cfg)
+def encrypt_pass(password, salt_id="$6$"):
+ return crypt.crypt(password, salt_id + util.rand_str(strlen=16))
+
+
def list_possible_azure_ds_devs():
# return a sorted list of devices that might have a azure datasource
devlist = []
diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
new file mode 100644
index 00000000..d348d20b
--- /dev/null
+++ b/cloudinit/sources/DataSourceSmartOS.py
@@ -0,0 +1,244 @@
+# 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.
+#
+
+
+import base64
+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),
+}
+
+# These are values which will never be base64 encoded.
+# They come from the cloud platform, not user
+SMARTOS_NO_BASE64 = ['root_authorized_keys', 'motd_sys_info',
+ 'iptables_disable']
+
+
+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.ds_cfg.get("serial_device", DEF_TTY_LOC)
+ self.seed_timeout = self.ds_cfg.get("serial_timeout", DEF_TTY_TIMEOUT)
+ self.smartos_no_base64 = self.ds_cfg.get('no_base64_decode',
+ SMARTOS_NO_BASE64)
+ self.b64_keys = self.ds_cfg.get('base64_keys', [])
+ self.b64_all = self.ds_cfg.get('base64_all', False)
+
+ 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. system_type=%s", system_type)
+ return False
+ self.is_smartdc = True
+ md['instance-id'] = system_uuid
+
+ b64_keys = self.query('base64_keys', strip=True, b64=False)
+ if b64_keys is not None:
+ self.b64_keys = [k.strip() for k in str(b64_keys).split(',')]
+
+ b64_all = self.query('base64_all', strip=True, b64=False)
+ if b64_all is not None:
+ self.b64_all = util.is_true(b64_all)
+
+ for ci_noun, attribute in SMARTOS_ATTRIB_MAP.iteritems():
+ smartos_noun, strip = attribute
+ md[ci_noun] = self.query(smartos_noun, strip=strip)
+
+ if not md['local-hostname']:
+ md['local-hostname'] = system_uuid
+
+ ud = None
+ if md['user-data']:
+ ud = md['user-data']
+ elif md['user-script']:
+ ud = md['user-script']
+
+ self.metadata = md
+ self.userdata_raw = ud
+ return True
+
+ def get_instance_id(self):
+ return self.metadata['instance-id']
+
+ def query(self, noun, strip=False, default=None, b64=None):
+ if b64 is None:
+ if noun in self.smartos_no_base64:
+ b64 = False
+ elif self.b64_all or noun in self.b64_keys:
+ b64 = True
+
+ return query_data(noun=noun, strip=strip, seed_device=self.seed,
+ seed_timeout=self.seed_timeout, default=default,
+ b64=b64)
+
+
+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, default=None,
+ b64=None):
+ """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 the response is expected to be base64 encoded, then set b64encoded
+ to true. Unfortantely, there is no way to know if something is 100%
+ encoded, so this method relies on being told if the data is base64 or
+ not.
+ """
+
+ 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 default
+
+ while not eom_found:
+ m = ser.readline()
+ if m.rstrip() == ".":
+ eom_found = True
+ else:
+ response.append(m)
+
+ ser.close()
+
+ if b64 is None:
+ b64 = query_data('b64-%s' % noun, seed_device=seed_device,
+ seed_timeout=seed_timeout, b64=False,
+ default=False, strip=True)
+ b64 = util.is_true(b64)
+
+ resp = None
+ if b64 or strip:
+ resp = "".join(response).rstrip()
+ else:
+ resp = "".join(response)
+
+ if b64:
+ try:
+ return base64.b64decode(resp)
+ except TypeError:
+ LOG.warn("Failed base64 decoding key '%s'", noun)
+ return resp
+
+ return resp
+
+
+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)