From 507aeed12312af7fc8a9fcfca6b845183a5a3c51 Mon Sep 17 00:00:00 2001 From: Ben Howard Date: Thu, 13 Feb 2014 16:42:02 -0700 Subject: Define default vendordata for SmartOS. In absence of a vendordata, use a default #cloud-config that writes per-boot script that fetches subsequent sdc:operator-scripts and executes it. --- cloudinit/sources/DataSourceSmartOS.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'cloudinit') diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py index 140c7814..8db652ef 100644 --- a/cloudinit/sources/DataSourceSmartOS.py +++ b/cloudinit/sources/DataSourceSmartOS.py @@ -95,6 +95,34 @@ BUILTIN_CLOUD_CONFIG = { 'device': 'ephemeral0'}], } +BUILTIN_VENDOR_DATA = """ +#cloud-config: +write_files: + - encoding: b64 + owner: root:root + path: %(script_d)s/01_sdc-operator-script.sh + permissions: '0755' + content: | + """ + base64.b64encode("""#!/bin/sh +# This file is written as part of the default vendor data for +# SmartOS. This script looks for the SmartDC operator script +# and then executes it. It will be run each boot. +# +# This requires the Joyent Metadata client to be installed. +# On Ubuntu, it is provided via the joyent-mdata-client package +# Or you can get it via https://github.com/joyent/mdata-client + +my_path=$(dirname $0) +[ -x /usr/sbin/mdata-get ] || exit 1 + +/usr/sbin/mdata-get sdc:operator-script > \ + $my_path/operator-script || exit 0 + +[ -e $my_path/operator-script ] || exit 0 +chmod 0700 $my_path/operator-script +exec /run/sdc/operator-script +""") + # @datadictionary: this is legacy path for placing files from metadata # per the SmartOS location. It is not preferable, but is done for # legacy reasons @@ -186,6 +214,11 @@ class DataSourceSmartOS(sources.DataSource): if md['user-data']: ud = md['user-data'] + if not md['vendordata']: + md['vendordata'] = BUILTIN_VENDOR_DATA % { + 'script_d': self.user_script_d + } + self.metadata = util.mergemanydict([md, self.metadata]) self.userdata_raw = ud self.vendordata_raw = md['vendordata'] -- cgit v1.2.3 From 1bf99b6fe9d11a9e3b1d452940d21779347ea461 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Thu, 13 Feb 2014 23:57:33 -0500 Subject: re-work vendor-data and smartos This reduces how much cloud-init is explicitly involved in what "vendor-data" could accomplish. The goal of vendor-data was to provide the vendor with a channel to run arbitrary code that accomodate for their specific platform. Much of those accomodations are currently being done in cloud-init. However, this now moves some of those things to default "vendor-data", instead of cloud-init proper. Basically, now we have an 'sdc:vendor-data' key in the metadata. If that does not exist, then cloud-init will use the default. The default, provides a boothook. That boothook writes a file into /var/lib/cloud/per-boot/ . That file will be both written on every boot and then executed at rc.local time frame (by 'scripts-per-boot'). It will then execute /var/lib/cloud/instance/data/user-script and /var/lib/cloud/instance/data/operator-script if they exist. So, the things that cloud-init is now doing outside of the default vendor-data that I would rather be done in vendor-data is: * managing the population of instance/data/user-script and instance/data/operator-script. These could very easily be done from the boothook, but doing them in cloud-init removes the necessity for having a 'mdata-get' command in the image (or some other way for the boothook script to query the datasource). * managing the LEGACY things. --- cloudinit/sources/DataSourceSmartOS.py | 97 +++++++++++++++---------- tests/unittests/test_datasource/test_smartos.py | 51 +++---------- 2 files changed, 70 insertions(+), 78 deletions(-) (limited to 'cloudinit') diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py index 8db652ef..ec561b0d 100644 --- a/cloudinit/sources/DataSourceSmartOS.py +++ b/cloudinit/sources/DataSourceSmartOS.py @@ -50,7 +50,8 @@ SMARTOS_ATTRIB_MAP = { 'iptables_disable': ('iptables_disable', True), 'motd_sys_info': ('motd_sys_info', True), 'availability_zone': ('sdc:datacenter_name', True), - 'vendordata': ('sdc:operator-script', False), + 'vendor-data': ('sdc:vendor-data', False), + 'operator-script': ('sdc:operator-script', False), } DS_NAME = 'SmartOS' @@ -95,33 +96,45 @@ BUILTIN_CLOUD_CONFIG = { 'device': 'ephemeral0'}], } -BUILTIN_VENDOR_DATA = """ -#cloud-config: -write_files: - - encoding: b64 - owner: root:root - path: %(script_d)s/01_sdc-operator-script.sh - permissions: '0755' - content: | - """ + base64.b64encode("""#!/bin/sh -# This file is written as part of the default vendor data for -# SmartOS. This script looks for the SmartDC operator script -# and then executes it. It will be run each boot. +## 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 +fname="%(per_boot_d)s/01_smartos_vendor_data.sh" +mkdir -p "${fname%%/*}" +cat > "$fname" <<"END_SCRIPT" +#!/bin/sh +## +# This file is written as part of the default vendor data for SmartOS. +# The SmartOS datasource writes the listed file from the listed metadata key +# sdc:operator-script -> %(operator_script)s +# user-script -> %(user_script)s # -# This requires the Joyent Metadata client to be installed. -# On Ubuntu, it is provided via the joyent-mdata-client package -# Or you can get it via https://github.com/joyent/mdata-client - -my_path=$(dirname $0) -[ -x /usr/sbin/mdata-get ] || exit 1 - -/usr/sbin/mdata-get sdc:operator-script > \ - $my_path/operator-script || exit 0 +# You can view content with 'mdata-get ' +# +for script in "%(operator_script)s" "%(user_script)s"; do + [ -x "$script" ] || continue + echo "executing '$script'" 1>&2 + "$script" +done +END_SCRIPT +chmod +x "$fname" +""" -[ -e $my_path/operator-script ] || exit 0 -chmod 0700 $my_path/operator-script -exec /run/sdc/operator-script -""") # @datadictionary: this is legacy path for placing files from metadata # per the SmartOS location. It is not preferable, but is done for @@ -148,8 +161,7 @@ class DataSourceSmartOS(sources.DataSource): self.b64_keys = self.ds_cfg.get('base64_keys') self.b64_all = self.ds_cfg.get('base64_all') self.script_base_d = os.path.join(self.paths.get_cpath("scripts")) - self.user_script_d = os.path.join(self.paths.get_cpath("scripts"), - 'per-boot') + self.data_d = os.path.join(self.paths.instance_link, 'data') def __str__(self): root = sources.DataSource.__str__(self) @@ -168,7 +180,7 @@ class DataSourceSmartOS(sources.DataSource): LOG.debug("No dmidata utility found") return False - system_uuid, system_type = dmi_info + 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 @@ -191,11 +203,18 @@ class DataSourceSmartOS(sources.DataSource): # to a file in the filesystem of the guest on each boot and then # executed. It may be of any format that would be considered # executable in the guest instance. - u_script = md.get('user-script') - u_script_f = "%s/99_user_script" % self.user_script_d + # + # We write 'user-script' and 'operator-script' into the + # instance/data directory. The default vendor-data then handles + # executing them later. + user_script = os.path.join(self.data_d, 'user-script') u_script_l = "%s/user-script" % LEGACY_USER_D - write_boot_content(u_script, u_script_f, link=u_script_l, shebang=True, - mode=0700) + write_boot_content(md.get('user-script'), content_f=user_script, + link=u_script_l, shebang=True, mode=0700) + + operator_script = os.path.join(self.data_d, 'operator-script') + write_boot_content(md.get('operator-script'), + content_f=operator_script, shebang=False, mode=0700) # @datadictionary: This key has no defined format, but its value # is written to the file /var/db/mdata-user-data on each boot prior @@ -214,14 +233,16 @@ class DataSourceSmartOS(sources.DataSource): if md['user-data']: ud = md['user-data'] - if not md['vendordata']: - md['vendordata'] = BUILTIN_VENDOR_DATA % { - 'script_d': self.user_script_d - } + if not md['vendor-data']: + md['vendor-data'] = BUILTIN_VENDOR_DATA % { + 'user_script': user_script, + 'operator_script': operator_script, + 'per_boot_d': os.path.join(self.paths.get_cpath("scripts"), 'per-boot'), + } self.metadata = util.mergemanydict([md, self.metadata]) self.userdata_raw = ud - self.vendordata_raw = md['vendordata'] + self.vendordata_raw = md['vendor-data'] return True def device_name_to_device(self, name): diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py index 062b44ee..19282bac 100644 --- a/tests/unittests/test_datasource/test_smartos.py +++ b/tests/unittests/test_datasource/test_smartos.py @@ -44,6 +44,7 @@ MOCK_RETURNS = { 'cloud-init:user-data': '\n'.join(['#!/bin/sh', '/bin/true', '']), 'sdc:datacenter_name': 'somewhere2', 'sdc:operator-script': '\n'.join(['bin/true', '']), + 'sdc:vendor-data': '\n'.join(['VENDOR_DATA', '']), 'user-data': '\n'.join(['something', '']), 'user-script': '\n'.join(['/bin/true', '']), } @@ -107,7 +108,6 @@ class MockSerial(object): yield '\n' -#class TestSmartOSDataSource(MockerTestCase): class TestSmartOSDataSource(helpers.FilesystemMockingTestCase): def setUp(self): helpers.FilesystemMockingTestCase.setUp(self) @@ -345,10 +345,10 @@ class TestSmartOSDataSource(helpers.FilesystemMockingTestCase): there is no script remaining. """ - script_d = os.path.join(self.tmp, "scripts", "per-boot") + script_d = os.path.join(self.tmp, "instance", "data") os.makedirs(script_d) - test_script_f = "%s/99_user_script" % script_d + test_script_f = os.path.join(script_d, 'user-script') with open(test_script_f, 'w') as f: f.write("TEST DATA") @@ -397,49 +397,20 @@ class TestSmartOSDataSource(helpers.FilesystemMockingTestCase): dsrc = self._get_ds(mockdata=MOCK_RETURNS) ret = dsrc.get_data() self.assertTrue(ret) - self.assertEquals(MOCK_RETURNS['sdc:operator-script'], - dsrc.metadata['vendordata']) + self.assertEquals(MOCK_RETURNS['sdc:vendor-data'], + dsrc.metadata['vendor-data']) def test_default_vendor_data(self): my_returns = MOCK_RETURNS.copy() - def_op_script = my_returns['sdc:operator-script'] - del my_returns['sdc:operator-script'] + def_op_script = my_returns['sdc:vendor-data'] + del my_returns['sdc:vendor-data'] dsrc = self._get_ds(mockdata=my_returns) ret = dsrc.get_data() self.assertTrue(ret) - self.assertNotEquals(def_op_script, dsrc.metadata['vendordata']) - - self.replicateTestRoot('simple_ubuntu', self.tmp) - cfg = { - 'cloud_init_modules': ['write-files'], - } - cloud_cfg = util.yaml_dumps(cfg) - util.ensure_dir(os.path.join(self.tmp, 'etc', 'cloud')) - util.write_file(os.path.join(self.tmp, 'etc', - 'cloud', 'cloud.cfg'), cloud_cfg) - - self._patchIn(self.tmp) - - initer = stages.Init() - initer.read_cfg() - initer.datasource = dsrc - initer.initialize() - initer.fetch() - _iid = initer.instancify() - initer.update() - initer.cloudify().run('consume_data', - initer.consume_data, - args=[PER_INSTANCE], - freq=PER_INSTANCE) - mods = stages.Modules(initer) - (_which_ran, _failures) = mods.run_section('cloud_init_modules') - pb_script_fns = os.path.join(dsrc.paths.get_cpath('scripts'), - 'per-boot', '01_sdc-operator-script.sh') - self.assertTrue(os.path.isfile(pb_script_fns)) - self.assertTrue(os.access(pb_script_fns, os.X_OK)) - - with open(pb_script_fns, 'r') as fd: - self.assertIn("#!/bin/sh", fd.readlines()[0]) + self.assertNotEquals(def_op_script, dsrc.metadata['vendor-data']) + + # we expect default vendor-data is a boothook + self.assertTrue(dsrc.vendordata_raw.startswith("#cloud-boothook")) def test_disable_iptables_flag(self): dsrc = self._get_ds(mockdata=MOCK_RETURNS) -- cgit v1.2.3