summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/cmd/tests/test_main.py3
-rw-r--r--cloudinit/helpers.py7
-rw-r--r--cloudinit/settings.py1
-rw-r--r--cloudinit/sources/DataSourceOpenStack.py8
-rw-r--r--cloudinit/sources/__init__.py13
-rw-r--r--cloudinit/sources/helpers/openstack.py5
-rw-r--r--cloudinit/stages.py106
-rw-r--r--doc/rtd/topics/datasources/openstack.rst8
-rw-r--r--tests/unittests/test_data.py37
-rw-r--r--tests/unittests/test_datasource/test_openstack.py32
10 files changed, 171 insertions, 49 deletions
diff --git a/cloudinit/cmd/tests/test_main.py b/cloudinit/cmd/tests/test_main.py
index 585b3b0e..78b27441 100644
--- a/cloudinit/cmd/tests/test_main.py
+++ b/cloudinit/cmd/tests/test_main.py
@@ -127,7 +127,8 @@ class TestMain(FilesystemMockingTestCase):
'syslog_fix_perms': [
'syslog:adm', 'root:adm', 'root:wheel', 'root:root'
],
- 'vendor_data': {'enabled': True, 'prefix': []}})
+ 'vendor_data': {'enabled': True, 'prefix': []},
+ 'vendor_data2': {'enabled': True, 'prefix': []}})
updated_cfg.pop('system_info')
self.assertEqual(updated_cfg, cfg)
diff --git a/cloudinit/helpers.py b/cloudinit/helpers.py
index 9752ad28..fc5011ec 100644
--- a/cloudinit/helpers.py
+++ b/cloudinit/helpers.py
@@ -230,6 +230,10 @@ class ConfigMerger(object):
cc_paths = ['cloud_config']
if self._include_vendor:
+ # the order is important here: we want vendor2
+ # (dynamic vendor data from OpenStack)
+ # to override vendor (static data from OpenStack)
+ cc_paths.append('vendor2_cloud_config')
cc_paths.append('vendor_cloud_config')
for cc_p in cc_paths:
@@ -337,9 +341,12 @@ class Paths(object):
"obj_pkl": "obj.pkl",
"cloud_config": "cloud-config.txt",
"vendor_cloud_config": "vendor-cloud-config.txt",
+ "vendor2_cloud_config": "vendor2-cloud-config.txt",
"data": "data",
"vendordata_raw": "vendor-data.txt",
+ "vendordata2_raw": "vendor-data2.txt",
"vendordata": "vendor-data.txt.i",
+ "vendordata2": "vendor-data2.txt.i",
"instance_id": ".instance-id",
"manual_clean_marker": "manual-clean",
"warnings": "warnings",
diff --git a/cloudinit/settings.py b/cloudinit/settings.py
index ca4ffa8e..7516e17b 100644
--- a/cloudinit/settings.py
+++ b/cloudinit/settings.py
@@ -56,6 +56,7 @@ CFG_BUILTIN = {
'network': {'renderers': None},
},
'vendor_data': {'enabled': True, 'prefix': []},
+ 'vendor_data2': {'enabled': True, 'prefix': []},
}
# Valid frequencies of handlers/modules
diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py
index b3406c67..619a171e 100644
--- a/cloudinit/sources/DataSourceOpenStack.py
+++ b/cloudinit/sources/DataSourceOpenStack.py
@@ -167,6 +167,14 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
LOG.warning("Invalid content in vendor-data: %s", e)
self.vendordata_raw = None
+ vd2 = results.get('vendordata2')
+ self.vendordata2_pure = vd2
+ try:
+ self.vendordata2_raw = sources.convert_vendordata(vd2)
+ except ValueError as e:
+ LOG.warning("Invalid content in vendor-data2: %s", e)
+ self.vendordata2_raw = None
+
return True
def _crawl_metadata(self):
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index 9dccc687..1ad1880d 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -187,7 +187,8 @@ class DataSource(metaclass=abc.ABCMeta):
cached_attr_defaults = (
('ec2_metadata', UNSET), ('network_json', UNSET),
('metadata', {}), ('userdata', None), ('userdata_raw', None),
- ('vendordata', None), ('vendordata_raw', None))
+ ('vendordata', None), ('vendordata_raw', None),
+ ('vendordata2', None), ('vendordata2_raw', None))
_dirty_cache = False
@@ -203,7 +204,9 @@ class DataSource(metaclass=abc.ABCMeta):
self.metadata = {}
self.userdata_raw = None
self.vendordata = None
+ self.vendordata2 = None
self.vendordata_raw = None
+ self.vendordata2_raw = None
self.ds_cfg = util.get_cfg_by_path(
self.sys_cfg, ("datasource", self.dsname), {})
@@ -392,6 +395,11 @@ class DataSource(metaclass=abc.ABCMeta):
self.vendordata = self.ud_proc.process(self.get_vendordata_raw())
return self.vendordata
+ def get_vendordata2(self):
+ if self.vendordata2 is None:
+ self.vendordata2 = self.ud_proc.process(self.get_vendordata2_raw())
+ return self.vendordata2
+
@property
def fallback_interface(self):
"""Determine the network interface used during local network config."""
@@ -494,6 +502,9 @@ class DataSource(metaclass=abc.ABCMeta):
def get_vendordata_raw(self):
return self.vendordata_raw
+ def get_vendordata2_raw(self):
+ return self.vendordata2_raw
+
# the data sources' config_obj is a cloud-config formated
# object that came to it from ways other than cloud-config
# because cloud-config content would be handled elsewhere
diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
index 3e6365f1..4f566e64 100644
--- a/cloudinit/sources/helpers/openstack.py
+++ b/cloudinit/sources/helpers/openstack.py
@@ -247,6 +247,11 @@ class BaseReader(metaclass=abc.ABCMeta):
False,
load_json_anytype,
)
+ files['vendordata2'] = (
+ self._path_join("openstack", version, 'vendor_data2.json'),
+ False,
+ load_json_anytype,
+ )
files['networkdata'] = (
self._path_join("openstack", version, 'network_data.json'),
False,
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index 0cce6e80..3ef4491c 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -360,8 +360,18 @@ class Init(object):
reporter=self.reporter)
def update(self):
- self._store_userdata()
- self._store_vendordata()
+ self._store_rawdata(self.datasource.get_userdata_raw(),
+ 'userdata')
+ self._store_processeddata(self.datasource.get_userdata(),
+ 'userdata')
+ self._store_rawdata(self.datasource.get_vendordata_raw(),
+ 'vendordata')
+ self._store_processeddata(self.datasource.get_vendordata(),
+ 'vendordata')
+ self._store_rawdata(self.datasource.get_vendordata2_raw(),
+ 'vendordata2')
+ self._store_processeddata(self.datasource.get_vendordata2(),
+ 'vendordata2')
def setup_datasource(self):
with events.ReportEventStack("setup-datasource",
@@ -381,28 +391,18 @@ class Init(object):
is_new_instance=self.is_new_instance())
self._write_to_cache()
- def _store_userdata(self):
- raw_ud = self.datasource.get_userdata_raw()
- if raw_ud is None:
- raw_ud = b''
- util.write_file(self._get_ipath('userdata_raw'), raw_ud, 0o600)
- # processed userdata is a Mime message, so write it as string.
- processed_ud = self.datasource.get_userdata()
- if processed_ud is None:
- raw_ud = ''
- util.write_file(self._get_ipath('userdata'), str(processed_ud), 0o600)
-
- def _store_vendordata(self):
- raw_vd = self.datasource.get_vendordata_raw()
- if raw_vd is None:
- raw_vd = b''
- util.write_file(self._get_ipath('vendordata_raw'), raw_vd, 0o600)
- # processed vendor data is a Mime message, so write it as string.
- processed_vd = str(self.datasource.get_vendordata())
- if processed_vd is None:
- processed_vd = ''
- util.write_file(self._get_ipath('vendordata'), str(processed_vd),
- 0o600)
+ def _store_rawdata(self, data, datasource):
+ # Raw data is bytes, not a string
+ if data is None:
+ data = b''
+ util.write_file(self._get_ipath('%s_raw' % datasource), data, 0o600)
+
+ def _store_processeddata(self, processed_data, datasource):
+ # processed is a Mime message, so write as string.
+ if processed_data is None:
+ processed_data = ''
+ util.write_file(self._get_ipath(datasource),
+ str(processed_data), 0o600)
def _default_handlers(self, opts=None):
if opts is None:
@@ -434,6 +434,11 @@ class Init(object):
opts={'script_path': 'vendor_scripts',
'cloud_config_path': 'vendor_cloud_config'})
+ def _default_vendordata2_handlers(self):
+ return self._default_handlers(
+ opts={'script_path': 'vendor_scripts',
+ 'cloud_config_path': 'vendor2_cloud_config'})
+
def _do_handlers(self, data_msg, c_handlers_list, frequency,
excluded=None):
"""
@@ -555,7 +560,12 @@ class Init(object):
with events.ReportEventStack("consume-vendor-data",
"reading and applying vendor-data",
parent=self.reporter):
- self._consume_vendordata(frequency)
+ self._consume_vendordata("vendordata", frequency)
+
+ with events.ReportEventStack("consume-vendor-data2",
+ "reading and applying vendor-data2",
+ parent=self.reporter):
+ self._consume_vendordata("vendordata2", frequency)
# Perform post-consumption adjustments so that
# modules that run during the init stage reflect
@@ -568,46 +578,62 @@ class Init(object):
# objects before the load of the userdata happened,
# this is expected.
- def _consume_vendordata(self, frequency=PER_INSTANCE):
+ def _consume_vendordata(self, vendor_source, frequency=PER_INSTANCE):
"""
Consume the vendordata and run the part handlers on it
"""
+
# User-data should have been consumed first.
# So we merge the other available cloud-configs (everything except
# vendor provided), and check whether or not we should consume
# vendor data at all. That gives user or system a chance to override.
- if not self.datasource.get_vendordata_raw():
- LOG.debug("no vendordata from datasource")
- return
+ if vendor_source == 'vendordata':
+ if not self.datasource.get_vendordata_raw():
+ LOG.debug("no vendordata from datasource")
+ return
+ cfg_name = 'vendor_data'
+ elif vendor_source == 'vendordata2':
+ if not self.datasource.get_vendordata2_raw():
+ LOG.debug("no vendordata2 from datasource")
+ return
+ cfg_name = 'vendor_data2'
+ else:
+ raise RuntimeError("vendor_source arg must be either 'vendordata'"
+ " or 'vendordata2'")
_cc_merger = helpers.ConfigMerger(paths=self._paths,
datasource=self.datasource,
additional_fns=[],
base_cfg=self.cfg,
include_vendor=False)
- vdcfg = _cc_merger.cfg.get('vendor_data', {})
+ vdcfg = _cc_merger.cfg.get(cfg_name, {})
if not isinstance(vdcfg, dict):
vdcfg = {'enabled': False}
- LOG.warning("invalid 'vendor_data' setting. resetting to: %s",
- vdcfg)
+ LOG.warning("invalid %s setting. resetting to: %s",
+ cfg_name, vdcfg)
enabled = vdcfg.get('enabled')
no_handlers = vdcfg.get('disabled_handlers', None)
if not util.is_true(enabled):
- LOG.debug("vendordata consumption is disabled.")
+ LOG.debug("%s consumption is disabled.", vendor_source)
return
- LOG.debug("vendor data will be consumed. disabled_handlers=%s",
- no_handlers)
+ LOG.debug("%s will be consumed. disabled_handlers=%s",
+ vendor_source, no_handlers)
- # Ensure vendordata source fetched before activation (just incase)
- vendor_data_msg = self.datasource.get_vendordata()
+ # Ensure vendordata source fetched before activation (just in case.)
- # This keeps track of all the active handlers, while excluding what the
- # users doesn't want run, i.e. boot_hook, cloud_config, shell_script
- c_handlers_list = self._default_vendordata_handlers()
+ # c_handlers_list keeps track of all the active handlers, while
+ # excluding what the users doesn't want run, i.e. boot_hook,
+ # cloud_config, shell_script
+ if vendor_source == 'vendordata':
+ vendor_data_msg = self.datasource.get_vendordata()
+ c_handlers_list = self._default_vendordata_handlers()
+ else:
+ vendor_data_msg = self.datasource.get_vendordata2()
+ c_handlers_list = self._default_vendordata2_handlers()
# Run the handlers
self._do_handlers(vendor_data_msg, c_handlers_list, frequency,
diff --git a/doc/rtd/topics/datasources/openstack.rst b/doc/rtd/topics/datasources/openstack.rst
index b23b4b7c..62d0fc03 100644
--- a/doc/rtd/topics/datasources/openstack.rst
+++ b/doc/rtd/topics/datasources/openstack.rst
@@ -82,4 +82,12 @@ For more general information about how cloud-init handles vendor data,
including how it can be disabled by users on instances, see
:doc:`/topics/vendordata`.
+OpenStack can also be configured to provide 'dynamic vendordata'
+which is provided by the DynamicJSON provider and appears under a
+different metadata path, /vendor_data2.json.
+
+Cloud-init will look for a ``cloud-init`` at the vendor_data2 path; if found,
+settings are applied after (and, hence, overriding) the settings from static
+vendor data. Both sets of vendor data can be overridden by user data.
+
.. vi: textwidth=78
diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py
index fb2b55e8..8c968ae9 100644
--- a/tests/unittests/test_data.py
+++ b/tests/unittests/test_data.py
@@ -33,11 +33,12 @@ INSTANCE_ID = "i-testing"
class FakeDataSource(sources.DataSource):
- def __init__(self, userdata=None, vendordata=None):
+ def __init__(self, userdata=None, vendordata=None, vendordata2=None):
sources.DataSource.__init__(self, {}, None, None)
self.metadata = {'instance-id': INSTANCE_ID}
self.userdata_raw = userdata
self.vendordata_raw = vendordata
+ self.vendordata2_raw = vendordata2
def count_messages(root):
@@ -105,13 +106,14 @@ class TestConsumeUserData(helpers.FilesystemMockingTestCase):
self.assertEqual('qux', cc['baz'])
self.assertEqual('qux2', cc['bar'])
- def test_simple_jsonp_vendor_and_user(self):
+ def test_simple_jsonp_vendor_and_vendor2_and_user(self):
# test that user-data wins over vendor
user_blob = '''
#cloud-config-jsonp
[
{ "op": "add", "path": "/baz", "value": "qux" },
- { "op": "add", "path": "/bar", "value": "qux2" }
+ { "op": "add", "path": "/bar", "value": "qux2" },
+ { "op": "add", "path": "/foobar", "value": "qux3" }
]
'''
vendor_blob = '''
@@ -119,12 +121,23 @@ class TestConsumeUserData(helpers.FilesystemMockingTestCase):
[
{ "op": "add", "path": "/baz", "value": "quxA" },
{ "op": "add", "path": "/bar", "value": "quxB" },
- { "op": "add", "path": "/foo", "value": "quxC" }
+ { "op": "add", "path": "/foo", "value": "quxC" },
+ { "op": "add", "path": "/corge", "value": "quxEE" }
+]
+'''
+ vendor2_blob = '''
+#cloud-config-jsonp
+[
+ { "op": "add", "path": "/corge", "value": "quxD" },
+ { "op": "add", "path": "/grault", "value": "quxFF" },
+ { "op": "add", "path": "/foobar", "value": "quxGG" }
]
'''
self.reRoot()
initer = stages.Init()
- initer.datasource = FakeDataSource(user_blob, vendordata=vendor_blob)
+ initer.datasource = FakeDataSource(user_blob,
+ vendordata=vendor_blob,
+ vendordata2=vendor2_blob)
initer.read_cfg()
initer.initialize()
initer.fetch()
@@ -138,9 +151,15 @@ class TestConsumeUserData(helpers.FilesystemMockingTestCase):
(_which_ran, _failures) = mods.run_section('cloud_init_modules')
cfg = mods.cfg
self.assertIn('vendor_data', cfg)
+ self.assertIn('vendor_data2', cfg)
+ # Confirm that vendordata2 overrides vendordata, and that
+ # userdata overrides both
self.assertEqual('qux', cfg['baz'])
self.assertEqual('qux2', cfg['bar'])
+ self.assertEqual('qux3', cfg['foobar'])
self.assertEqual('quxC', cfg['foo'])
+ self.assertEqual('quxD', cfg['corge'])
+ self.assertEqual('quxFF', cfg['grault'])
def test_simple_jsonp_no_vendor_consumed(self):
# make sure that vendor data is not consumed
@@ -294,6 +313,10 @@ run:
#!/bin/bash
echo "test"
'''
+ vendor2_blob = '''
+#!/bin/bash
+echo "dynamic test"
+'''
user_blob = '''
#cloud-config
@@ -303,7 +326,9 @@ vendor_data:
'''
new_root = self.reRoot()
initer = stages.Init()
- initer.datasource = FakeDataSource(user_blob, vendordata=vendor_blob)
+ initer.datasource = FakeDataSource(user_blob,
+ vendordata=vendor_blob,
+ vendordata2=vendor2_blob)
initer.read_cfg()
initer.initialize()
initer.fetch()
diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/test_datasource/test_openstack.py
index 415755aa..478f3503 100644
--- a/tests/unittests/test_datasource/test_openstack.py
+++ b/tests/unittests/test_datasource/test_openstack.py
@@ -40,6 +40,9 @@ USER_DATA = b'#!/bin/sh\necho This is user data\n'
VENDOR_DATA = {
'magic': '',
}
+VENDOR_DATA2 = {
+ 'static': {}
+}
OSTACK_META = {
'availability_zone': 'nova',
'files': [{'content_path': '/content/0000', 'path': '/etc/foo.cfg'},
@@ -60,6 +63,7 @@ OS_FILES = {
{'links': [], 'networks': [], 'services': []}),
'openstack/latest/user_data': USER_DATA,
'openstack/latest/vendor_data.json': json.dumps(VENDOR_DATA),
+ 'openstack/latest/vendor_data2.json': json.dumps(VENDOR_DATA2),
}
EC2_FILES = {
'latest/user-data': USER_DATA,
@@ -142,6 +146,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
_register_uris(self.VERSION, EC2_FILES, EC2_META, OS_FILES)
f = _read_metadata_service()
self.assertEqual(VENDOR_DATA, f.get('vendordata'))
+ self.assertEqual(VENDOR_DATA2, f.get('vendordata2'))
self.assertEqual(CONTENT_0, f['files']['/etc/foo.cfg'])
self.assertEqual(CONTENT_1, f['files']['/etc/bar/bar.cfg'])
self.assertEqual(2, len(f['files']))
@@ -163,6 +168,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
_register_uris(self.VERSION, {}, {}, OS_FILES)
f = _read_metadata_service()
self.assertEqual(VENDOR_DATA, f.get('vendordata'))
+ self.assertEqual(VENDOR_DATA2, f.get('vendordata2'))
self.assertEqual(CONTENT_0, f['files']['/etc/foo.cfg'])
self.assertEqual(CONTENT_1, f['files']['/etc/bar/bar.cfg'])
self.assertEqual(USER_DATA, f.get('userdata'))
@@ -195,6 +201,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
_register_uris(self.VERSION, {}, {}, os_files)
f = _read_metadata_service()
self.assertEqual(VENDOR_DATA, f.get('vendordata'))
+ self.assertEqual(VENDOR_DATA2, f.get('vendordata2'))
self.assertEqual(CONTENT_0, f['files']['/etc/foo.cfg'])
self.assertEqual(CONTENT_1, f['files']['/etc/bar/bar.cfg'])
self.assertFalse(f.get('userdata'))
@@ -210,6 +217,17 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
self.assertEqual(CONTENT_1, f['files']['/etc/bar/bar.cfg'])
self.assertFalse(f.get('vendordata'))
+ def test_vendordata2_empty(self):
+ os_files = copy.deepcopy(OS_FILES)
+ for k in list(os_files.keys()):
+ if k.endswith('vendor_data2.json'):
+ os_files.pop(k, None)
+ _register_uris(self.VERSION, {}, {}, os_files)
+ f = _read_metadata_service()
+ self.assertEqual(CONTENT_0, f['files']['/etc/foo.cfg'])
+ self.assertEqual(CONTENT_1, f['files']['/etc/bar/bar.cfg'])
+ self.assertFalse(f.get('vendordata2'))
+
def test_vendordata_invalid(self):
os_files = copy.deepcopy(OS_FILES)
for k in list(os_files.keys()):
@@ -218,6 +236,14 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
_register_uris(self.VERSION, {}, {}, os_files)
self.assertRaises(BrokenMetadata, _read_metadata_service)
+ def test_vendordata2_invalid(self):
+ os_files = copy.deepcopy(OS_FILES)
+ for k in list(os_files.keys()):
+ if k.endswith('vendor_data2.json'):
+ os_files[k] = '{' # some invalid json
+ _register_uris(self.VERSION, {}, {}, os_files)
+ self.assertRaises(BrokenMetadata, _read_metadata_service)
+
def test_metadata_invalid(self):
os_files = copy.deepcopy(OS_FILES)
for k in list(os_files.keys()):
@@ -246,6 +272,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
self.assertEqual(USER_DATA, ds_os.userdata_raw)
self.assertEqual(2, len(ds_os.files))
self.assertEqual(VENDOR_DATA, ds_os.vendordata_pure)
+ self.assertEqual(VENDOR_DATA2, ds_os.vendordata2_pure)
self.assertIsNone(ds_os.vendordata_raw)
m_dhcp.assert_not_called()
@@ -278,6 +305,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
self.assertEqual(USER_DATA, ds_os_local.userdata_raw)
self.assertEqual(2, len(ds_os_local.files))
self.assertEqual(VENDOR_DATA, ds_os_local.vendordata_pure)
+ self.assertEqual(VENDOR_DATA2, ds_os_local.vendordata2_pure)
self.assertIsNone(ds_os_local.vendordata_raw)
m_dhcp.assert_called_with('eth9', None)
@@ -401,7 +429,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
self.assertIsNone(ds_os.vendordata_raw)
self.assertEqual(
['dsmode', 'ec2-metadata', 'files', 'metadata', 'networkdata',
- 'userdata', 'vendordata', 'version'],
+ 'userdata', 'vendordata', 'vendordata2', 'version'],
sorted(crawled_data.keys()))
self.assertEqual('local', crawled_data['dsmode'])
self.assertEqual(EC2_META, crawled_data['ec2-metadata'])
@@ -415,6 +443,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
crawled_data['networkdata'])
self.assertEqual(USER_DATA, crawled_data['userdata'])
self.assertEqual(VENDOR_DATA, crawled_data['vendordata'])
+ self.assertEqual(VENDOR_DATA2, crawled_data['vendordata2'])
self.assertEqual(2, crawled_data['version'])
@@ -681,6 +710,7 @@ class TestMetadataReader(test_helpers.HttprettyTestCase):
'version': 2,
'metadata': expected_md,
'vendordata': vendor_data,
+ 'vendordata2': vendor_data2,
'networkdata': network_data,
'ec2-metadata': mock_read_ec2.return_value,
'files': {},