From 831ab0be03f2b5635534321611cd0fb1b5a44127 Mon Sep 17 00:00:00 2001 From: Clint Byrum Date: Fri, 21 Mar 2014 13:46:13 -0700 Subject: [PATCH] Parse deployments if found in cfn metadata Will result in multiple cache files to be merged per OS::Heat::StructuredDeployment. This is needed as the new features for software configuration break things up a bit differently in Metadata. Change-Id: Iec0fd947bac674f6b6f36e8c0789d10580c325fd Closes-Bug: #1295787 --- os_collect_config/cfn.py | 29 +++++- os_collect_config/tests/test_cfn.py | 137 ++++++++++++++++++++++------ 2 files changed, 138 insertions(+), 28 deletions(-) diff --git a/os_collect_config/cfn.py b/os_collect_config/cfn.py index 657fd7a..cfb3abb 100644 --- a/os_collect_config/cfn.py +++ b/os_collect_config/cfn.py @@ -43,6 +43,12 @@ opts = [ help='Secret Access Key'), cfg.StrOpt('access-key-id', help='Access Key ID'), + cfg.MultiStrOpt('deployment-key', + default=['deployments'], + help='Key(s) to explode into multiple collected outputs. ' + 'Parsed according to the expected Metadata created by ' + 'OS::Heat::StructuredDeployment. Only Exploded if seen at ' + 'the root of the Metadata.') ] name = 'cfn' @@ -126,4 +132,25 @@ class Collector(object): 'Sub-key %s does not exist. (%s)' % (subkey, path)) raise exc.CfnMetadataNotAvailable final_content.update(value) - return [('cfn', final_content)] + final_list = [] + for depkey in cfg.CONF.cfn.deployment_key: + if depkey in final_content: + deployments = final_content[depkey] + if not isinstance(deployments, list): + logger.warn( + 'Deployment-key %s was found but does not contain a ' + 'list.' % (depkey,)) + continue + logger.debug( + 'Deployment found for %s' % (depkey,)) + for deployment in deployments: + if 'name' not in deployment: + logger.warn( + 'No name found for a deployment under %s.' % + (depkey,)) + continue + final_list.append((deployment['name'], + deployment['config'])) + del final_content[depkey] + final_list.insert(0, ('cfn', final_content)) + return final_list diff --git a/os_collect_config/tests/test_cfn.py b/os_collect_config/tests/test_cfn.py index 6260d55..a9b5e5c 100644 --- a/os_collect_config/tests/test_cfn.py +++ b/os_collect_config/tests/test_cfn.py @@ -38,6 +38,37 @@ META_DATA = {u'int1': 1, }} +SOFTWARE_CONFIG_DATA = { + u'old-style': u'value', + u'deployments': [ + { + u'inputs': [ + { + u'type': u'String', + u'name': u'input1', + u'value': u'value1' + } + ], + u'group': 'Heat::Ungrouped', + u'name': 'dep-name1', + u'outputs': None, + u'options': None, + u'config': { + u'config1': 'value1' + } + } + ] +} + + +SOFTWARE_CONFIG_IMPOSTER_DATA = { + u'old-style': u'value', + u'deployments': { + u"not": u"a list" + } +} + + class FakeResponse(dict): def __init__(self, text): self.text = text @@ -46,6 +77,37 @@ class FakeResponse(dict): pass +class FakeReqSession(object): + + SESSION_META_DATA = META_DATA + + def __init__(self, testcase, expected_netloc): + self._test = testcase + self._expected_netloc = expected_netloc + + def get(self, url, params, headers): + self._test.addDetail('url', test_content.text_content(url)) + url = urlparse.urlparse(url) + self._test.assertEqual(self._expected_netloc, url.netloc) + self._test.assertEqual('/v1/', url.path) + self._test.assertEqual('application/json', + headers['Content-Type']) + self._test.assertIn('SignatureVersion', params) + self._test.assertEqual('2', params['SignatureVersion']) + self._test.assertIn('Signature', params) + self._test.assertIn('Action', params) + self._test.assertEqual('DescribeStackResource', + params['Action']) + self._test.assertIn('LogicalResourceId', params) + self._test.assertEqual('foo', params['LogicalResourceId']) + root = etree.Element('DescribeStackResourceResponse') + result = etree.SubElement(root, 'DescribeStackResourceResult') + detail = etree.SubElement(result, 'StackResourceDetail') + metadata = etree.SubElement(detail, 'Metadata') + metadata.text = json.dumps(self.SESSION_META_DATA) + return FakeResponse(etree.tostring(root)) + + class FakeRequests(object): exceptions = requests.exceptions @@ -54,35 +116,33 @@ class FakeRequests(object): self._expected_netloc = expected_netloc def Session(self): - class FakeReqSession(object): - def __init__(self, testcase, expected_netloc): - self._test = testcase - self._expected_netloc = expected_netloc - def get(self, url, params, headers): - self._test.addDetail('url', test_content.text_content(url)) - url = urlparse.urlparse(url) - self._test.assertEqual(self._expected_netloc, url.netloc) - self._test.assertEqual('/v1/', url.path) - self._test.assertEqual('application/json', - headers['Content-Type']) - self._test.assertIn('SignatureVersion', params) - self._test.assertEqual('2', params['SignatureVersion']) - self._test.assertIn('Signature', params) - self._test.assertIn('Action', params) - self._test.assertEqual('DescribeStackResource', - params['Action']) - self._test.assertIn('LogicalResourceId', params) - self._test.assertEqual('foo', params['LogicalResourceId']) - root = etree.Element('DescribeStackResourceResponse') - result = etree.SubElement(root, 'DescribeStackResourceResult') - detail = etree.SubElement(result, 'StackResourceDetail') - metadata = etree.SubElement(detail, 'Metadata') - metadata.text = json.dumps(META_DATA) - return FakeResponse(etree.tostring(root)) return FakeReqSession(self._test, self._expected_netloc) +class FakeReqSessionSoftwareConfig(FakeReqSession): + + SESSION_META_DATA = SOFTWARE_CONFIG_DATA + + +class FakeRequestsSoftwareConfig(FakeRequests): + + FAKE_SESSION = FakeReqSessionSoftwareConfig + + def Session(self): + return self.FAKE_SESSION(self._test, self._expected_netloc) + + +class FakeReqSessionConfigImposter(FakeReqSession): + + SESSION_META_DATA = SOFTWARE_CONFIG_IMPOSTER_DATA + + +class FakeRequestsConfigImposter(FakeRequestsSoftwareConfig): + + FAKE_SESSION = FakeReqSessionConfigImposter + + class FakeFailRequests(object): exceptions = requests.exceptions @@ -91,9 +151,9 @@ class FakeFailRequests(object): raise requests.exceptions.HTTPError(403, 'Forbidden') -class TestCfn(testtools.TestCase): +class TestCfnBase(testtools.TestCase): def setUp(self): - super(TestCfn, self).setUp() + super(TestCfnBase, self).setUp() self.log = self.useFixture(fixtures.FakeLogger()) self.useFixture(fixtures.NestedTempfile()) self.hint_file = tempfile.NamedTemporaryFile() @@ -107,6 +167,8 @@ class TestCfn(testtools.TestCase): cfg.CONF.cfn.access_key_id = '0123456789ABCDEF' cfg.CONF.cfn.secret_access_key = 'FEDCBA9876543210' + +class TestCfn(TestCfnBase): def test_collect_cfn(self): cfn_md = cfn.Collector(requests_impl=FakeRequests(self)).collect() self.assertThat(cfn_md, matchers.IsInstance(list)) @@ -164,3 +226,24 @@ class TestCfn(testtools.TestCase): requests_impl=FakeRequests(self, expected_netloc='127.0.1.1:8000')) cfn_collect.collect() + + +class TestCfnSoftwareConfig(TestCfnBase): + def test_collect_cfn_software_config(self): + cfn_md = cfn.Collector( + requests_impl=FakeRequestsSoftwareConfig(self)).collect() + self.assertThat(cfn_md, matchers.IsInstance(list)) + self.assertEqual('cfn', cfn_md[0][0]) + cfn_config = cfn_md[0][1] + self.assertEqual({'old-style': 'value'}, cfn_config) + self.assertEqual('dep-name1', cfn_md[1][0]) + config = cfn_md[1][1] + self.assertEqual('value1', config['config1']) + + def test_collect_cfn_deployments_not_list(self): + cfn_md = cfn.Collector( + requests_impl=FakeRequestsConfigImposter(self)).collect() + self.assertEqual(1, len(cfn_md)) + self.assertEqual('cfn', cfn_md[0][0]) + self.assertIn('not', cfn_md[0][1]['deployments']) + self.assertEqual('a list', cfn_md[0][1]['deployments']['not'])