diff -Nru cloud-init-0.7.5/debian/changelog cloud-init-0.7.5/debian/changelog --- cloud-init-0.7.5/debian/changelog 2015-11-23 15:59:00.000000000 +0000 +++ cloud-init-0.7.5/debian/changelog 2016-02-05 20:36:36.000000000 +0000 @@ -1,3 +1,11 @@ +cloud-init (0.7.5-0ubuntu1.16) trusty; urgency=medium + + * Joyent Smart DataOS: + - d/patches/lp-1540965-SmartOS-Add-support-for-Joyent-LX-Brand-Zones.patch: + SmartOS: Add support for Joyent LX-Brand Zones (LP: #1540965) + + -- Robert C Jennings Tue, 02 Feb 2016 09:32:02 -0600 + cloud-init (0.7.5-0ubuntu1.15) trusty; urgency=medium * Microsoft Azure: diff -Nru cloud-init-0.7.5/debian/patches/lp-1540965-SmartOS-Add-support-for-Joyent-LX-Brand-Zones.patch cloud-init-0.7.5/debian/patches/lp-1540965-SmartOS-Add-support-for-Joyent-LX-Brand-Zones.patch --- cloud-init-0.7.5/debian/patches/lp-1540965-SmartOS-Add-support-for-Joyent-LX-Brand-Zones.patch 1970-01-01 00:00:00.000000000 +0000 +++ cloud-init-0.7.5/debian/patches/lp-1540965-SmartOS-Add-support-for-Joyent-LX-Brand-Zones.patch 2016-02-05 20:36:36.000000000 +0000 @@ -0,0 +1,713 @@ +From: Robert C Jennings +Date: Tue, 2 Feb 2016 09:21:07 -0600 +Subject: SmartOS: Add support for Joyent LX-Brand Zones + +LX-brand zones on Joyent's SmartOS use a different metadata source +(socket file) than the KVM-based SmartOS virtualization (serial port). +This patch adds support for recognizing the different flavors of +virtualization on SmartOS and setting up a metadata source file object. +After the file object is created, the rest of the code for the datasource +can remain common. + +This patch reads the metadata byte-by-byte rather than using readline +because we can not perform a readline on the file-like object for the +socket as this block indefintely waiting for an EOF that is will not be +sent by the host platform. This patch also moves to V2 metadata as it +provides checksum validation and makes reading the metadata much more +reliable. + +Author: Robert C Jennings +Bug-Ubuntu: https://launchpad.net/bugs/1540965 +--- + cloudinit/sources/DataSourceSmartOS.py | 373 +++++++++++++++--------- + doc/examples/cloud-config-datasources.txt | 7 + + tests/unittests/test_datasource/test_smartos.py | 60 +++- + 3 files changed, 288 insertions(+), 152 deletions(-) + +Index: b/cloudinit/sources/DataSourceSmartOS.py +=================================================================== +--- a/cloudinit/sources/DataSourceSmartOS.py ++++ b/cloudinit/sources/DataSourceSmartOS.py +@@ -20,28 +20,39 @@ + # Datasource for provisioning on SmartOS. This works on Joyent + # and public/private Clouds using SmartOS. + # +-# SmartOS hosts use a serial console (/dev/ttyS1) on Linux Guests. ++# SmartOS hosts use a serial console (/dev/ttyS1) on KVM Linux Guests + # The meta-data is transmitted via key/value pairs made by + # requests on the console. For example, to get the hostname, you + # would send "GET hostname" on /dev/ttyS1. ++# For Linux Guests running in LX-Brand Zones on SmartOS hosts ++# a socket (/native/.zonecontrol/metadata.sock) is used instead ++# of a serial console. + # + # Certain behavior is defined by the DataDictionary + # http://us-east.manta.joyent.com/jmc/public/mdata/datadict.html + # Comments with "@datadictionary" are snippets of the definition + + import base64 ++import binascii ++import contextlib ++import os ++import random ++import re ++import socket ++import stat ++ ++import serial ++ + from cloudinit import log as logging + from cloudinit import sources + from cloudinit import util +-import os +-import os.path +-import serial + + + LOG = logging.getLogger(__name__) + + SMARTOS_ATTRIB_MAP = { +- #Cloud-init Key : (SmartOS Key, Strip line endings) ++ # Cloud-init Key : (SmartOS Key, Strip line endings) ++ 'instance-id': ('sdc:uuid', True), + 'local-hostname': ('hostname', True), + 'public-keys': ('root_authorized_keys', True), + 'user-script': ('user-script', False), +@@ -72,6 +83,7 @@ DS_CFG_PATH = ['datasource', DS_NAME] + # + BUILTIN_DS_CONFIG = { + 'serial_device': '/dev/ttyS1', ++ 'metadata_sockfile': '/native/.zonecontrol/metadata.sock', + 'seed_timeout': 60, + 'no_base64_decode': ['root_authorized_keys', + 'motd_sys_info', +@@ -79,6 +91,7 @@ BUILTIN_DS_CONFIG = { + 'user-data', + 'user-script', + 'sdc:datacenter_name', ++ 'sdc:uuid', + ], + 'base64_keys': [], + 'base64_all': False, +@@ -96,21 +109,21 @@ BUILTIN_CLOUD_CONFIG = { + 'device': 'ephemeral0'}], + } + +-## builtin vendor-data is a boothook that writes a script into +-## /var/lib/cloud/scripts/per-boot. *That* script then handles +-## executing the 'operator-script' and 'user-script' files +-## that cloud-init writes into /var/lib/cloud/instance/data/ +-## if they exist. +-## +-## This is all very indirect, but its done like this so that at +-## some point in the future, perhaps cloud-init wouldn't do it at +-## all, but rather the vendor actually provide vendor-data that accomplished +-## their desires. (That is the point of vendor-data). +-## +-## cloud-init does cheat a bit, and write the operator-script and user-script +-## itself. It could have the vendor-script do that, but it seems better +-## to not require the image to contain a tool (mdata-get) to read those +-## keys when we have a perfectly good one inside cloud-init. ++# builtin vendor-data is a boothook that writes a script into ++# /var/lib/cloud/scripts/per-boot. *That* script then handles ++# executing the 'operator-script' and 'user-script' files ++# that cloud-init writes into /var/lib/cloud/instance/data/ ++# if they exist. ++# ++# This is all very indirect, but its done like this so that at ++# some point in the future, perhaps cloud-init wouldn't do it at ++# all, but rather the vendor actually provide vendor-data that accomplished ++# their desires. (That is the point of vendor-data). ++# ++# cloud-init does cheat a bit, and write the operator-script and user-script ++# itself. It could have the vendor-script do that, but it seems better ++# to not require the image to contain a tool (mdata-get) to read those ++# keys when we have a perfectly good one inside cloud-init. + BUILTIN_VENDOR_DATA = """\ + #cloud-boothook + #!/bin/sh +@@ -146,17 +159,27 @@ class DataSourceSmartOS(sources.DataSour + def __init__(self, sys_cfg, distro, paths): + sources.DataSource.__init__(self, sys_cfg, distro, paths) + self.is_smartdc = None +- + self.ds_cfg = util.mergemanydict([ + self.ds_cfg, + util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}), + BUILTIN_DS_CONFIG]) + + self.metadata = {} +- self.cfg = BUILTIN_CLOUD_CONFIG + +- self.seed = self.ds_cfg.get("serial_device") +- self.seed_timeout = self.ds_cfg.get("serial_timeout") ++ # SDC LX-Brand Zones lack dmidecode (no /dev/mem) but ++ # report 'BrandZ virtual linux' as the kernel version ++ if os.uname()[3].lower() == 'brandz virtual linux': ++ LOG.debug("Host is SmartOS, guest in Zone") ++ self.is_smartdc = True ++ self.smartos_type = 'lx-brand' ++ self.cfg = {} ++ self.seed = self.ds_cfg.get("metadata_sockfile") ++ else: ++ self.is_smartdc = True ++ self.smartos_type = 'kvm' ++ self.seed = self.ds_cfg.get("serial_device") ++ self.cfg = BUILTIN_CLOUD_CONFIG ++ self.seed_timeout = self.ds_cfg.get("serial_timeout") + self.smartos_no_base64 = self.ds_cfg.get('no_base64_decode') + self.b64_keys = self.ds_cfg.get('base64_keys') + self.b64_all = self.ds_cfg.get('base64_all') +@@ -166,12 +189,50 @@ class DataSourceSmartOS(sources.DataSour + root = sources.DataSource.__str__(self) + return "%s [seed=%s]" % (root, self.seed) + ++ def _get_seed_file_object(self): ++ if not self.seed: ++ raise AttributeError("seed device is not set") ++ ++ if self.smartos_type == 'lx-brand': ++ if not stat.S_ISSOCK(os.stat(self.seed).st_mode): ++ LOG.debug("Seed %s is not a socket", self.seed) ++ return None ++ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) ++ sock.connect(self.seed) ++ return sock.makefile('rw') ++ else: ++ if not stat.S_ISCHR(os.stat(self.seed).st_mode): ++ LOG.debug("Seed %s is not a character device") ++ return None ++ ser = serial.Serial(self.seed, timeout=self.seed_timeout) ++ if not ser.isOpen(): ++ raise SystemError("Unable to open %s" % self.seed) ++ return ser ++ return None ++ ++ def _set_provisioned(self): ++ '''Mark the instance provisioning state as successful. ++ ++ When run in a zone, the host OS will look for /var/svc/provisioning ++ to be renamed as /var/svc/provision_success. This should be done ++ after meta-data is successfully retrieved and from this point ++ the host considers the provision of the zone to be a success and ++ keeps the zone running. ++ ''' ++ ++ LOG.debug('Instance provisioning state set as successful') ++ svc_path = '/var/svc' ++ if os.path.exists('/'.join([svc_path, 'provisioning'])): ++ os.rename('/'.join([svc_path, 'provisioning']), ++ '/'.join([svc_path, 'provision_success'])) ++ + def get_data(self): + md = {} + ud = "" + +- if not os.path.exists(self.seed): +- LOG.debug("Host does not appear to be on SmartOS") ++ if not device_exists(self.seed): ++ LOG.debug("No metadata device '%s' found for SmartOS datasource", ++ self.seed) + return False + + uname_arch = os.uname()[4] +@@ -180,29 +241,36 @@ class DataSourceSmartOS(sources.DataSour + LOG.debug("Disabling SmartOS datasource on arm (LP: #1243287)") + return False + +- dmi_info = dmi_data() +- if dmi_info is False: +- LOG.debug("No dmidata utility found") ++ # SDC KVM instances will provide dmi data, LX-brand does not ++ if self.smartos_type == 'kvm': ++ dmi_info = dmi_data() ++ if dmi_info is False: ++ LOG.debug("No dmidata utility found") ++ return False ++ ++ system_type = dmi_info ++ if 'smartdc' not in system_type.lower(): ++ LOG.debug("Host is not on SmartOS. system_type=%s", ++ system_type) ++ return False ++ LOG.debug("Host is SmartOS, guest in KVM") ++ ++ seed_obj = self._get_seed_file_object() ++ if seed_obj is None: ++ LOG.debug('Seed file object not found.') + return False +- +- system_uuid, system_type = tuple(dmi_info) +- if 'smartdc' not in system_type.lower(): +- LOG.debug("Host is not on SmartOS. system_type=%s", system_type) +- return False +- self.is_smartdc = True +- md['instance-id'] = system_uuid +- +- b64_keys = self.query('base64_keys', strip=True, b64=False) +- if b64_keys is not None: +- self.b64_keys = [k.strip() for k in str(b64_keys).split(',')] +- +- b64_all = self.query('base64_all', strip=True, b64=False) +- if b64_all is not None: +- self.b64_all = util.is_true(b64_all) +- +- for ci_noun, attribute in SMARTOS_ATTRIB_MAP.iteritems(): +- smartos_noun, strip = attribute +- md[ci_noun] = self.query(smartos_noun, strip=strip) ++ with contextlib.closing(seed_obj) as seed: ++ b64_keys = self.query('base64_keys', seed, strip=True, b64=False) ++ if b64_keys is not None: ++ self.b64_keys = [k.strip() for k in str(b64_keys).split(',')] ++ ++ b64_all = self.query('base64_all', seed, strip=True, b64=False) ++ if b64_all is not None: ++ self.b64_all = util.is_true(b64_all) ++ ++ for ci_noun, attribute in SMARTOS_ATTRIB_MAP.iteritems(): ++ smartos_noun, strip = attribute ++ md[ci_noun] = self.query(smartos_noun, seed, strip=strip) + + # @datadictionary: This key may contain a program that is written + # to a file in the filesystem of the guest on each boot and then +@@ -217,11 +285,12 @@ class DataSourceSmartOS(sources.DataSour + user_script = os.path.join(data_d, 'user-script') + u_script_l = "%s/user-script" % LEGACY_USER_D + write_boot_content(md.get('user-script'), content_f=user_script, +- link=u_script_l, shebang=True, mode=0700) ++ link=u_script_l, shebang=True, mode=0o700) + + operator_script = os.path.join(data_d, 'operator-script') + write_boot_content(md.get('operator-script'), +- content_f=operator_script, shebang=False, mode=0700) ++ content_f=operator_script, shebang=False, ++ mode=0o700) + + # @datadictionary: This key has no defined format, but its value + # is written to the file /var/db/mdata-user-data on each boot prior +@@ -234,7 +303,7 @@ class DataSourceSmartOS(sources.DataSour + + # Handle the cloud-init regular meta + if not md['local-hostname']: +- md['local-hostname'] = system_uuid ++ md['local-hostname'] = md['instance-id'] + + ud = None + if md['user-data']: +@@ -251,6 +320,8 @@ class DataSourceSmartOS(sources.DataSour + self.metadata = util.mergemanydict([md, self.metadata]) + self.userdata_raw = ud + self.vendordata_raw = md['vendor-data'] ++ ++ self._set_provisioned() + return True + + def device_name_to_device(self, name): +@@ -262,120 +333,144 @@ class DataSourceSmartOS(sources.DataSour + def get_instance_id(self): + return self.metadata['instance-id'] + +- def query(self, noun, strip=False, default=None, b64=None): ++ def query(self, noun, seed_file, strip=False, default=None, b64=None): + if b64 is None: + if noun in self.smartos_no_base64: + b64 = False + elif self.b64_all or noun in self.b64_keys: + b64 = True + +- return query_data(noun=noun, strip=strip, seed_device=self.seed, +- seed_timeout=self.seed_timeout, default=default, +- b64=b64) ++ return self._query_data(noun, seed_file, strip=strip, ++ default=default, b64=b64) + ++ def _query_data(self, noun, seed_file, strip=False, ++ default=None, b64=None): ++ """Makes a request via "GET " ++ ++ In the response, the first line is the status, while subsequent ++ lines are is the value. A blank line with a "." is used to ++ indicate end of response. ++ ++ If the response is expected to be base64 encoded, then set ++ b64encoded to true. Unfortantely, there is no way to know if ++ something is 100% encoded, so this method relies on being told ++ if the data is base64 or not. ++ """ + +-def get_serial(seed_device, seed_timeout): +- """This is replaced in unit testing, allowing us to replace +- serial.Serial with a mocked class. +- +- The timeout value of 60 seconds should never be hit. The value +- is taken from SmartOS own provisioning tools. Since we are reading +- each line individually up until the single ".", the transfer is +- usually very fast (i.e. microseconds) to get the response. +- """ +- if not seed_device: +- raise AttributeError("seed_device value is not set") ++ if not noun: ++ return False + +- ser = serial.Serial(seed_device, timeout=seed_timeout) +- if not ser.isOpen(): +- raise SystemError("Unable to open %s" % seed_device) ++ response = JoyentMetadataClient(seed_file).get_metadata(noun) + +- return ser ++ if response is None: ++ return default + ++ if b64 is None: ++ b64 = self._query_data('b64-%s' % noun, seed_file, b64=False, ++ default=False, strip=True) ++ b64 = util.is_true(b64) ++ ++ resp = None ++ if b64 or strip: ++ resp = "".join(response).rstrip() ++ else: ++ resp = "".join(response) + +-def query_data(noun, seed_device, seed_timeout, strip=False, default=None, +- b64=None): +- """Makes a request to via the serial console via "GET " ++ if b64: ++ try: ++ return base64.b64decode(resp) ++ except TypeError: ++ LOG.warn("Failed base64 decoding key '%s'", noun) ++ return resp + +- In the response, the first line is the status, while subsequent lines +- are is the value. A blank line with a "." is used to indicate end of +- response. ++ return resp + +- If the response is expected to be base64 encoded, then set b64encoded +- to true. Unfortantely, there is no way to know if something is 100% +- encoded, so this method relies on being told if the data is base64 or +- not. +- """ + +- if not noun: +- return False ++def device_exists(device): ++ """Symplistic method to determine if the device exists or not""" ++ return os.path.exists(device) + +- ser = get_serial(seed_device, seed_timeout) +- ser.write("GET %s\n" % noun.rstrip()) +- status = str(ser.readline()).rstrip() +- response = [] +- eom_found = False +- +- if 'SUCCESS' not in status: +- ser.close() +- return default +- +- while not eom_found: +- m = ser.readline() +- if m.rstrip() == ".": +- eom_found = True +- else: +- response.append(m) + +- ser.close() ++class JoyentMetadataFetchException(Exception): ++ pass + +- if b64 is None: +- b64 = query_data('b64-%s' % noun, seed_device=seed_device, +- seed_timeout=seed_timeout, b64=False, +- default=False, strip=True) +- b64 = util.is_true(b64) +- +- resp = None +- if b64 or strip: +- resp = "".join(response).rstrip() +- else: +- resp = "".join(response) + +- if b64: +- try: +- return base64.b64decode(resp) +- except TypeError: +- LOG.warn("Failed base64 decoding key '%s'", noun) +- return resp ++class JoyentMetadataClient(object): ++ """ ++ A client implementing v2 of the Joyent Metadata Protocol Specification. + +- return resp ++ The full specification can be found at ++ http://eng.joyent.com/mdata/protocol.html ++ """ ++ line_regex = re.compile( ++ r'V2 (?P\d+) (?P[0-9a-f]+)' ++ r' (?P(?P[0-9a-f]+) (?PSUCCESS|NOTFOUND)' ++ r'( (?P.+))?)') ++ ++ def __init__(self, metasource): ++ self.metasource = metasource ++ ++ def _checksum(self, body): ++ return '{0:08x}'.format( ++ binascii.crc32(body.encode('utf-8')) & 0xffffffff) ++ ++ def _get_value_from_frame(self, expected_request_id, frame): ++ frame_data = self.line_regex.match(frame).groupdict() ++ if int(frame_data['length']) != len(frame_data['body']): ++ raise JoyentMetadataFetchException( ++ 'Incorrect frame length given ({0} != {1}).'.format( ++ frame_data['length'], len(frame_data['body']))) ++ expected_checksum = self._checksum(frame_data['body']) ++ if frame_data['checksum'] != expected_checksum: ++ raise JoyentMetadataFetchException( ++ 'Invalid checksum (expected: {0}; got {1}).'.format( ++ expected_checksum, frame_data['checksum'])) ++ if frame_data['request_id'] != expected_request_id: ++ raise JoyentMetadataFetchException( ++ 'Request ID mismatch (expected: {0}; got {1}).'.format( ++ expected_request_id, frame_data['request_id'])) ++ if not frame_data.get('payload', None): ++ LOG.debug('No value found.') ++ return None ++ value = base64.b64decode(frame_data['payload']) ++ LOG.debug('Value "%s" found.', value) ++ return value ++ ++ def get_metadata(self, metadata_key): ++ LOG.debug('Fetching metadata key "%s"...', metadata_key) ++ request_id = '{0:08x}'.format(random.randint(0, 0xffffffff)) ++ message_body = '{0} GET {1}'.format(request_id, ++ base64.b64encode(metadata_key)) ++ msg = 'V2 {0} {1} {2}\n'.format( ++ len(message_body), self._checksum(message_body), message_body) ++ LOG.debug('Writing "%s" to metadata transport.', msg) ++ self.metasource.write(msg.encode('ascii')) ++ self.metasource.flush() ++ ++ response = self.metasource.read(1) ++ while response[-1] != '\n': ++ response = ''.join([response, self.metasource.read(1)]) ++ response = str(response).rstrip() ++ ++ if 'SUCCESS' not in response: ++ return None ++ ++ response = response.decode('ascii') ++ LOG.debug('Read "%s" from metadata transport.', response) ++ return self._get_value_from_frame(request_id, response) + + + def dmi_data(): +- sys_uuid, sys_type = None, None +- dmidecode_path = util.which('dmidecode') +- if not dmidecode_path: +- return False +- +- sys_uuid_cmd = [dmidecode_path, "-s", "system-uuid"] +- try: +- LOG.debug("Getting hostname from dmidecode") +- (sys_uuid, _err) = util.subp(sys_uuid_cmd) +- except Exception as e: +- util.logexc(LOG, "Failed to get system UUID", e) +- +- sys_type_cmd = [dmidecode_path, "-s", "system-product-name"] +- try: +- LOG.debug("Determining hypervisor product name via dmidecode") +- (sys_type, _err) = util.subp(sys_type_cmd) +- except Exception as e: +- util.logexc(LOG, "Failed to get system UUID", e) ++ sys_type = util.read_dmi_data("system-product-name") ++ ++ if not sys_type: ++ return None + +- return (sys_uuid.lower().strip(), sys_type.strip()) ++ return sys_type + + + def write_boot_content(content, content_f, link=None, shebang=False, +- mode=0400): ++ mode=0o400): + """ + Write the content to content_f. Under the following rules: + 1. If no content, remove the file +@@ -417,7 +512,7 @@ def write_boot_content(content, content_ + + except Exception as e: + util.logexc(LOG, ("Failed to identify script type for %s" % +- content_f, e)) ++ content_f, e)) + + if link: + try: +Index: b/doc/examples/cloud-config-datasources.txt +=================================================================== +--- a/doc/examples/cloud-config-datasources.txt ++++ b/doc/examples/cloud-config-datasources.txt +@@ -51,12 +51,19 @@ datasource: + policy: on # [can be 'on', 'off' or 'force'] + + SmartOS: ++ # For KVM guests: + # Smart OS datasource works over a serial console interacting with + # a server on the other end. By default, the second serial console is the + # device. SmartOS also uses a serial timeout of 60 seconds. + serial_device: /dev/ttyS1 + serial_timeout: 60 + ++ # For LX-Brand Zones guests: ++ # Smart OS datasource works over a socket interacting with ++ # the host on the other end. By default, the socket file is in ++ # the native .zoncontrol directory. ++ metadata_sockfile: /native/.zonecontrol/metadata.sock ++ + # a list of keys that will not be base64 decoded even if base64_all + no_base64_decode: ['root_authorized_keys', 'motd_sys_info', + 'iptables_disable'] +Index: b/tests/unittests/test_datasource/test_smartos.py +=================================================================== +--- a/tests/unittests/test_datasource/test_smartos.py ++++ b/tests/unittests/test_datasource/test_smartos.py +@@ -32,6 +32,12 @@ import re + import stat + import uuid + ++ ++try: ++ from unittest import mock ++except ImportError: ++ import mock ++ + MOCK_RETURNS = { + 'hostname': 'test-host', + 'root_authorized_keys': 'ssh-rsa AAAAB3Nz...aC1yc2E= keyname', +@@ -41,17 +47,18 @@ MOCK_RETURNS = { + 'cloud-init:user-data': '\n'.join(['#!/bin/sh', '/bin/true', '']), + 'sdc:datacenter_name': 'somewhere2', + 'sdc:operator-script': '\n'.join(['bin/true', '']), ++ 'sdc:uuid': str(uuid.uuid4()), + 'sdc:vendor-data': '\n'.join(['VENDOR_DATA', '']), + 'user-data': '\n'.join(['something', '']), + 'user-script': '\n'.join(['/bin/true', '']), + } + +-DMI_DATA_RETURN = (str(uuid.uuid4()), 'smartdc') ++DMI_DATA_RETURN = 'smartdc' + + +-class MockSerial(object): +- """Fake a serial terminal for testing the code that +- interfaces with the serial""" ++class MockMetaFile(object): ++ """Fake a metadata file object for testing the code that ++ reads/writes the metadata source""" + + port = None + +@@ -75,6 +82,9 @@ class MockSerial(object): + def write(self, line): + line = line.replace('GET ', '') + self.last = line.rstrip() ++ self.new = True ++ self.count = 0 ++ self.mocked_out = [] + + def readline(self): + if self.new: +@@ -140,7 +150,8 @@ class TestSmartOSDataSource(helpers.File + ret = apply_patches(patches) + self.unapply += ret + +- def _get_ds(self, sys_cfg=None, ds_cfg=None, mockdata=None, dmi_data=None): ++ def _get_ds(self, sys_cfg=None, ds_cfg=None, mockdata=None, dmi_data=None, ++ is_lxbrand=False): + mod = DataSourceSmartOS + + if mockdata is None: +@@ -149,16 +160,17 @@ class TestSmartOSDataSource(helpers.File + if dmi_data is None: + dmi_data = DMI_DATA_RETURN + +- def _get_serial(*_): +- return MockSerial(mockdata) +- + def _dmi_data(): + return dmi_data + + def _os_uname(): +- # LP: #1243287. tests assume this runs, but running test on +- # arm would cause them all to fail. +- return ('LINUX', 'NODENAME', 'RELEASE', 'VERSION', 'x86_64') ++ if not is_lxbrand: ++ # LP: #1243287. tests assume this runs, but running test on ++ # arm would cause them all to fail. ++ return ('LINUX', 'NODENAME', 'RELEASE', 'VERSION', 'x86_64') ++ else: ++ return ('LINUX', 'NODENAME', 'RELEASE', 'BRANDZ VIRTUAL LINUX', ++ 'X86_64') + + if sys_cfg is None: + sys_cfg = {} +@@ -168,11 +180,17 @@ class TestSmartOSDataSource(helpers.File + sys_cfg['datasource']['SmartOS'] = ds_cfg + + self.apply_patches([(mod, 'LEGACY_USER_D', self.legacy_user_d)]) +- self.apply_patches([(mod, 'get_serial', _get_serial)]) + self.apply_patches([(mod, 'dmi_data', _dmi_data)]) + self.apply_patches([(os, 'uname', _os_uname)]) ++ self.apply_patches([(mod, 'device_exists', lambda d: True)]) + dsrc = mod.DataSourceSmartOS(sys_cfg, distro=None, + paths=self.paths) ++ ++ def _get_seed_file_object(): ++ return MockMetaFile(mockdata) ++ ++ self.apply_patches([(dsrc, '_get_seed_file_object', ++ _get_seed_file_object)]) + return dsrc + + def test_seed(self): +@@ -180,14 +198,29 @@ class TestSmartOSDataSource(helpers.File + dsrc = self._get_ds() + ret = dsrc.get_data() + self.assertTrue(ret) ++ self.assertEquals('kvm', dsrc.smartos_type) + self.assertEquals('/dev/ttyS1', dsrc.seed) + ++ def test_seed_lxbrand(self): ++ # default seed should be /dev/ttyS1 ++ dsrc = self._get_ds(is_lxbrand=True) ++ ret = dsrc.get_data() ++ self.assertTrue(ret) ++ self.assertEquals('lx-brand', dsrc.smartos_type) ++ self.assertEquals('/native/.zonecontrol/metadata.sock', dsrc.seed) ++ + def test_issmartdc(self): + dsrc = self._get_ds() + ret = dsrc.get_data() + self.assertTrue(ret) + self.assertTrue(dsrc.is_smartdc) + ++ def test_issmartdc_lxbrand(self): ++ dsrc = self._get_ds(is_lxbrand=True) ++ ret = dsrc.get_data() ++ self.assertTrue(ret) ++ self.assertTrue(dsrc.is_smartdc) ++ + def test_no_base64(self): + ds_cfg = {'no_base64_decode': ['test_var1'], 'all_base': True} + dsrc = self._get_ds(ds_cfg=ds_cfg) +@@ -198,7 +231,8 @@ class TestSmartOSDataSource(helpers.File + dsrc = self._get_ds(mockdata=MOCK_RETURNS) + ret = dsrc.get_data() + self.assertTrue(ret) +- self.assertEquals(DMI_DATA_RETURN[0], dsrc.metadata['instance-id']) ++ self.assertEquals(MOCK_RETURNS['sdc:uuid'], ++ dsrc.metadata['instance-id']) + + def test_root_keys(self): + dsrc = self._get_ds(mockdata=MOCK_RETURNS) diff -Nru cloud-init-0.7.5/debian/patches/series cloud-init-0.7.5/debian/patches/series --- cloud-init-0.7.5/debian/patches/series 2015-11-23 16:49:20.000000000 +0000 +++ cloud-init-0.7.5/debian/patches/series 2016-02-05 20:36:36.000000000 +0000 @@ -19,3 +19,4 @@ lp-1177432-same-archives-as-ubuntu-server.patch lp-1506244-azure-ssh-key-values.patch lp-1506187-azure_use_unique_vm_id.patch +lp-1540965-SmartOS-Add-support-for-Joyent-LX-Brand-Zones.patch