diff -Nru cloud-init-0.7.7~bzr1091/debian/changelog cloud-init-0.7.7~bzr1091/debian/changelog --- cloud-init-0.7.7~bzr1091/debian/changelog 2015-04-17 10:04:17.000000000 -0600 +++ cloud-init-0.7.7~bzr1091/debian/changelog 2015-05-25 09:40:18.000000000 -0600 @@ -1,3 +1,12 @@ +cloud-init (0.7.7~bzr1091-0ubuntu2) vivid; urgency=medium + + * 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). + - Allow provisioning of Ubuntu Snappy w/out WALinuxAgent (LP: #1448885). + + -- Ben Howard Mon, 25 May 2015 09:38:58 -0600 + cloud-init (0.7.7~bzr1091-0ubuntu1) vivid; urgency=medium * New upstream snapshot. diff -Nru cloud-init-0.7.7~bzr1091/debian/patches/lp-1375252-1458052-Azure-hostname_password.patch cloud-init-0.7.7~bzr1091/debian/patches/lp-1375252-1458052-Azure-hostname_password.patch --- cloud-init-0.7.7~bzr1091/debian/patches/lp-1375252-1458052-Azure-hostname_password.patch 1969-12-31 17:00:00.000000000 -0700 +++ cloud-init-0.7.7~bzr1091/debian/patches/lp-1375252-1458052-Azure-hostname_password.patch 2015-05-25 09:45:31.000000000 -0600 @@ -0,0 +1,1489 @@ +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). + - Remove dependency on WALinuxAgent for provisioning (LP: #1448885). +Bug-Ubuntu: https://bugs.launchpad.net/bugs/1375252 +Bug-Ubuntu: https://bugs.launchpad.net/bugs/1448885 +Bug-Ubuntu: https://bugs.launchpad.net/bugs/1458052 + +--- cloud-init-0.7.7~bzr1091.orig/cloudinit/sources/DataSourceAzure.py ++++ cloud-init-0.7.7~bzr1091/cloudinit/sources/DataSourceAzure.py +@@ -17,17 +17,22 @@ + # along with this program. If not, see . + + 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 +@@ -132,8 +221,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( +@@ -154,46 +241,18 @@ class DataSourceAzureNet(sources.DataSou + # 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, 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() + + +@@ -384,14 +420,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=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: +- +- +- +- +- """ +- 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.7~bzr1091/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: ++ ++ ++ ++ ++ """ ++ root = ElementTree.fromstring(content) ++ depnode = root.find('Deployment') ++ return depnode.get('name') ++ ++ ++class WALinuxAgentShim(object): ++ ++ REPORT_READY_XML_TEMPLATE = '\n'.join([ ++ '', ++ '', ++ ' {incarnation}', ++ ' ', ++ ' {container_id}', ++ ' ', ++ ' ', ++ ' {instance_id}', ++ ' ', ++ ' Ready', ++ ' ', ++ ' ', ++ ' ', ++ ' ', ++ '']) ++ ++ 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.7~bzr1091.orig/tests/unittests/test_datasource/test_azure.py ++++ cloud-init-0.7.7~bzr1091/tests/unittests/test_datasource/test_azure.py +@@ -18,7 +18,7 @@ import stat + import yaml + import shutil + import tempfile +-import unittest ++import xml.etree.ElementTree as ET + + + def construct_valid_ovf_env(data=None, pubkeys=None, userdata=None): +@@ -116,9 +116,6 @@ class TestAzureDataSource(TestCase): + 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']}) +@@ -126,20 +123,61 @@ class TestAzureDataSource(TestCase): + mod = DataSourceAzure + mod.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d + ++ self.get_metadata_from_fabric = mock.MagicMock(return_value={ ++ 'instance-id': 'i-my-azure-id', ++ 'public-keys': [], ++ }) ++ + self.apply_patches([ + (mod, 'list_possible_azure_ds_devs', dsdevs), + (mod, 'invoke_agent', _invoke_agent), + (mod, 'wait_for_files', _wait_for_files), + (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), +- ]) ++ (mod, 'perform_hostname_bounce', mock.MagicMock()), ++ (mod, 'get_hostname', mock.MagicMock()), ++ (mod, 'set_hostname', mock.MagicMock()), ++ (mod, 'get_metadata_from_fabric', self.get_metadata_from_fabric), ++ ]) + + 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), +@@ -272,47 +310,6 @@ class TestAzureDataSource(TestCase): + for mypk in mypklist: + self.assertIn(mypk, dsrc.cfg['_pubkeys']) + +- 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': b64e(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': b64e(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 = {} +@@ -359,6 +356,31 @@ class TestAzureDataSource(TestCase): + + self.assertEqual(userdata.encode('us-ascii'), 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}) +@@ -368,7 +390,7 @@ class TestAzureDataSource(TestCase): + # 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_ovf_can_include_unicode(self): + xml = construct_valid_ovf_env(data={}) +@@ -417,12 +439,198 @@ class TestAzureDataSource(TestCase): + self.assertEqual(dsrc.userdata_raw, b"NEW_USERDATA") + self.assertTrue(os.path.exists( + os.path.join(self.waagent_d, 'otherfile'))) +- self.assertFalse( +- 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'))) ++ self.assertFalse(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'))) ++ new_xml = load_file(os.path.join(self.waagent_d, 'ovf-env.xml')) ++ self.xml_equals(new_ovfenv, new_xml) ++ ++ def test_exception_fetching_fabric_data_doesnt_propagate(self): ++ ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) ++ ds.ds_cfg['agent_command'] = '__builtin__' ++ self.get_metadata_from_fabric.side_effect = Exception ++ self.assertFalse(ds.get_data()) ++ ++ def test_fabric_data_included_in_metadata(self): ++ ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) ++ ds.ds_cfg['agent_command'] = '__builtin__' ++ self.get_metadata_from_fabric.return_value = {'test': 'value'} ++ ret = ds.get_data() ++ self.assertTrue(ret) ++ self.assertEqual('value', ds.metadata['test']) ++ ++ ++class TestAzureBounce(TestCase): ++ ++ def mock_out_azure_moving_parts(self): ++ self.patches.enter_context( ++ mock.patch.object(DataSourceAzure, 'invoke_agent')) ++ self.patches.enter_context( ++ mock.patch.object(DataSourceAzure, 'wait_for_files')) ++ self.patches.enter_context( ++ mock.patch.object(DataSourceAzure, 'iid_from_shared_config', ++ mock.MagicMock(return_value='i-my-azure-id'))) ++ self.patches.enter_context( ++ mock.patch.object(DataSourceAzure, 'list_possible_azure_ds_devs', ++ mock.MagicMock(return_value=[]))) ++ self.patches.enter_context( ++ mock.patch.object(DataSourceAzure, 'find_ephemeral_disk', ++ mock.MagicMock(return_value=None))) ++ self.patches.enter_context( ++ mock.patch.object(DataSourceAzure, 'find_ephemeral_part', ++ mock.MagicMock(return_value=None))) ++ self.patches.enter_context( ++ mock.patch.object(DataSourceAzure, 'get_metadata_from_fabric', ++ mock.MagicMock(return_value={}))) ++ ++ def setUp(self): ++ super(TestAzureBounce, self).setUp() ++ self.tmp = tempfile.mkdtemp() ++ self.waagent_d = os.path.join(self.tmp, 'var', 'lib', 'waagent') ++ self.paths = helpers.Paths({'cloud_dir': self.tmp}) ++ self.addCleanup(shutil.rmtree, self.tmp) ++ DataSourceAzure.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d ++ self.patches = ExitStack() ++ self.mock_out_azure_moving_parts() ++ self.get_hostname = self.patches.enter_context( ++ mock.patch.object(DataSourceAzure, 'get_hostname')) ++ self.set_hostname = self.patches.enter_context( ++ mock.patch.object(DataSourceAzure, 'set_hostname')) ++ self.subp = self.patches.enter_context( ++ mock.patch('cloudinit.sources.DataSourceAzure.util.subp')) ++ ++ def tearDown(self): ++ self.patches.close() ++ ++ def _get_ds(self, ovfcontent=None): ++ if ovfcontent is not None: ++ populate_dir(os.path.join(self.paths.seed_dir, "azure"), ++ {'ovf-env.xml': ovfcontent}) ++ return DataSourceAzure.DataSourceAzureNet( ++ {}, distro=None, paths=self.paths) ++ ++ def get_ovf_env_with_dscfg(self, hostname, cfg): ++ odata = { ++ 'HostName': hostname, ++ 'dscfg': { ++ 'text': b64e(yaml.dump(cfg)), ++ 'encoding': 'base64' ++ } ++ } ++ return construct_valid_ovf_env(data=odata) ++ ++ def test_disabled_bounce_does_not_change_hostname(self): ++ cfg = {'hostname_bounce': {'policy': 'off'}} ++ self._get_ds(self.get_ovf_env_with_dscfg('test-host', cfg)).get_data() ++ self.assertEqual(0, self.set_hostname.call_count) ++ ++ @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') ++ def test_disabled_bounce_does_not_perform_bounce( ++ self, perform_hostname_bounce): ++ cfg = {'hostname_bounce': {'policy': 'off'}} ++ self._get_ds(self.get_ovf_env_with_dscfg('test-host', cfg)).get_data() ++ self.assertEqual(0, perform_hostname_bounce.call_count) ++ ++ def test_same_hostname_does_not_change_hostname(self): ++ host_name = 'unchanged-host-name' ++ self.get_hostname.return_value = host_name ++ cfg = {'hostname_bounce': {'policy': 'yes'}} ++ self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg)).get_data() ++ self.assertEqual(0, self.set_hostname.call_count) ++ ++ @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') ++ def test_unchanged_hostname_does_not_perform_bounce( ++ self, perform_hostname_bounce): ++ host_name = 'unchanged-host-name' ++ self.get_hostname.return_value = host_name ++ cfg = {'hostname_bounce': {'policy': 'yes'}} ++ self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg)).get_data() ++ self.assertEqual(0, perform_hostname_bounce.call_count) ++ ++ @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') ++ def test_force_performs_bounce_regardless(self, perform_hostname_bounce): ++ host_name = 'unchanged-host-name' ++ self.get_hostname.return_value = host_name ++ cfg = {'hostname_bounce': {'policy': 'force'}} ++ self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg)).get_data() ++ self.assertEqual(1, perform_hostname_bounce.call_count) ++ ++ def test_different_hostnames_sets_hostname(self): ++ expected_hostname = 'azure-expected-host-name' ++ self.get_hostname.return_value = 'default-host-name' ++ self._get_ds( ++ self.get_ovf_env_with_dscfg(expected_hostname, {})).get_data() ++ self.assertEqual(expected_hostname, ++ self.set_hostname.call_args_list[0][0][0]) ++ ++ @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') ++ def test_different_hostnames_performs_bounce( ++ self, perform_hostname_bounce): ++ expected_hostname = 'azure-expected-host-name' ++ self.get_hostname.return_value = 'default-host-name' ++ self._get_ds( ++ self.get_ovf_env_with_dscfg(expected_hostname, {})).get_data() ++ self.assertEqual(1, perform_hostname_bounce.call_count) ++ ++ def test_different_hostnames_sets_hostname_back(self): ++ initial_host_name = 'default-host-name' ++ self.get_hostname.return_value = initial_host_name ++ self._get_ds( ++ self.get_ovf_env_with_dscfg('some-host-name', {})).get_data() ++ self.assertEqual(initial_host_name, ++ self.set_hostname.call_args_list[-1][0][0]) ++ ++ @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') ++ def test_failure_in_bounce_still_resets_host_name( ++ self, perform_hostname_bounce): ++ perform_hostname_bounce.side_effect = Exception ++ initial_host_name = 'default-host-name' ++ self.get_hostname.return_value = initial_host_name ++ self._get_ds( ++ self.get_ovf_env_with_dscfg('some-host-name', {})).get_data() ++ self.assertEqual(initial_host_name, ++ self.set_hostname.call_args_list[-1][0][0]) ++ ++ def test_environment_correct_for_bounce_command(self): ++ interface = 'int0' ++ hostname = 'my-new-host' ++ old_hostname = 'my-old-host' ++ self.get_hostname.return_value = old_hostname ++ cfg = {'hostname_bounce': {'interface': interface, 'policy': 'force'}} ++ data = self.get_ovf_env_with_dscfg(hostname, cfg) ++ self._get_ds(data).get_data() ++ self.assertEqual(1, self.subp.call_count) ++ bounce_env = self.subp.call_args[1]['env'] ++ self.assertEqual(interface, bounce_env['interface']) ++ self.assertEqual(hostname, bounce_env['hostname']) ++ self.assertEqual(old_hostname, bounce_env['old_hostname']) ++ ++ def test_default_bounce_command_used_by_default(self): ++ cmd = 'default-bounce-command' ++ DataSourceAzure.BUILTIN_DS_CONFIG['hostname_bounce']['command'] = cmd ++ cfg = {'hostname_bounce': {'policy': 'force'}} ++ data = self.get_ovf_env_with_dscfg('some-hostname', cfg) ++ self._get_ds(data).get_data() ++ self.assertEqual(1, self.subp.call_count) ++ bounce_args = self.subp.call_args[1]['args'] ++ self.assertEqual(cmd, bounce_args) ++ ++ @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') ++ def test_set_hostname_option_can_disable_bounce( ++ self, perform_hostname_bounce): ++ cfg = {'set_hostname': False, 'hostname_bounce': {'policy': 'force'}} ++ data = self.get_ovf_env_with_dscfg('some-hostname', cfg) ++ self._get_ds(data).get_data() ++ ++ self.assertEqual(0, perform_hostname_bounce.call_count) ++ ++ def test_set_hostname_option_can_disable_hostname_set(self): ++ cfg = {'set_hostname': False, 'hostname_bounce': {'policy': 'force'}} ++ data = self.get_ovf_env_with_dscfg('some-hostname', cfg) ++ self._get_ds(data).get_data() ++ ++ self.assertEqual(0, self.set_hostname.call_count) + + + class TestReadAzureOvf(TestCase): +@@ -438,17 +646,3 @@ class TestReadAzureOvf(TestCase): + (_md, _ud, cfg) = DataSourceAzure.read_azure_ovf(content) + for mypk in mypklist: + self.assertIn(mypk, cfg['_pubkeys']) +- +- +-class TestReadAzureSharedConfig(unittest.TestCase): +- def test_valid_content(self): +- xml = """ +- +- +- +- +- +- +- """ +- ret = DataSourceAzure.iid_from_shared_config_content(xml) +- self.assertEqual("MY_INSTANCE_ID", ret) +--- /dev/null ++++ cloud-init-0.7.7~bzr1091/tests/unittests/test_datasource/test_azure_helper.py +@@ -0,0 +1,444 @@ ++import os ++import struct ++import unittest ++ ++from cloudinit.sources.helpers import azure as azure_helper ++from ..helpers import TestCase ++ ++try: ++ from unittest import mock ++except ImportError: ++ import mock ++ ++try: ++ from contextlib import ExitStack ++except ImportError: ++ from contextlib2 import ExitStack ++ ++ ++GOAL_STATE_TEMPLATE = """\ ++ ++ ++ 2012-11-30 ++ {incarnation} ++ ++ Started ++ 300000 ++ ++ 16001 ++ ++ FALSE ++ ++ ++ {container_id} ++ ++ ++ {instance_id} ++ Started ++ ++ ++ http://100.86.192.70:80/...hostingEnvironmentConfig... ++ ++ {shared_config_url} ++ ++ http://100.86.192.70:80/...extensionsConfig... ++ ++ http://100.86.192.70:80/...fullConfig... ++ {certificates_url} ++ 68ce47.0.68ce47.0.utl-trusty--292258.1.xml ++ ++ ++ ++ ++ ++""" ++ ++ ++class TestReadAzureSharedConfig(unittest.TestCase): ++ ++ def test_valid_content(self): ++ xml = """ ++ ++ ++ ++ ++ ++ ++ """ ++ ret = azure_helper.iid_from_shared_config_content(xml) ++ self.assertEqual("MY_INSTANCE_ID", ret) ++ ++ ++class TestFindEndpoint(TestCase): ++ ++ def setUp(self): ++ super(TestFindEndpoint, self).setUp() ++ patches = ExitStack() ++ self.addCleanup(patches.close) ++ ++ self.load_file = patches.enter_context( ++ mock.patch.object(azure_helper.util, 'load_file')) ++ ++ def test_missing_file(self): ++ self.load_file.side_effect = IOError ++ self.assertRaises(IOError, ++ azure_helper.WALinuxAgentShim.find_endpoint) ++ ++ def test_missing_special_azure_line(self): ++ self.load_file.return_value = '' ++ self.assertRaises(Exception, ++ azure_helper.WALinuxAgentShim.find_endpoint) ++ ++ def _build_lease_content(self, ip_address, use_hex=True): ++ ip_address_repr = ':'.join( ++ [hex(int(part)).replace('0x', '') ++ for part in ip_address.split('.')]) ++ if not use_hex: ++ ip_address_repr = struct.pack( ++ '>L', int(ip_address_repr.replace(':', ''), 16)) ++ ip_address_repr = '"{0}"'.format(ip_address_repr.decode('utf-8')) ++ return '\n'.join([ ++ 'lease {', ++ ' interface "eth0";', ++ ' option unknown-245 {0};'.format(ip_address_repr), ++ '}']) ++ ++ def test_hex_string(self): ++ ip_address = '98.76.54.32' ++ file_content = self._build_lease_content(ip_address) ++ self.load_file.return_value = file_content ++ self.assertEqual(ip_address, ++ azure_helper.WALinuxAgentShim.find_endpoint()) ++ ++ def test_hex_string_with_single_character_part(self): ++ ip_address = '4.3.2.1' ++ file_content = self._build_lease_content(ip_address) ++ self.load_file.return_value = file_content ++ self.assertEqual(ip_address, ++ azure_helper.WALinuxAgentShim.find_endpoint()) ++ ++ def test_packed_string(self): ++ ip_address = '98.76.54.32' ++ file_content = self._build_lease_content(ip_address, use_hex=False) ++ self.load_file.return_value = file_content ++ self.assertEqual(ip_address, ++ azure_helper.WALinuxAgentShim.find_endpoint()) ++ ++ def test_latest_lease_used(self): ++ ip_addresses = ['4.3.2.1', '98.76.54.32'] ++ file_content = '\n'.join([self._build_lease_content(ip_address) ++ for ip_address in ip_addresses]) ++ self.load_file.return_value = file_content ++ self.assertEqual(ip_addresses[-1], ++ azure_helper.WALinuxAgentShim.find_endpoint()) ++ ++ ++class TestGoalStateParsing(TestCase): ++ ++ default_parameters = { ++ 'incarnation': 1, ++ 'container_id': 'MyContainerId', ++ 'instance_id': 'MyInstanceId', ++ 'shared_config_url': 'MySharedConfigUrl', ++ 'certificates_url': 'MyCertificatesUrl', ++ } ++ ++ def _get_goal_state(self, http_client=None, **kwargs): ++ if http_client is None: ++ http_client = mock.MagicMock() ++ parameters = self.default_parameters.copy() ++ parameters.update(kwargs) ++ xml = GOAL_STATE_TEMPLATE.format(**parameters) ++ if parameters['certificates_url'] is None: ++ new_xml_lines = [] ++ for line in xml.splitlines(): ++ if 'Certificates' in line: ++ continue ++ new_xml_lines.append(line) ++ xml = '\n'.join(new_xml_lines) ++ return azure_helper.GoalState(xml, http_client) ++ ++ def test_incarnation_parsed_correctly(self): ++ incarnation = '123' ++ goal_state = self._get_goal_state(incarnation=incarnation) ++ self.assertEqual(incarnation, goal_state.incarnation) ++ ++ def test_container_id_parsed_correctly(self): ++ container_id = 'TestContainerId' ++ goal_state = self._get_goal_state(container_id=container_id) ++ self.assertEqual(container_id, goal_state.container_id) ++ ++ def test_instance_id_parsed_correctly(self): ++ instance_id = 'TestInstanceId' ++ goal_state = self._get_goal_state(instance_id=instance_id) ++ self.assertEqual(instance_id, goal_state.instance_id) ++ ++ def test_shared_config_xml_parsed_and_fetched_correctly(self): ++ http_client = mock.MagicMock() ++ shared_config_url = 'TestSharedConfigUrl' ++ goal_state = self._get_goal_state( ++ http_client=http_client, shared_config_url=shared_config_url) ++ shared_config_xml = goal_state.shared_config_xml ++ self.assertEqual(1, http_client.get.call_count) ++ self.assertEqual(shared_config_url, http_client.get.call_args[0][0]) ++ self.assertEqual(http_client.get.return_value.contents, ++ shared_config_xml) ++ ++ def test_certificates_xml_parsed_and_fetched_correctly(self): ++ http_client = mock.MagicMock() ++ certificates_url = 'TestSharedConfigUrl' ++ goal_state = self._get_goal_state( ++ http_client=http_client, certificates_url=certificates_url) ++ certificates_xml = goal_state.certificates_xml ++ self.assertEqual(1, http_client.get.call_count) ++ self.assertEqual(certificates_url, http_client.get.call_args[0][0]) ++ self.assertTrue(http_client.get.call_args[1].get('secure', False)) ++ self.assertEqual(http_client.get.return_value.contents, ++ certificates_xml) ++ ++ def test_missing_certificates_skips_http_get(self): ++ http_client = mock.MagicMock() ++ goal_state = self._get_goal_state( ++ http_client=http_client, certificates_url=None) ++ certificates_xml = goal_state.certificates_xml ++ self.assertEqual(0, http_client.get.call_count) ++ self.assertIsNone(certificates_xml) ++ ++ ++class TestAzureEndpointHttpClient(TestCase): ++ ++ regular_headers = { ++ 'x-ms-agent-name': 'WALinuxAgent', ++ 'x-ms-version': '2012-11-30', ++ } ++ ++ def setUp(self): ++ super(TestAzureEndpointHttpClient, self).setUp() ++ patches = ExitStack() ++ self.addCleanup(patches.close) ++ ++ self.read_file_or_url = patches.enter_context( ++ mock.patch.object(azure_helper.util, 'read_file_or_url')) ++ ++ def test_non_secure_get(self): ++ client = azure_helper.AzureEndpointHttpClient(mock.MagicMock()) ++ url = 'MyTestUrl' ++ response = client.get(url, secure=False) ++ self.assertEqual(1, self.read_file_or_url.call_count) ++ self.assertEqual(self.read_file_or_url.return_value, response) ++ self.assertEqual(mock.call(url, headers=self.regular_headers), ++ self.read_file_or_url.call_args) ++ ++ def test_secure_get(self): ++ url = 'MyTestUrl' ++ certificate = mock.MagicMock() ++ expected_headers = self.regular_headers.copy() ++ expected_headers.update({ ++ "x-ms-cipher-name": "DES_EDE3_CBC", ++ "x-ms-guest-agent-public-x509-cert": certificate, ++ }) ++ client = azure_helper.AzureEndpointHttpClient(certificate) ++ response = client.get(url, secure=True) ++ self.assertEqual(1, self.read_file_or_url.call_count) ++ self.assertEqual(self.read_file_or_url.return_value, response) ++ self.assertEqual(mock.call(url, headers=expected_headers), ++ self.read_file_or_url.call_args) ++ ++ def test_post(self): ++ data = mock.MagicMock() ++ url = 'MyTestUrl' ++ client = azure_helper.AzureEndpointHttpClient(mock.MagicMock()) ++ response = client.post(url, data=data) ++ self.assertEqual(1, self.read_file_or_url.call_count) ++ self.assertEqual(self.read_file_or_url.return_value, response) ++ self.assertEqual( ++ mock.call(url, data=data, headers=self.regular_headers), ++ self.read_file_or_url.call_args) ++ ++ def test_post_with_extra_headers(self): ++ url = 'MyTestUrl' ++ client = azure_helper.AzureEndpointHttpClient(mock.MagicMock()) ++ extra_headers = {'test': 'header'} ++ client.post(url, extra_headers=extra_headers) ++ self.assertEqual(1, self.read_file_or_url.call_count) ++ expected_headers = self.regular_headers.copy() ++ expected_headers.update(extra_headers) ++ self.assertEqual( ++ mock.call(mock.ANY, data=mock.ANY, headers=expected_headers), ++ self.read_file_or_url.call_args) ++ ++ ++class TestOpenSSLManager(TestCase): ++ ++ def setUp(self): ++ super(TestOpenSSLManager, self).setUp() ++ patches = ExitStack() ++ self.addCleanup(patches.close) ++ ++ self.subp = patches.enter_context( ++ mock.patch.object(azure_helper.util, 'subp')) ++ try: ++ self.open = patches.enter_context( ++ mock.patch('__builtin__.open')) ++ except ImportError: ++ self.open = patches.enter_context( ++ mock.patch('builtins.open')) ++ ++ @mock.patch.object(azure_helper, 'cd', mock.MagicMock()) ++ @mock.patch.object(azure_helper.tempfile, 'mkdtemp') ++ def test_openssl_manager_creates_a_tmpdir(self, mkdtemp): ++ manager = azure_helper.OpenSSLManager() ++ self.assertEqual(mkdtemp.return_value, manager.tmpdir) ++ ++ def test_generate_certificate_uses_tmpdir(self): ++ subp_directory = {} ++ ++ def capture_directory(*args, **kwargs): ++ subp_directory['path'] = os.getcwd() ++ ++ self.subp.side_effect = capture_directory ++ manager = azure_helper.OpenSSLManager() ++ self.assertEqual(manager.tmpdir, subp_directory['path']) ++ ++ @mock.patch.object(azure_helper, 'cd', mock.MagicMock()) ++ @mock.patch.object(azure_helper.tempfile, 'mkdtemp', mock.MagicMock()) ++ @mock.patch.object(azure_helper.util, 'del_dir') ++ def test_clean_up(self, del_dir): ++ manager = azure_helper.OpenSSLManager() ++ manager.clean_up() ++ self.assertEqual([mock.call(manager.tmpdir)], del_dir.call_args_list) ++ ++ ++class TestWALinuxAgentShim(TestCase): ++ ++ def setUp(self): ++ super(TestWALinuxAgentShim, self).setUp() ++ patches = ExitStack() ++ self.addCleanup(patches.close) ++ ++ self.AzureEndpointHttpClient = patches.enter_context( ++ mock.patch.object(azure_helper, 'AzureEndpointHttpClient')) ++ self.find_endpoint = patches.enter_context( ++ mock.patch.object( ++ azure_helper.WALinuxAgentShim, 'find_endpoint')) ++ self.GoalState = patches.enter_context( ++ mock.patch.object(azure_helper, 'GoalState')) ++ self.iid_from_shared_config_content = patches.enter_context( ++ mock.patch.object(azure_helper, 'iid_from_shared_config_content')) ++ self.OpenSSLManager = patches.enter_context( ++ mock.patch.object(azure_helper, 'OpenSSLManager')) ++ patches.enter_context( ++ mock.patch.object(azure_helper.time, 'sleep', mock.MagicMock())) ++ ++ def test_http_client_uses_certificate(self): ++ shim = azure_helper.WALinuxAgentShim() ++ shim.register_with_azure_and_fetch_data() ++ self.assertEqual( ++ [mock.call(self.OpenSSLManager.return_value.certificate)], ++ self.AzureEndpointHttpClient.call_args_list) ++ ++ def test_correct_url_used_for_goalstate(self): ++ self.find_endpoint.return_value = 'test_endpoint' ++ shim = azure_helper.WALinuxAgentShim() ++ shim.register_with_azure_and_fetch_data() ++ get = self.AzureEndpointHttpClient.return_value.get ++ self.assertEqual( ++ [mock.call('http://test_endpoint/machine/?comp=goalstate')], ++ get.call_args_list) ++ self.assertEqual( ++ [mock.call(get.return_value.contents, ++ self.AzureEndpointHttpClient.return_value)], ++ self.GoalState.call_args_list) ++ ++ def test_certificates_used_to_determine_public_keys(self): ++ shim = azure_helper.WALinuxAgentShim() ++ data = shim.register_with_azure_and_fetch_data() ++ self.assertEqual( ++ [mock.call(self.GoalState.return_value.certificates_xml)], ++ self.OpenSSLManager.return_value.parse_certificates.call_args_list) ++ self.assertEqual( ++ self.OpenSSLManager.return_value.parse_certificates.return_value, ++ data['public-keys']) ++ ++ def test_absent_certificates_produces_empty_public_keys(self): ++ self.GoalState.return_value.certificates_xml = None ++ shim = azure_helper.WALinuxAgentShim() ++ data = shim.register_with_azure_and_fetch_data() ++ self.assertEqual([], data['public-keys']) ++ ++ def test_instance_id_returned_in_data(self): ++ shim = azure_helper.WALinuxAgentShim() ++ data = shim.register_with_azure_and_fetch_data() ++ self.assertEqual( ++ [mock.call(self.GoalState.return_value.shared_config_xml)], ++ self.iid_from_shared_config_content.call_args_list) ++ self.assertEqual(self.iid_from_shared_config_content.return_value, ++ data['instance-id']) ++ ++ def test_correct_url_used_for_report_ready(self): ++ self.find_endpoint.return_value = 'test_endpoint' ++ shim = azure_helper.WALinuxAgentShim() ++ shim.register_with_azure_and_fetch_data() ++ expected_url = 'http://test_endpoint/machine?comp=health' ++ self.assertEqual( ++ [mock.call(expected_url, data=mock.ANY, extra_headers=mock.ANY)], ++ self.AzureEndpointHttpClient.return_value.post.call_args_list) ++ ++ def test_goal_state_values_used_for_report_ready(self): ++ self.GoalState.return_value.incarnation = 'TestIncarnation' ++ self.GoalState.return_value.container_id = 'TestContainerId' ++ self.GoalState.return_value.instance_id = 'TestInstanceId' ++ shim = azure_helper.WALinuxAgentShim() ++ shim.register_with_azure_and_fetch_data() ++ posted_document = ( ++ self.AzureEndpointHttpClient.return_value.post.call_args[1]['data'] ++ ) ++ self.assertIn('TestIncarnation', posted_document) ++ self.assertIn('TestContainerId', posted_document) ++ self.assertIn('TestInstanceId', posted_document) ++ ++ def test_clean_up_can_be_called_at_any_time(self): ++ shim = azure_helper.WALinuxAgentShim() ++ shim.clean_up() ++ ++ def test_clean_up_will_clean_up_openssl_manager_if_instantiated(self): ++ shim = azure_helper.WALinuxAgentShim() ++ shim.register_with_azure_and_fetch_data() ++ shim.clean_up() ++ self.assertEqual( ++ 1, self.OpenSSLManager.return_value.clean_up.call_count) ++ ++ def test_failure_to_fetch_goalstate_bubbles_up(self): ++ class SentinelException(Exception): ++ pass ++ self.AzureEndpointHttpClient.return_value.get.side_effect = ( ++ SentinelException) ++ shim = azure_helper.WALinuxAgentShim() ++ self.assertRaises(SentinelException, ++ shim.register_with_azure_and_fetch_data) ++ ++ ++class TestGetMetadataFromFabric(TestCase): ++ ++ @mock.patch.object(azure_helper, 'WALinuxAgentShim') ++ def test_data_from_shim_returned(self, shim): ++ ret = azure_helper.get_metadata_from_fabric() ++ self.assertEqual( ++ shim.return_value.register_with_azure_and_fetch_data.return_value, ++ ret) ++ ++ @mock.patch.object(azure_helper, 'WALinuxAgentShim') ++ def test_success_calls_clean_up(self, shim): ++ azure_helper.get_metadata_from_fabric() ++ self.assertEqual(1, shim.return_value.clean_up.call_count) ++ ++ @mock.patch.object(azure_helper, 'WALinuxAgentShim') ++ def test_failure_in_registration_calls_clean_up(self, shim): ++ class SentinelException(Exception): ++ pass ++ shim.return_value.register_with_azure_and_fetch_data.side_effect = ( ++ SentinelException) ++ self.assertRaises(SentinelException, ++ azure_helper.get_metadata_from_fabric) ++ self.assertEqual(1, shim.return_value.clean_up.call_count) diff -Nru cloud-init-0.7.7~bzr1091/debian/patches/series cloud-init-0.7.7~bzr1091/debian/patches/series --- cloud-init-0.7.7~bzr1091/debian/patches/series 2015-04-16 15:05:14.000000000 -0600 +++ cloud-init-0.7.7~bzr1091/debian/patches/series 2015-05-25 09:44:30.000000000 -0600 @@ -0,0 +1 @@ +lp-1375252-1458052-Azure-hostname_password.patch