summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Moser <smoser@brickies.net>2017-01-10 11:25:27 -0500
committerScott Moser <smoser@brickies.net>2017-01-10 11:25:27 -0500
commitb09ab0098d87a19cf37494f1cde98698ca966f98 (patch)
treeb6bc79271eafe652f253dc56e89a9fb8669f5e04
parent670cd160f43cd4d2ff32d03515926a019c966fb8 (diff)
downloadcloud-init-git-ubuntu/0.7.5-0ubuntu1.6.tar.gz
Import version 0.7.5-0ubuntu1.6ubuntu/0.7.5-0ubuntu1.6
Imported using git-import-dsc
-rw-r--r--debian/changelog9
-rw-r--r--debian/patches/lp-1375252-1458052-Azure-hostname_password.patch845
-rw-r--r--debian/patches/series1
3 files changed, 855 insertions, 0 deletions
diff --git a/debian/changelog b/debian/changelog
index 95bfb5d7..fc7c06fa 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,12 @@
+cloud-init (0.7.5-0ubuntu1.6) trusty; urgency=medium
+
+ * d/patches/lp-1375252-1458052-Azure-hostname_password.patch:
+ Backport of 15.10 Azure Datasource to fix various issues:
+ - Azure Datasource writes user password in plain text (LP: #1458052).
+ - Hostname not preserved across Azure reboots (LP: #1375252).
+
+ -- Ben Howard <ben.howard@ubuntu.com> Mon, 25 May 2015 09:30:20 -0600
+
cloud-init (0.7.5-0ubuntu1.5) trusty; urgency=medium
* Backport support for fetching passwords in CloudStack (LP: #1422388).
diff --git a/debian/patches/lp-1375252-1458052-Azure-hostname_password.patch b/debian/patches/lp-1375252-1458052-Azure-hostname_password.patch
new file mode 100644
index 00000000..1355f73a
--- /dev/null
+++ b/debian/patches/lp-1375252-1458052-Azure-hostname_password.patch
@@ -0,0 +1,845 @@
+Description: Backport the 15.10 Azure Datasource
+ Backport of 15.10 Azure Datasource to fix various issues:
+ - Azure Datasource writes user password in plain text (LP: #1458052).
+ - Hostname not preserved across Azure reboots (LP: #1375252).
+Author: Ben Howard <ben.howard@ubuntu.com>
+Bug-Ubuntu: https://bugs.launchpad.net/bugs/1375252
+Bug-Ubuntu: https://bugs.launchpad.net/bugs/1458052
+Forwarded: yes
+
+--- cloud-init-0.7.6~bzr1022.orig/cloudinit/sources/DataSourceAzure.py
++++ cloud-init-0.7.6~bzr1022/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.DataSou
+ 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
+@@ -131,8 +220,6 @@ class DataSourceAzureNet(sources.DataSou
+ # 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(
+@@ -151,48 +238,20 @@ class DataSourceAzureNet(sources.DataSou
+
+ # walinux agent writes files world readable, but expects
+ # the directory to be protected.
+- write_files(ddir, 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")
++ write_files(ddir, files, dirmode=0o700)
+
+- 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:
+@@ -298,39 +357,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
+@@ -343,15 +378,16 @@ def apply_hostname_bounce(hostname, poli
+ 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()
+
+
+@@ -383,14 +419,30 @@ def wait_for_files(flist, maxwait=60, na
+
+
+ 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=0600)
++ 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):
+@@ -461,20 +513,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)
+@@ -559,7 +597,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
+
+@@ -592,7 +630,7 @@ def load_azure_ds_dir(source_dir):
+ if not os.path.isfile(ovf_file):
+ raise NonAzureDataSource("No ovf-env file found")
+
+- with open(ovf_file, "r") as fp:
++ with open(ovf_file, "rb") as fp:
+ contents = fp.read()
+
+ md, ud, cfg = read_azure_ovf(contents)
+@@ -605,19 +643,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
+
+--- /dev/null
++++ cloud-init-0.7.6~bzr1022/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()
+--- cloud-init-0.7.6~bzr1022.orig/tests/unittests/test_datasource/test_azure.py
++++ cloud-init-0.7.6~bzr1022/tests/unittests/test_datasource/test_azure.py
+@@ -9,6 +9,20 @@ from mocker import MockerTestCase
+ import os
+ import stat
+ import yaml
++import xml.etree.ElementTree as ET
++
++OVERRIDE_BUILTIN_DS_CONFIG = {
++ 'agent_command': ['bin/true'],
++ 'data_dir': "/var/lib/waagent",
++ 'set_hostname': True,
++ 'hostname_bounce': {
++ 'interface': 'eth0',
++ 'policy': False,
++ 'command': '/bin/true',
++ 'hostname_command': '/bin/true',
++ },
++ 'disk_aliases': {'ephemeral0': '/dev/sdb'},
++}
+
+
+ def construct_valid_ovf_env(data=None, pubkeys=None, userdata=None):
+@@ -107,14 +121,13 @@ class TestAzureDataSource(MockerTestCase
+ data['iid_from_shared_cfg'] = path
+ return 'i-my-azure-id'
+
+- def _apply_hostname_bounce(**kwargs):
+- data['apply_hostname_bounce'] = kwargs
+-
+ if data.get('ovfcontent') is not None:
+ populate_dir(os.path.join(self.paths.seed_dir, "azure"),
+ {'ovf-env.xml': data['ovfcontent']})
+
++
+ mod = DataSourceAzure
++ mod.BUILTIN_DS_CONFIG = OVERRIDE_BUILTIN_DS_CONFIG
+ mod.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
+
+ self.apply_patches([(mod, 'list_possible_azure_ds_devs', dsdevs)])
+@@ -124,15 +137,46 @@ class TestAzureDataSource(MockerTestCase
+ (mod, 'pubkeys_from_crt_files',
+ _pubkeys_from_crt_files),
+ (mod, 'iid_from_shared_config',
+- _iid_from_shared_config),
+- (mod, 'apply_hostname_bounce',
+- _apply_hostname_bounce), ])
++ _iid_from_shared_config)])
+
+ dsrc = mod.DataSourceAzureNet(
+ data.get('sys_cfg', {}), distro=None, paths=self.paths)
+
+ return dsrc
+
++ def xml_equals(self, oxml, nxml):
++ """Compare two sets of XML to make sure they are equal"""
++
++ def create_tag_index(xml):
++ et = ET.fromstring(xml)
++ ret = {}
++ for x in et.iter():
++ ret[x.tag] = x
++ return ret
++
++ def tags_exists(x, y):
++ for tag in x.keys():
++ self.assertIn(tag, y)
++ for tag in y.keys():
++ self.assertIn(tag, x)
++
++ def tags_equal(x, y):
++ for x_tag, x_val in x.items():
++ y_val = y.get(x_val.tag)
++ self.assertEquals(x_val.text, y_val.text)
++
++ old_cnt = create_tag_index(oxml)
++ new_cnt = create_tag_index(nxml)
++ tags_exists(old_cnt, new_cnt)
++ tags_equal(old_cnt, new_cnt)
++
++ def xml_notequals(self, oxml, nxml):
++ try:
++ self.xml_equals(oxml, nxml)
++ except AssertionError as e:
++ return
++ raise AssertionError("XML is the same")
++
+ def test_basic_seed_dir(self):
+ odata = {'HostName': "myhost", 'UserName': "myuser"}
+ data = {'ovfcontent': construct_valid_ovf_env(data=odata),
+@@ -258,44 +302,6 @@ class TestAzureDataSource(MockerTestCase
+ def test_disabled_bounce(self):
+ pass
+
+- def test_apply_bounce_call_1(self):
+- # hostname needs to get through to apply_hostname_bounce
+- odata = {'HostName': 'my-random-hostname'}
+- data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
+-
+- self._get_ds(data).get_data()
+- self.assertIn('hostname', data['apply_hostname_bounce'])
+- self.assertEqual(data['apply_hostname_bounce']['hostname'],
+- odata['HostName'])
+-
+- def test_apply_bounce_call_configurable(self):
+- # hostname_bounce should be configurable in datasource cfg
+- cfg = {'hostname_bounce': {'interface': 'eth1', 'policy': 'off',
+- 'command': 'my-bounce-command',
+- 'hostname_command': 'my-hostname-command'}}
+- odata = {'HostName': "xhost",
+- 'dscfg': {'text': base64.b64encode(yaml.dump(cfg)),
+- 'encoding': 'base64'}}
+- data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
+- self._get_ds(data).get_data()
+-
+- for k in cfg['hostname_bounce']:
+- self.assertIn(k, data['apply_hostname_bounce'])
+-
+- for k, v in cfg['hostname_bounce'].items():
+- self.assertEqual(data['apply_hostname_bounce'][k], v)
+-
+- def test_set_hostname_disabled(self):
+- # config specifying set_hostname off should not bounce
+- cfg = {'set_hostname': False}
+- odata = {'HostName': "xhost",
+- 'dscfg': {'text': base64.b64encode(yaml.dump(cfg)),
+- 'encoding': 'base64'}}
+- data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
+- self._get_ds(data).get_data()
+-
+- self.assertEqual(data.get('apply_hostname_bounce', "N/A"), "N/A")
+-
+ def test_default_ephemeral(self):
+ # make sure the ephemeral device works
+ odata = {}
+@@ -342,6 +348,31 @@ class TestAzureDataSource(MockerTestCase
+
+ self.assertEqual(userdata, dsrc.userdata_raw)
+
++ def test_password_redacted_in_ovf(self):
++ odata = {'HostName': "myhost", 'UserName': "myuser",
++ 'UserPassword': "mypass"}
++ data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
++ dsrc = self._get_ds(data)
++ ret = dsrc.get_data()
++
++ self.assertTrue(ret)
++ ovf_env_path = os.path.join(self.waagent_d, 'ovf-env.xml')
++
++ # The XML should not be same since the user password is redacted
++ on_disk_ovf = load_file(ovf_env_path)
++ self.xml_notequals(data['ovfcontent'], on_disk_ovf)
++
++ # Make sure that the redacted password on disk is not used by CI
++ self.assertNotEquals(dsrc.cfg.get('password'),
++ DataSourceAzure.DEF_PASSWD_REDACTION)
++
++ # Make sure that the password was really encrypted
++ et = ET.fromstring(on_disk_ovf)
++ for elem in et.iter():
++ if 'UserPassword' in elem.tag:
++ self.assertEquals(DataSourceAzure.DEF_PASSWD_REDACTION,
++ elem.text)
++
+ def test_ovf_env_arrives_in_waagent_dir(self):
+ xml = construct_valid_ovf_env(data={}, userdata="FOODATA")
+ dsrc = self._get_ds({'ovfcontent': xml})
+@@ -351,7 +382,7 @@ class TestAzureDataSource(MockerTestCase
+ # we expect that the ovf-env.xml file is copied there.
+ ovf_env_path = os.path.join(self.waagent_d, 'ovf-env.xml')
+ self.assertTrue(os.path.exists(ovf_env_path))
+- self.assertEqual(xml, load_file(ovf_env_path))
++ self.xml_equals(xml, load_file(ovf_env_path))
+
+ def test_existing_ovf_same(self):
+ # waagent/SharedConfig left alone if found ovf-env.xml same as cached
+@@ -398,9 +429,8 @@ class TestAzureDataSource(MockerTestCase
+ os.path.exists(os.path.join(self.waagent_d, 'SharedConfig.xml')))
+ self.assertTrue(
+ os.path.exists(os.path.join(self.waagent_d, 'ovf-env.xml')))
+- self.assertEqual(new_ovfenv,
+- load_file(os.path.join(self.waagent_d, 'ovf-env.xml')))
+-
++ new_xml = load_file(os.path.join(self.waagent_d, 'ovf-env.xml'))
++ self.xml_equals(new_ovfenv, new_xml)
+
+ class TestReadAzureOvf(MockerTestCase):
+ def test_invalid_xml_raises_non_azure_ds(self):
diff --git a/debian/patches/series b/debian/patches/series
index d5b3ff16..ab598321 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -6,3 +6,4 @@ lp-1404311-gce-data_encoding.patch
lp-1422919-azure-g5_ephemeral.patch
lp-1422388-cloudstack-passwords.patch
lp-1356855-fix-cloudstack-metadata.patch
+lp-1375252-1458052-Azure-hostname_password.patch