diff options
-rw-r--r-- | cloudinit/sources/DataSourceSmartOS.py | 60 | ||||
-rw-r--r-- | doc/sources/smartos/README.rst | 66 | ||||
-rw-r--r-- | tests/unittests/test_datasource/test_smartos.py | 67 |
3 files changed, 176 insertions, 17 deletions
diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py index 1ce20c10..e0bb871c 100644 --- a/cloudinit/sources/DataSourceSmartOS.py +++ b/cloudinit/sources/DataSourceSmartOS.py @@ -27,6 +27,7 @@ # +import base64 from cloudinit import log as logging from cloudinit import sources from cloudinit import util @@ -49,6 +50,10 @@ SMARTOS_ATTRIB_MAP = { 'motd_sys_info': ('motd_sys_info', True), } +# These are values which will never be base64 encoded. +SMARTOS_NO_BASE64 = ['root_authorized_keys', 'motd_sys_info', + 'iptables_disable'] + class DataSourceSmartOS(sources.DataSource): def __init__(self, sys_cfg, distro, paths): @@ -56,6 +61,10 @@ class DataSourceSmartOS(sources.DataSource): self.seed_dir = os.path.join(paths.seed_dir, 'sdc') self.is_smartdc = None self.seed = self.sys_cfg.get("serial_device", DEF_TTY_LOC) + self.all_base64 = self.sys_cfg.get("decode_base64", False) + self.base_64_encoded = [] + self.smartos_no_base64 = self.sys_cfg.get("no_base64_decode", + SMARTOS_NO_BASE64) self.seed_timeout = self.sys_cfg.get("serial_timeout", DEF_TTY_TIMEOUT) @@ -84,17 +93,41 @@ class DataSourceSmartOS(sources.DataSource): self.is_smartdc = True md['instance-id'] = system_uuid + self.base_64_encoded = query_data('base_64_enocded', + self.seed, + self.seed_timeout, + strip=True) + if self.base_64_encoded: + self.base_64_encoded = str(self.base_64_encoded).split(',') + else: + self.base_64_encoded = [] + + if not self.all_base64: + self.all_base64 = util.is_true(query_data('meta_encoded_base64', + self.seed, + self.seed_timeout, + strip=True)) + for ci_noun, attribute in SMARTOS_ATTRIB_MAP.iteritems(): smartos_noun, strip = attribute + + b64encoded = False + if self.all_base64 and \ + (smartos_noun not in self.smartos_no_base64 and \ + ci_noun not in self.smartos_no_base64): + b64encoded = True + md[ci_noun] = query_data(smartos_noun, self.seed, - self.seed_timeout, strip=strip) + self.seed_timeout, strip=strip, + b64encoded=b64encoded) if not md['local-hostname']: md['local-hostname'] = system_uuid + ud = None if md['user-data']: ud = md['user-data'] - else: + elif md['user-script']: ud = md['user-script'] self.metadata = md @@ -124,12 +157,17 @@ def get_serial(seed_device, seed_timeout): return ser -def query_data(noun, seed_device, seed_timeout, strip=False): +def query_data(noun, seed_device, seed_timeout, strip=False, b64encoded=False): """Makes a request to via the serial console 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. """ if not noun: @@ -153,12 +191,22 @@ def query_data(noun, seed_device, seed_timeout, strip=False): response.append(m) ser.close() + + resp = None if not strip: - return "".join(response) + resp = "".join(response) + elif b64encoded: + resp = "".join(response).rstrip() else: - return "".join(response).rstrip() + resp = "".join(response).rstrip() + + if b64encoded: + try: + return base64.b64decode(resp) + except TypeError: + return resp - return None + return resp def dmi_data(): diff --git a/doc/sources/smartos/README.rst b/doc/sources/smartos/README.rst new file mode 100644 index 00000000..96310857 --- /dev/null +++ b/doc/sources/smartos/README.rst @@ -0,0 +1,66 @@ +================== +SmartOS Datasource +================== + +This datasource finds metadata and user-data from the SmartOS virtualization +platform (i.e. Joyent). + +SmartOS Platform +---------------- +The SmartOS virtualization platform meta-data to the instance via the second +serial console. On Linux, this is /dev/ttyS1. The data is a provided via a +simple protocol, where something queries for the userdata, where the console +responds with the status and if "SUCCESS" returns until a single ".\n". + +The format is lossy. As such, new versions of the SmartOS tooling will include +support for base64 encoded data. + +Userdata +-------- + +In SmartOS parlance, user-data is a actually meta-data. This userdata can be +provided a key-value pairs. + +Cloud-init supports reading the traditional meta-data fields supported by the +SmartOS tools. These are: + * root_authorized_keys + * hostname + * enable_motd_sys_info + * iptables_disable + +Note: At this time iptables_disable and enable_motd_sys_info are read but + are not actioned. + +user-script +----------- + +SmartOS traditionally supports sending over a user-script for execution at the +rc.local level. Cloud-init supports running user-scripts as if they were +cloud-init user-data. In this sense, anything with a shell interpetter +directive will run + +user-data and user-script +------------------------- + +In the event that a user defines the meta-data key of "user-data" it will +always supercede any user-script data. This is for consistency. + +base64 +------ + +In order to provide a lossy format, all data except for: + * root_authorized_keys + * enable_motd_sys_info + * iptables_disable + +This means that user-script and user-data as well as other values can be +base64 encoded to provide a lossy format. Since Cloud-init can only guess +as to whether or not something is truly base64 encoded, the following +meta-data keys are hints as to whether or not to base64 decode something: + * decode_base64: Except for excluded keys, attempt to base64 decode + the values. If the value fails to decode properly, it will be + returned in its text + * base_64_encoded: A comma deliminated list of which values are base64 + encoded. + * no_base64_decode: This is a configuration setting (i.e. /etc/cloud/cloud.cfg.d) + that sets which values should not be base64 decoded. diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py index 6c12f1e2..ae621433 100644 --- a/tests/unittests/test_datasource/test_smartos.py +++ b/tests/unittests/test_datasource/test_smartos.py @@ -22,6 +22,7 @@ # return responses. # +import base64 from cloudinit import helpers from cloudinit.sources import DataSourceSmartOS @@ -35,7 +36,7 @@ mock_returns = { 'enable_motd_sys_info': None, 'system_uuid': str(uuid.uuid4()), 'smartdc': 'smartdc', - 'userdata': """ + 'user-data': """ #!/bin/sh /bin/true """, @@ -48,12 +49,14 @@ class MockSerial(object): port = None - def __init__(self): + def __init__(self, b64encode=False): self.last = None self.last = None self.new = True self.count = 0 self.mocked_out = [] + self.b64encode = b64encode + self.b64excluded = DataSourceSmartOS.SMARTOS_NO_BASE64 def open(self): return True @@ -87,11 +90,17 @@ class MockSerial(object): def _format_out(self): if self.last in mock_returns: - try: - for l in mock_returns[self.last].splitlines(): - yield "%s\n" % l - except: - yield "%s\n" % mock_returns[self.last] + _mret = mock_returns[self.last] + if self.b64encode and \ + self.last not in self.b64excluded: + yield base64.b64encode(_mret) + + else: + try: + for l in _mret.splitlines(): + yield "%s\n" % l.rstrip() + except: + yield "%s\n" % _mret.rstrip() yield '\n' yield '.' @@ -116,16 +125,19 @@ class TestSmartOSDataSource(MockerTestCase): ret = apply_patches(patches) self.unapply += ret - def _get_ds(self): + def _get_ds(self, b64encode=False, sys_cfg=None): + mod = DataSourceSmartOS def _get_serial(*_): - return MockSerial() + return MockSerial(b64encode=b64encode) def _dmi_data(): return mock_returns['system_uuid'], 'smartdc' - data = {'sys_cfg': {}} - mod = DataSourceSmartOS + if not sys_cfg: + sys_cfg = {} + + data = {'sys_cfg': sys_cfg} self.apply_patches([(mod, 'get_serial', _get_serial)]) self.apply_patches([(mod, 'dmi_data', _dmi_data)]) dsrc = mod.DataSourceSmartOS( @@ -158,6 +170,13 @@ class TestSmartOSDataSource(MockerTestCase): self.assertEquals(mock_returns['root_authorized_keys'], dsrc.metadata['public-keys']) + def test_hostname_b64(self): + dsrc = self._get_ds(b64encode=True) + ret = dsrc.get_data() + self.assertTrue(ret) + self.assertEquals(base64.b64encode(mock_returns['hostname']), + dsrc.metadata['local-hostname']) + def test_hostname(self): dsrc = self._get_ds() ret = dsrc.get_data() @@ -165,6 +184,32 @@ class TestSmartOSDataSource(MockerTestCase): self.assertEquals(mock_returns['hostname'], dsrc.metadata['local-hostname']) + def test_base64(self): + """This tests to make sure that SmartOS system key/value pairs + are not interpetted as being base64 encoded, while making + sure that the others are when 'decode_base64' is set""" + dsrc = self._get_ds(sys_cfg={'decode_base64': True}, + b64encode=True) + ret = dsrc.get_data() + self.assertTrue(ret) + self.assertEquals(mock_returns['hostname'], + dsrc.metadata['local-hostname']) + self.assertEquals("%s" % mock_returns['user-data'], + dsrc.userdata_raw) + self.assertEquals(mock_returns['root_authorized_keys'], + dsrc.metadata['public-keys']) + self.assertEquals(mock_returns['disable_iptables_flag'], + dsrc.metadata['iptables_disable']) + self.assertEquals(mock_returns['enable_motd_sys_info'], + dsrc.metadata['motd_sys_info']) + + def test_userdata(self): + dsrc = self._get_ds() + ret = dsrc.get_data() + self.assertTrue(ret) + self.assertEquals("%s\n" % mock_returns['user-data'], + dsrc.userdata_raw) + def test_disable_iptables_flag(self): dsrc = self._get_ds() ret = dsrc.get_data() |