summaryrefslogtreecommitdiff
path: root/cloudinit
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit')
-rw-r--r--cloudinit/config/cc_apt_pipelining.py2
-rw-r--r--cloudinit/config/cc_disk_setup.py3
-rw-r--r--cloudinit/config/cc_snappy.py6
-rw-r--r--cloudinit/distros/__init__.py23
-rw-r--r--cloudinit/distros/rhel.py11
-rw-r--r--cloudinit/handlers/__init__.py11
-rw-r--r--cloudinit/sources/DataSourceAzure.py235
-rw-r--r--cloudinit/sources/DataSourceGCE.py92
-rw-r--r--cloudinit/sources/DataSourceOpenNebula.py1
-rw-r--r--cloudinit/sources/helpers/azure.py293
-rw-r--r--cloudinit/user_data.py9
-rw-r--r--cloudinit/util.py16
12 files changed, 524 insertions, 178 deletions
diff --git a/cloudinit/config/cc_apt_pipelining.py b/cloudinit/config/cc_apt_pipelining.py
index e5629175..40c32c84 100644
--- a/cloudinit/config/cc_apt_pipelining.py
+++ b/cloudinit/config/cc_apt_pipelining.py
@@ -43,7 +43,7 @@ def handle(_name, cfg, _cloud, log, _args):
write_apt_snippet("0", log, DEFAULT_FILE)
elif apt_pipe_value_s in ("none", "unchanged", "os"):
return
- elif apt_pipe_value_s in [str(b) for b in xrange(0, 6)]:
+ elif apt_pipe_value_s in [str(b) for b in range(0, 6)]:
write_apt_snippet(apt_pipe_value_s, log, DEFAULT_FILE)
else:
log.warn("Invalid option for apt_pipeling: %s", apt_pipe_value)
diff --git a/cloudinit/config/cc_disk_setup.py b/cloudinit/config/cc_disk_setup.py
index f899210b..e2ce6db4 100644
--- a/cloudinit/config/cc_disk_setup.py
+++ b/cloudinit/config/cc_disk_setup.py
@@ -304,8 +304,7 @@ def is_disk_used(device):
# If the child count is higher 1, then there are child nodes
# such as partition or device mapper nodes
- use_count = [x for x in enumerate_disk(device)]
- if len(use_count.splitlines()) > 1:
+ if len(list(enumerate_disk(device))) > 1:
return True
# If we see a file system, then its used
diff --git a/cloudinit/config/cc_snappy.py b/cloudinit/config/cc_snappy.py
index 6a7ae09b..7aaec94a 100644
--- a/cloudinit/config/cc_snappy.py
+++ b/cloudinit/config/cc_snappy.py
@@ -42,12 +42,10 @@ Example config:
"""
from cloudinit import log as logging
-from cloudinit import templater
from cloudinit import util
from cloudinit.settings import PER_INSTANCE
import glob
-import six
import tempfile
import os
@@ -72,7 +70,7 @@ def parse_filename(fname):
name = fname_noext.partition("_")[0]
shortname = name.partition(".")[0]
return(name, shortname, fname_noext)
-
+
def get_fs_package_ops(fspath):
if not fspath:
@@ -98,7 +96,7 @@ def makeop(op, name, config=None, path=None, cfgfile=None):
def get_package_config(configs, name):
# load the package's config from the configs dict.
- # prefer full-name entry (config-example.canonical)
+ # prefer full-name entry (config-example.canonical)
# over short name entry (config-example)
if name in configs:
return configs[name]
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index ab874b45..e0cce670 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -208,6 +208,15 @@ class Distro(object):
and sys_hostname != hostname):
update_files.append(sys_fn)
+ # If something else has changed the hostname after we set it
+ # initially, we should not overwrite those changes (we should
+ # only be setting the hostname once per instance)
+ if (sys_hostname and prev_hostname and
+ sys_hostname != prev_hostname):
+ LOG.info("%s differs from %s, assuming user maintained hostname.",
+ prev_hostname_fn, sys_fn)
+ return
+
# Remove duplicates (incase the previous config filename)
# is the same as the system config filename, don't bother
# doing it twice
@@ -222,11 +231,6 @@ class Distro(object):
util.logexc(LOG, "Failed to write hostname %s to %s", hostname,
fn)
- if (sys_hostname and prev_hostname and
- sys_hostname != prev_hostname):
- LOG.debug("%s differs from %s, assuming user maintained hostname.",
- prev_hostname_fn, sys_fn)
-
# If the system hostname file name was provided set the
# non-fqdn as the transient hostname.
if sys_fn in update_files:
@@ -318,6 +322,7 @@ class Distro(object):
"gecos": '--comment',
"homedir": '--home',
"primary_group": '--gid',
+ "uid": '--uid',
"groups": '--groups',
"passwd": '--password',
"shell": '--shell',
@@ -551,8 +556,12 @@ def _get_package_mirror_info(mirror_info, availability_zone=None,
if not mirror_info:
mirror_info = {}
- ec2_az_re = ("^[a-z][a-z]-(%s)-[1-9][0-9]*[a-z]$" %
- "north|northeast|east|southeast|south|southwest|west|northwest")
+ # ec2 availability zones are named cc-direction-[0-9][a-d] (us-east-1b)
+ # the region is us-east-1. so region = az[0:-1]
+ directions_re = '|'.join([
+ 'central', 'east', 'north', 'northeast', 'northwest',
+ 'south', 'southeast', 'southwest', 'west'])
+ ec2_az_re = ("^[a-z][a-z]-(%s)-[1-9][0-9]*[a-z]$" % directions_re)
subst = {}
if availability_zone:
diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py
index 7408989c..30c805a6 100644
--- a/cloudinit/distros/rhel.py
+++ b/cloudinit/distros/rhel.py
@@ -116,6 +116,7 @@ class Distro(distros.Distro):
(dist, vers) = util.system_info()['dist'][:2]
major = (int)(vers.split('.')[0])
return ((dist.startswith('Red Hat Enterprise Linux') and major >= 7)
+ or (dist.startswith('CentOS Linux') and major >= 7)
or (dist.startswith('Fedora') and major >= 18))
def apply_locale(self, locale, out_fn=None):
@@ -132,7 +133,11 @@ class Distro(distros.Distro):
rhel_util.update_sysconfig_file(out_fn, locale_cfg)
def _write_hostname(self, hostname, out_fn):
- if self.uses_systemd():
+ # systemd will never update previous-hostname for us, so
+ # we need to do it ourselves
+ if self.uses_systemd() and out_fn.endswith('/previous-hostname'):
+ util.write_file(out_fn, hostname)
+ elif self.uses_systemd():
util.subp(['hostnamectl', 'set-hostname', str(hostname)])
else:
host_cfg = {
@@ -155,7 +160,9 @@ class Distro(distros.Distro):
return (host_fn, self._read_hostname(host_fn))
def _read_hostname(self, filename, default=None):
- if self.uses_systemd():
+ if self.uses_systemd() and filename.endswith('/previous-hostname'):
+ return util.load_file(filename).strip()
+ elif self.uses_systemd():
(out, _err) = util.subp(['hostname'])
if len(out):
return out
diff --git a/cloudinit/handlers/__init__.py b/cloudinit/handlers/__init__.py
index d62fcd19..53d5604a 100644
--- a/cloudinit/handlers/__init__.py
+++ b/cloudinit/handlers/__init__.py
@@ -170,12 +170,12 @@ def _extract_first_or_bytes(blob, size):
start = blob.split("\n", 1)[0]
else:
# We want to avoid decoding the whole blob (it might be huge)
- # By taking 4*size bytes we have a guarantee to decode size utf8 chars
- start = blob[:4*size].decode(errors='ignore').split("\n", 1)[0]
+ # By taking 4*size bytes we guarantee to decode size utf8 chars
+ start = blob[:4 * size].decode(errors='ignore').split("\n", 1)[0]
if len(start) >= size:
start = start[:size]
except UnicodeDecodeError:
- # Bytes array doesn't contain a text object -- return chunk of raw bytes
+ # Bytes array doesn't contain text so return chunk of raw bytes
start = blob[0:size]
return start
@@ -263,7 +263,10 @@ def fixup_handler(mod, def_freq=PER_INSTANCE):
def type_from_starts_with(payload, default=None):
- payload_lc = payload.lower()
+ try:
+ payload_lc = util.decode_binary(payload).lower()
+ except UnicodeDecodeError:
+ return default
payload_lc = payload_lc.lstrip()
for text in INCLUSION_SRCH:
if payload_lc.startswith(text):
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 6e030217..d0a882ca 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -17,17 +17,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
+import contextlib
import crypt
import fnmatch
import os
import os.path
import time
+import xml.etree.ElementTree as ET
+
from xml.dom import minidom
from cloudinit import log as logging
from cloudinit.settings import PER_ALWAYS
from cloudinit import sources
from cloudinit import util
+from cloudinit.sources.helpers.azure import (
+ get_metadata_from_fabric, iid_from_shared_config_content)
LOG = logging.getLogger(__name__)
@@ -65,6 +70,40 @@ BUILTIN_CLOUD_CONFIG = {
DS_CFG_PATH = ['datasource', DS_NAME]
DEF_EPHEMERAL_LABEL = 'Temporary Storage'
+# The redacted password fails to meet password complexity requirements
+# so we can safely use this to mask/redact the password in the ovf-env.xml
+DEF_PASSWD_REDACTION = 'REDACTED'
+
+
+def get_hostname(hostname_command='hostname'):
+ return util.subp(hostname_command, capture=True)[0].strip()
+
+
+def set_hostname(hostname, hostname_command='hostname'):
+ util.subp([hostname_command, hostname])
+
+
+@contextlib.contextmanager
+def temporary_hostname(temp_hostname, cfg, hostname_command='hostname'):
+ """
+ Set a temporary hostname, restoring the previous hostname on exit.
+
+ Will have the value of the previous hostname when used as a context
+ manager, or None if the hostname was not changed.
+ """
+ policy = cfg['hostname_bounce']['policy']
+ previous_hostname = get_hostname(hostname_command)
+ if (not util.is_true(cfg.get('set_hostname'))
+ or util.is_false(policy)
+ or (previous_hostname == temp_hostname and policy != 'force')):
+ yield None
+ return
+ set_hostname(temp_hostname, hostname_command)
+ try:
+ yield previous_hostname
+ finally:
+ set_hostname(previous_hostname, hostname_command)
+
class DataSourceAzureNet(sources.DataSource):
def __init__(self, sys_cfg, distro, paths):
@@ -80,6 +119,56 @@ class DataSourceAzureNet(sources.DataSource):
root = sources.DataSource.__str__(self)
return "%s [seed=%s]" % (root, self.seed)
+ def get_metadata_from_agent(self):
+ temp_hostname = self.metadata.get('local-hostname')
+ hostname_command = self.ds_cfg['hostname_bounce']['hostname_command']
+ with temporary_hostname(temp_hostname, self.ds_cfg,
+ hostname_command=hostname_command) \
+ as previous_hostname:
+ if (previous_hostname is not None
+ and util.is_true(self.ds_cfg.get('set_hostname'))):
+ cfg = self.ds_cfg['hostname_bounce']
+ try:
+ perform_hostname_bounce(hostname=temp_hostname,
+ cfg=cfg,
+ prev_hostname=previous_hostname)
+ except Exception as e:
+ LOG.warn("Failed publishing hostname: %s", e)
+ util.logexc(LOG, "handling set_hostname failed")
+
+ try:
+ invoke_agent(self.ds_cfg['agent_command'])
+ except util.ProcessExecutionError:
+ # claim the datasource even if the command failed
+ util.logexc(LOG, "agent command '%s' failed.",
+ self.ds_cfg['agent_command'])
+
+ ddir = self.ds_cfg['data_dir']
+ shcfgxml = os.path.join(ddir, "SharedConfig.xml")
+ wait_for = [shcfgxml]
+
+ fp_files = []
+ for pk in self.cfg.get('_pubkeys', []):
+ bname = str(pk['fingerprint'] + ".crt")
+ fp_files += [os.path.join(ddir, bname)]
+
+ 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)
+
+ metadata = {}
+ if shcfgxml in missing:
+ LOG.warn("SharedConfig.xml missing, using static instance-id")
+ else:
+ try:
+ metadata['instance-id'] = iid_from_shared_config(shcfgxml)
+ except ValueError as e:
+ LOG.warn("failed to get instance id in %s: %s", shcfgxml, e)
+ metadata['public-keys'] = pubkeys_from_crt_files(fp_files)
+ return metadata
+
def get_data(self):
# azure removes/ejects the cdrom containing the ovf-env.xml
# file on reboot. So, in order to successfully reboot we
@@ -132,8 +221,6 @@ class DataSourceAzureNet(sources.DataSource):
# now update ds_cfg to reflect contents pass in config
user_ds_cfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {})
self.ds_cfg = util.mergemanydict([user_ds_cfg, self.ds_cfg])
- mycfg = self.ds_cfg
- ddir = mycfg['data_dir']
if found != ddir:
cached_ovfenv = util.load_file(
@@ -154,46 +241,18 @@ class DataSourceAzureNet(sources.DataSource):
# the directory to be protected.
write_files(ddir, files, dirmode=0o700)
- # 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(ddir, "SharedConfig.xml")
- wait_for = [shcfgxml]
-
- fp_files = []
- for pk in self.cfg.get('_pubkeys', []):
- bname = str(pk['fingerprint'] + ".crt")
- fp_files += [os.path.join(ddir, bname)]
-
- 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)
-
- if shcfgxml in missing:
- LOG.warn("SharedConfig.xml missing, using static instance-id")
+ if self.ds_cfg['agent_command'] == '__builtin__':
+ metadata_func = get_metadata_from_fabric
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)
+ metadata_func = self.get_metadata_from_agent
+ try:
+ fabric_data = metadata_func()
+ except Exception as exc:
+ LOG.info("Error communicating with Azure fabric; assume we aren't"
+ " on Azure.", exc_info=True)
+ return False
- pubkeys = pubkeys_from_crt_files(fp_files)
- self.metadata['public-keys'] = pubkeys
+ self.metadata.update(fabric_data)
found_ephemeral = find_ephemeral_disk()
if found_ephemeral:
@@ -299,39 +358,15 @@ def support_new_ephemeral(cfg):
return mod_list
-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"):
+def perform_hostname_bounce(hostname, cfg, prev_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
+ command = cfg['command']
+ interface = cfg['interface']
+ policy = cfg['policy']
+ msg = ("hostname=%s policy=%s interface=%s" %
+ (hostname, policy, interface))
env = os.environ.copy()
env['interface'] = interface
env['hostname'] = hostname
@@ -344,15 +379,16 @@ def apply_hostname_bounce(hostname, policy, interface, command,
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})
+ get_uptime=True, func=util.subp,
+ kwargs={'args': command, 'shell': shell, 'capture': False,
+ 'env': env})
-def crtfile_to_pubkey(fname):
+def crtfile_to_pubkey(fname, data=None):
pipeline = ('openssl x509 -noout -pubkey < "$0" |'
'ssh-keygen -i -m PKCS8 -f /dev/stdin')
- (out, _err) = util.subp(['sh', '-c', pipeline, fname], capture=True)
+ (out, _err) = util.subp(['sh', '-c', pipeline, fname],
+ capture=True, data=data)
return out.rstrip()
@@ -384,14 +420,30 @@ def wait_for_files(flist, maxwait=60, naplen=.5):
def write_files(datadir, files, dirmode=None):
+
+ def _redact_password(cnt, fname):
+ """Azure provides the UserPassword in plain text. So we redact it"""
+ try:
+ root = ET.fromstring(cnt)
+ for elem in root.iter():
+ if ('UserPassword' in elem.tag and
+ elem.text != DEF_PASSWD_REDACTION):
+ elem.text = DEF_PASSWD_REDACTION
+ return ET.tostring(root)
+ except Exception as e:
+ LOG.critical("failed to redact userpassword in {}".format(fname))
+ return cnt
+
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=0o600)
+ fname = os.path.join(datadir, name)
+ if 'ovf-env.xml' in name:
+ content = _redact_password(content, fname)
+ util.write_file(filename=fname, content=content, mode=0o600)
def invoke_agent(cmd):
@@ -462,20 +514,6 @@ def load_azure_ovf_pubkeys(sshnode):
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)
@@ -560,7 +598,7 @@ def read_azure_ovf(contents):
defuser = {}
if username:
defuser['name'] = username
- if password:
+ if password and DEF_PASSWD_REDACTION != password:
defuser['passwd'] = encrypt_pass(password)
defuser['lock_passwd'] = False
@@ -606,19 +644,6 @@ def iid_from_shared_config(path):
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
diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py
index 608c07f1..f4ed915d 100644
--- a/cloudinit/sources/DataSourceGCE.py
+++ b/cloudinit/sources/DataSourceGCE.py
@@ -30,6 +30,31 @@ BUILTIN_DS_CONFIG = {
REQUIRED_FIELDS = ('instance-id', 'availability-zone', 'local-hostname')
+class GoogleMetadataFetcher(object):
+ headers = {'X-Google-Metadata-Request': True}
+
+ def __init__(self, metadata_address):
+ self.metadata_address = metadata_address
+
+ def get_value(self, path, is_text):
+ value = None
+ try:
+ resp = url_helper.readurl(url=self.metadata_address + path,
+ headers=self.headers)
+ except url_helper.UrlError as exc:
+ msg = "url %s raised exception %s"
+ LOG.debug(msg, path, exc)
+ else:
+ if resp.code == 200:
+ if is_text:
+ value = util.decode_binary(resp.contents)
+ else:
+ value = resp.contents
+ else:
+ LOG.debug("url %s returned code %s", path, resp.code)
+ return value
+
+
class DataSourceGCE(sources.DataSource):
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
@@ -50,17 +75,15 @@ class DataSourceGCE(sources.DataSource):
return public_key
def get_data(self):
- # GCE metadata server requires a custom header since v1
- headers = {'X-Google-Metadata-Request': True}
-
# url_map: (our-key, path, required, is_text)
url_map = [
- ('instance-id', 'instance/id', True, True),
- ('availability-zone', 'instance/zone', True, True),
- ('local-hostname', 'instance/hostname', True, True),
- ('public-keys', 'project/attributes/sshKeys', False, True),
- ('user-data', 'instance/attributes/user-data', False, False),
- ('user-data-encoding', 'instance/attributes/user-data-encoding',
+ ('instance-id', ('instance/id',), True, True),
+ ('availability-zone', ('instance/zone',), True, True),
+ ('local-hostname', ('instance/hostname',), True, True),
+ ('public-keys', ('project/attributes/sshKeys',
+ 'instance/attributes/sshKeys'), False, True),
+ ('user-data', ('instance/attributes/user-data',), False, False),
+ ('user-data-encoding', ('instance/attributes/user-data-encoding',),
False, True),
]
@@ -69,40 +92,25 @@ class DataSourceGCE(sources.DataSource):
LOG.debug("%s is not resolvable", self.metadata_address)
return False
+ metadata_fetcher = GoogleMetadataFetcher(self.metadata_address)
# iterate over url_map keys to get metadata items
- found = False
- for (mkey, path, required, is_text) in url_map:
- try:
- resp = url_helper.readurl(url=self.metadata_address + path,
- headers=headers)
- if resp.code == 200:
- found = True
- if is_text:
- self.metadata[mkey] = util.decode_binary(resp.contents)
- else:
- self.metadata[mkey] = resp.contents
+ running_on_gce = False
+ for (mkey, paths, required, is_text) in url_map:
+ value = None
+ for path in paths:
+ new_value = metadata_fetcher.get_value(path, is_text)
+ if new_value is not None:
+ value = new_value
+ if value:
+ running_on_gce = True
+ if required and value is None:
+ msg = "required key %s returned nothing. not GCE"
+ if not running_on_gce:
+ LOG.debug(msg, mkey)
else:
- if required:
- msg = "required url %s returned code %s. not GCE"
- if not found:
- LOG.debug(msg, path, resp.code)
- else:
- LOG.warn(msg, path, resp.code)
- return False
- else:
- self.metadata[mkey] = None
- except url_helper.UrlError as e:
- if required:
- msg = "required url %s raised exception %s. not GCE"
- if not found:
- LOG.debug(msg, path, e)
- else:
- LOG.warn(msg, path, e)
- return False
- msg = "Failed to get %s metadata item: %s."
- LOG.debug(msg, path, e)
-
- self.metadata[mkey] = None
+ LOG.warn(msg, mkey)
+ return False
+ self.metadata[mkey] = value
if self.metadata['public-keys']:
lines = self.metadata['public-keys'].splitlines()
@@ -116,7 +124,7 @@ class DataSourceGCE(sources.DataSource):
else:
LOG.warn('unknown user-data-encoding: %s, ignoring', encoding)
- return found
+ return running_on_gce
@property
def launch_index(self):
diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py
index 61709c1b..ac2c3b45 100644
--- a/cloudinit/sources/DataSourceOpenNebula.py
+++ b/cloudinit/sources/DataSourceOpenNebula.py
@@ -24,7 +24,6 @@
# 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 pwd
import re
diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py
new file mode 100644
index 00000000..281d733e
--- /dev/null
+++ b/cloudinit/sources/helpers/azure.py
@@ -0,0 +1,293 @@
+import logging
+import os
+import re
+import socket
+import struct
+import tempfile
+import time
+from contextlib import contextmanager
+from xml.etree import ElementTree
+
+from cloudinit import util
+
+
+LOG = logging.getLogger(__name__)
+
+
+@contextmanager
+def cd(newdir):
+ prevdir = os.getcwd()
+ os.chdir(os.path.expanduser(newdir))
+ try:
+ yield
+ finally:
+ os.chdir(prevdir)
+
+
+class AzureEndpointHttpClient(object):
+
+ headers = {
+ 'x-ms-agent-name': 'WALinuxAgent',
+ 'x-ms-version': '2012-11-30',
+ }
+
+ def __init__(self, certificate):
+ self.extra_secure_headers = {
+ "x-ms-cipher-name": "DES_EDE3_CBC",
+ "x-ms-guest-agent-public-x509-cert": certificate,
+ }
+
+ def get(self, url, secure=False):
+ headers = self.headers
+ if secure:
+ headers = self.headers.copy()
+ headers.update(self.extra_secure_headers)
+ return util.read_file_or_url(url, headers=headers)
+
+ def post(self, url, data=None, extra_headers=None):
+ headers = self.headers
+ if extra_headers is not None:
+ headers = self.headers.copy()
+ headers.update(extra_headers)
+ return util.read_file_or_url(url, data=data, headers=headers)
+
+
+class GoalState(object):
+
+ def __init__(self, xml, http_client):
+ self.http_client = http_client
+ self.root = ElementTree.fromstring(xml)
+ self._certificates_xml = None
+
+ def _text_from_xpath(self, xpath):
+ element = self.root.find(xpath)
+ if element is not None:
+ return element.text
+ return None
+
+ @property
+ def container_id(self):
+ return self._text_from_xpath('./Container/ContainerId')
+
+ @property
+ def incarnation(self):
+ return self._text_from_xpath('./Incarnation')
+
+ @property
+ def instance_id(self):
+ return self._text_from_xpath(
+ './Container/RoleInstanceList/RoleInstance/InstanceId')
+
+ @property
+ def shared_config_xml(self):
+ url = self._text_from_xpath('./Container/RoleInstanceList/RoleInstance'
+ '/Configuration/SharedConfig')
+ return self.http_client.get(url).contents
+
+ @property
+ def certificates_xml(self):
+ if self._certificates_xml is None:
+ url = self._text_from_xpath(
+ './Container/RoleInstanceList/RoleInstance'
+ '/Configuration/Certificates')
+ if url is not None:
+ self._certificates_xml = self.http_client.get(
+ url, secure=True).contents
+ return self._certificates_xml
+
+
+class OpenSSLManager(object):
+
+ certificate_names = {
+ 'private_key': 'TransportPrivate.pem',
+ 'certificate': 'TransportCert.pem',
+ }
+
+ def __init__(self):
+ self.tmpdir = tempfile.mkdtemp()
+ self.certificate = None
+ self.generate_certificate()
+
+ def clean_up(self):
+ util.del_dir(self.tmpdir)
+
+ def generate_certificate(self):
+ LOG.debug('Generating certificate for communication with fabric...')
+ if self.certificate is not None:
+ LOG.debug('Certificate already generated.')
+ return
+ with cd(self.tmpdir):
+ util.subp([
+ 'openssl', 'req', '-x509', '-nodes', '-subj',
+ '/CN=LinuxTransport', '-days', '32768', '-newkey', 'rsa:2048',
+ '-keyout', self.certificate_names['private_key'],
+ '-out', self.certificate_names['certificate'],
+ ])
+ certificate = ''
+ for line in open(self.certificate_names['certificate']):
+ if "CERTIFICATE" not in line:
+ certificate += line.rstrip()
+ self.certificate = certificate
+ LOG.debug('New certificate generated.')
+
+ def parse_certificates(self, certificates_xml):
+ tag = ElementTree.fromstring(certificates_xml).find(
+ './/Data')
+ certificates_content = tag.text
+ lines = [
+ b'MIME-Version: 1.0',
+ b'Content-Disposition: attachment; filename="Certificates.p7m"',
+ b'Content-Type: application/x-pkcs7-mime; name="Certificates.p7m"',
+ b'Content-Transfer-Encoding: base64',
+ b'',
+ certificates_content.encode('utf-8'),
+ ]
+ with cd(self.tmpdir):
+ with open('Certificates.p7m', 'wb') as f:
+ f.write(b'\n'.join(lines))
+ out, _ = util.subp(
+ 'openssl cms -decrypt -in Certificates.p7m -inkey'
+ ' {private_key} -recip {certificate} | openssl pkcs12 -nodes'
+ ' -password pass:'.format(**self.certificate_names),
+ shell=True)
+ private_keys, certificates = [], []
+ current = []
+ for line in out.splitlines():
+ current.append(line)
+ if re.match(r'[-]+END .*?KEY[-]+$', line):
+ private_keys.append('\n'.join(current))
+ current = []
+ elif re.match(r'[-]+END .*?CERTIFICATE[-]+$', line):
+ certificates.append('\n'.join(current))
+ current = []
+ keys = []
+ for certificate in certificates:
+ with cd(self.tmpdir):
+ public_key, _ = util.subp(
+ 'openssl x509 -noout -pubkey |'
+ 'ssh-keygen -i -m PKCS8 -f /dev/stdin',
+ data=certificate,
+ shell=True)
+ keys.append(public_key)
+ return keys
+
+
+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}"/>
+ """
+ root = ElementTree.fromstring(content)
+ depnode = root.find('Deployment')
+ return depnode.get('name')
+
+
+class WALinuxAgentShim(object):
+
+ REPORT_READY_XML_TEMPLATE = '\n'.join([
+ '<?xml version="1.0" encoding="utf-8"?>',
+ '<Health xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
+ ' xmlns:xsd="http://www.w3.org/2001/XMLSchema">',
+ ' <GoalStateIncarnation>{incarnation}</GoalStateIncarnation>',
+ ' <Container>',
+ ' <ContainerId>{container_id}</ContainerId>',
+ ' <RoleInstanceList>',
+ ' <Role>',
+ ' <InstanceId>{instance_id}</InstanceId>',
+ ' <Health>',
+ ' <State>Ready</State>',
+ ' </Health>',
+ ' </Role>',
+ ' </RoleInstanceList>',
+ ' </Container>',
+ '</Health>'])
+
+ def __init__(self):
+ LOG.debug('WALinuxAgentShim instantiated...')
+ self.endpoint = self.find_endpoint()
+ self.openssl_manager = None
+ self.values = {}
+
+ def clean_up(self):
+ if self.openssl_manager is not None:
+ self.openssl_manager.clean_up()
+
+ @staticmethod
+ def find_endpoint():
+ LOG.debug('Finding Azure endpoint...')
+ content = util.load_file('/var/lib/dhcp/dhclient.eth0.leases')
+ value = None
+ for line in content.splitlines():
+ if 'unknown-245' in line:
+ value = line.strip(' ').split(' ', 2)[-1].strip(';\n"')
+ if value is None:
+ raise Exception('No endpoint found in DHCP config.')
+ if ':' in value:
+ hex_string = ''
+ for hex_pair in value.split(':'):
+ if len(hex_pair) == 1:
+ hex_pair = '0' + hex_pair
+ hex_string += hex_pair
+ value = struct.pack('>L', int(hex_string.replace(':', ''), 16))
+ else:
+ value = value.encode('utf-8')
+ endpoint_ip_address = socket.inet_ntoa(value)
+ LOG.debug('Azure endpoint found at %s', endpoint_ip_address)
+ return endpoint_ip_address
+
+ def register_with_azure_and_fetch_data(self):
+ self.openssl_manager = OpenSSLManager()
+ http_client = AzureEndpointHttpClient(self.openssl_manager.certificate)
+ LOG.info('Registering with Azure...')
+ attempts = 0
+ while True:
+ try:
+ response = http_client.get(
+ 'http://{0}/machine/?comp=goalstate'.format(self.endpoint))
+ except Exception:
+ if attempts < 10:
+ time.sleep(attempts + 1)
+ else:
+ raise
+ else:
+ break
+ attempts += 1
+ LOG.debug('Successfully fetched GoalState XML.')
+ goal_state = GoalState(response.contents, http_client)
+ public_keys = []
+ if goal_state.certificates_xml is not None:
+ LOG.debug('Certificate XML found; parsing out public keys.')
+ public_keys = self.openssl_manager.parse_certificates(
+ goal_state.certificates_xml)
+ data = {
+ 'instance-id': iid_from_shared_config_content(
+ goal_state.shared_config_xml),
+ 'public-keys': public_keys,
+ }
+ self._report_ready(goal_state, http_client)
+ return data
+
+ def _report_ready(self, goal_state, http_client):
+ LOG.debug('Reporting ready to Azure fabric.')
+ document = self.REPORT_READY_XML_TEMPLATE.format(
+ incarnation=goal_state.incarnation,
+ container_id=goal_state.container_id,
+ instance_id=goal_state.instance_id,
+ )
+ http_client.post(
+ "http://{0}/machine?comp=health".format(self.endpoint),
+ data=document,
+ extra_headers={'Content-Type': 'text/xml; charset=utf-8'},
+ )
+ LOG.info('Reported ready to Azure fabric.')
+
+
+def get_metadata_from_fabric():
+ shim = WALinuxAgentShim()
+ try:
+ return shim.register_with_azure_and_fetch_data()
+ finally:
+ shim.clean_up()
diff --git a/cloudinit/user_data.py b/cloudinit/user_data.py
index eb3c7336..f7c5787c 100644
--- a/cloudinit/user_data.py
+++ b/cloudinit/user_data.py
@@ -49,6 +49,7 @@ INCLUDE_TYPES = ['text/x-include-url', 'text/x-include-once-url']
ARCHIVE_TYPES = ["text/cloud-config-archive"]
UNDEF_TYPE = "text/plain"
ARCHIVE_UNDEF_TYPE = "text/cloud-config"
+ARCHIVE_UNDEF_BINARY_TYPE = "application/octet-stream"
# This seems to hit most of the gzip possible content types.
DECOMP_TYPES = [
@@ -265,11 +266,15 @@ class UserDataProcessor(object):
content = ent.get('content', '')
mtype = ent.get('type')
if not mtype:
- mtype = handlers.type_from_starts_with(content,
- ARCHIVE_UNDEF_TYPE)
+ default = ARCHIVE_UNDEF_TYPE
+ if isinstance(content, six.binary_type):
+ default = ARCHIVE_UNDEF_BINARY_TYPE
+ mtype = handlers.type_from_starts_with(content, default)
maintype, subtype = mtype.split('/', 1)
if maintype == "text":
+ if isinstance(content, six.binary_type):
+ content = content.decode()
msg = MIMEText(content, _subtype=subtype)
else:
msg = MIMEBase(maintype, subtype)
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 971c1c2d..db4e02b8 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -121,8 +121,12 @@ def fully_decoded_payload(part):
if (six.PY3 and
part.get_content_maintype() == 'text' and
isinstance(cte_payload, bytes)):
- charset = part.get_charset() or 'utf-8'
- return cte_payload.decode(charset, errors='surrogateescape')
+ charset = part.get_charset()
+ if charset and charset.input_codec:
+ encoding = charset.input_codec
+ else:
+ encoding = 'utf-8'
+ return cte_payload.decode(encoding, errors='surrogateescape')
return cte_payload
@@ -762,10 +766,6 @@ def fetch_ssl_details(paths=None):
return ssl_details
-def load_tfile_or_url(*args, **kwargs):
- return(decode_binary(read_file_or_url(*args, **kwargs).contents))
-
-
def read_file_or_url(url, timeout=5, retries=10,
headers=None, data=None, sec_between=1, ssl_details=None,
headers_cb=None, exception_cb=None):
@@ -833,10 +833,10 @@ def read_seeded(base="", ext="", timeout=5, retries=10, file_retries=0):
ud_url = "%s%s%s" % (base, "user-data", ext)
md_url = "%s%s%s" % (base, "meta-data", ext)
- md_resp = load_tfile_or_url(md_url, timeout, retries, file_retries)
+ md_resp = read_file_or_url(md_url, timeout, retries, file_retries)
md = None
if md_resp.ok():
- md = load_yaml(md_resp.contents, default={})
+ md = load_yaml(decode_binary(md_resp.contents), default={})
ud_resp = read_file_or_url(ud_url, timeout, retries, file_retries)
ud = None