summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Moser <smoser@brickies.net>2017-01-10 11:25:32 -0500
committerScott Moser <smoser@brickies.net>2017-01-10 11:25:32 -0500
commit2f37bf611eede21b38591a3ce35a73db8e13951e (patch)
treeec27e116451c88fe3f3ff00f153e164503c0e716
parentabbc05962e30bcb4473f7cda95b4733e5b5edea8 (diff)
downloadcloud-init-git-2f37bf611eede21b38591a3ce35a73db8e13951e.tar.gz
Import version 0.7.5-0ubuntu1.16ubuntu/0.7.5-0ubuntu1.16
Imported using git-import-dsc
-rw-r--r--debian/changelog8
-rw-r--r--debian/patches/lp-1540965-SmartOS-Add-support-for-Joyent-LX-Brand-Zones.patch713
-rw-r--r--debian/patches/series1
3 files changed, 722 insertions, 0 deletions
diff --git a/debian/changelog b/debian/changelog
index 6229bbdb..79301732 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -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 <robert.jennings@canonical.com> Tue, 02 Feb 2016 09:32:02 -0600
+
cloud-init (0.7.5-0ubuntu1.15) trusty; urgency=medium
* Microsoft Azure:
diff --git a/debian/patches/lp-1540965-SmartOS-Add-support-for-Joyent-LX-Brand-Zones.patch b/debian/patches/lp-1540965-SmartOS-Add-support-for-Joyent-LX-Brand-Zones.patch
new file mode 100644
index 00000000..01d3bb89
--- /dev/null
+++ b/debian/patches/lp-1540965-SmartOS-Add-support-for-Joyent-LX-Brand-Zones.patch
@@ -0,0 +1,713 @@
+From: Robert C Jennings <robert.jennings@canonical.com>
+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 <robert.jennings@canonical.com>
+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 <NOUN>"
++
++ In the response, the first line is the status, while subsequent
++ lines are is the value. A blank line with a "." is used to
++ indicate end of response.
++
++ If the response is expected to be base64 encoded, then set
++ b64encoded to true. Unfortantely, there is no way to know if
++ something is 100% encoded, so this method relies on being told
++ if the data is base64 or not.
++ """
+
+-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 <NOUN>"
++ 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<length>\d+) (?P<checksum>[0-9a-f]+)'
++ r' (?P<body>(?P<request_id>[0-9a-f]+) (?P<status>SUCCESS|NOTFOUND)'
++ r'( (?P<payload>.+))?)')
++
++ 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 --git a/debian/patches/series b/debian/patches/series
index d323c22f..0e299668 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -19,3 +19,4 @@ lp-1493453-nocloudds-vendor_data.patch
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