Trigger cfn-init via script rather than lib
The current cfn-init software-config hook doesn't work at all since cfn_helper is installed in a venv, not in site-packages. This change switches to writing out metadata to /var/cache/heat-cfntools/last_metadata and invoking cfn-init. /var/lib/heat-cfntools/cfn-init-data cannot be used since that is already populated with all boot metadata, which may still be used by os-collect-config bootstrapping. Change-Id: I7252a6f12223613b55b4b6417383673faa0d52b3 Closes-Bug: #1321513
This commit is contained in:
@@ -13,22 +13,69 @@
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from heat_cfntools.cfntools import cfn_helper
|
||||
|
||||
# Ideally this path would be /var/lib/heat-cfntools/cfn-init-data
|
||||
# but this is where all boot metadata is stored
|
||||
LAST_METADATA_DIR = os.environ.get('HEAT_CFN_INIT_LAST_METADATA_DIR',
|
||||
'/var/cache/heat-cfntools')
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
c = json.load(sys.stdin)
|
||||
CFN_INIT_CMD = os.environ.get('HEAT_CFN_INIT_CMD',
|
||||
'/opt/aws/bin/cfn-init')
|
||||
|
||||
|
||||
def main(argv=sys.argv, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr):
|
||||
log = logging.getLogger('heat-config')
|
||||
handler = logging.StreamHandler(stderr)
|
||||
handler.setFormatter(
|
||||
logging.Formatter(
|
||||
'[%(asctime)s] (%(name)s) [%(levelname)s] %(message)s'))
|
||||
log.addHandler(handler)
|
||||
log.setLevel('DEBUG')
|
||||
|
||||
c = json.load(stdin)
|
||||
|
||||
config = c.get('config', {})
|
||||
if not isinstance(config, dict):
|
||||
config = json.loads(config)
|
||||
meta = {'AWS::CloudFormation::Init': config}
|
||||
|
||||
metadata = cfn_helper.Metadata(None, None)
|
||||
metadata.retrieve(meta_str=json.dumps(meta))
|
||||
metadata.cfn_init()
|
||||
if not os.path.isdir(LAST_METADATA_DIR):
|
||||
os.makedirs(LAST_METADATA_DIR, 0o700)
|
||||
|
||||
fn = os.path.join(LAST_METADATA_DIR, 'last_metadata')
|
||||
with os.fdopen(os.open(fn, os.O_CREAT | os.O_WRONLY, 0o700), 'w') as f:
|
||||
json.dump(meta, f)
|
||||
|
||||
log.debug('Running %s' % CFN_INIT_CMD)
|
||||
subproc = subprocess.Popen([CFN_INIT_CMD], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
cstdout, cstderr = subproc.communicate()
|
||||
|
||||
if cstdout:
|
||||
log.info(cstdout)
|
||||
if cstderr:
|
||||
log.info(cstderr)
|
||||
|
||||
if subproc.returncode:
|
||||
log.error("Error running %s. [%s]\n" % (
|
||||
CFN_INIT_CMD, subproc.returncode))
|
||||
else:
|
||||
log.info('Completed %s' % CFN_INIT_CMD)
|
||||
|
||||
response = {
|
||||
'deploy_stdout': cstdout,
|
||||
'deploy_stderr': cstderr,
|
||||
'deploy_status_code': subproc.returncode,
|
||||
}
|
||||
|
||||
json.dump(response, stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
||||
sys.exit(main())
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
'''
|
||||
A fake config tool for unit testing the software-config hooks.
|
||||
|
||||
JSON containing the current environment variables and command line arguments
|
||||
are written to the file specified by the path in environment variable
|
||||
TEST_STATE_PATH.
|
||||
|
||||
Environment variable TEST_RESPONSE defines JSON specifying what files to write
|
||||
out, and what to print to stdout and stderr.
|
||||
'''
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
with open(os.environ.get('TEST_STATE_PATH'), 'w') as f:
|
||||
json.dump({'env': dict(os.environ), 'args': argv}, f)
|
||||
|
||||
if 'TEST_RESPONSE' not in os.environ:
|
||||
return
|
||||
|
||||
response = json.loads(os.environ.get('TEST_RESPONSE'))
|
||||
for k, v in response.get('files', {}).iteritems():
|
||||
open(k, 'w')
|
||||
with open(k, 'w') as f:
|
||||
f.write(v)
|
||||
|
||||
sys.stdout.write(response.get('stdout', ''))
|
||||
sys.stderr.write(response.get('stderr', ''))
|
||||
return response.get('returncode', 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,112 @@
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import fixtures
|
||||
|
||||
from tests.software_config import common
|
||||
|
||||
|
||||
class HookCfnInitTest(common.RunScriptTest):
|
||||
|
||||
data = {
|
||||
'group': 'cfn-init',
|
||||
'inputs': [],
|
||||
'config': {'foo': 'bar'}
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(HookCfnInitTest, self).setUp()
|
||||
self.hook_path = self.relative_path(
|
||||
__file__,
|
||||
'../..',
|
||||
'hot/software-config/elements',
|
||||
'heat-config-cfn-init/install.d/hook-cfn-init.py')
|
||||
|
||||
self.fake_tool_path = self.relative_path(
|
||||
__file__,
|
||||
'config-tool-fake.py')
|
||||
|
||||
self.metadata_dir = self.useFixture(fixtures.TempDir())
|
||||
# use the temp dir to store the fake config tool state too
|
||||
self.test_state_path = self.metadata_dir.join('test_state.json')
|
||||
self.env = os.environ.copy()
|
||||
self.env.update({
|
||||
'HEAT_CFN_INIT_LAST_METADATA_DIR': self.metadata_dir.join(),
|
||||
'HEAT_CFN_INIT_CMD': self.fake_tool_path,
|
||||
'TEST_STATE_PATH': self.test_state_path,
|
||||
})
|
||||
|
||||
def test_hook(self):
|
||||
|
||||
self.env.update({
|
||||
'TEST_RESPONSE': json.dumps({
|
||||
'stdout': 'cfn-init success',
|
||||
'stderr': 'thing happened'
|
||||
}),
|
||||
})
|
||||
returncode, stdout, stderr = self.run_cmd(
|
||||
[self.hook_path], self.env, json.dumps(self.data))
|
||||
|
||||
self.assertEqual(0, returncode, stderr)
|
||||
self.assertEqual({
|
||||
'deploy_stdout': 'cfn-init success',
|
||||
'deploy_stderr': 'thing happened',
|
||||
'deploy_status_code': 0
|
||||
}, json.loads(stdout))
|
||||
|
||||
# assert last_metadata was written with cfn-init metadata
|
||||
self.assertEqual(
|
||||
{'AWS::CloudFormation::Init': {'foo': 'bar'}},
|
||||
self.json_from_file(self.metadata_dir.join('last_metadata')))
|
||||
|
||||
# assert cfn-init was called with no args
|
||||
self.assertEqual(
|
||||
[self.fake_tool_path],
|
||||
self.json_from_file(self.test_state_path)['args'])
|
||||
|
||||
def test_hook_cfn_init_failed(self):
|
||||
|
||||
self.env.update({
|
||||
'TEST_RESPONSE': json.dumps({
|
||||
'stderr': 'bad thing happened',
|
||||
'returncode': 1
|
||||
}),
|
||||
})
|
||||
returncode, stdout, stderr = self.run_cmd(
|
||||
[self.hook_path], self.env, json.dumps(self.data))
|
||||
|
||||
self.assertEqual(0, returncode, stderr)
|
||||
self.assertEqual({
|
||||
'deploy_stdout': '',
|
||||
'deploy_stderr': 'bad thing happened',
|
||||
'deploy_status_code': 1
|
||||
}, json.loads(stdout))
|
||||
|
||||
self.assertEqual(
|
||||
{'AWS::CloudFormation::Init': {'foo': 'bar'}},
|
||||
self.json_from_file(self.metadata_dir.join('last_metadata')))
|
||||
|
||||
# assert cfn-init was called with no args
|
||||
self.assertEqual(
|
||||
[self.fake_tool_path],
|
||||
self.json_from_file(self.test_state_path)['args'])
|
||||
|
||||
def test_hook_invalid_json(self):
|
||||
|
||||
returncode, stdout, stderr = self.run_cmd(
|
||||
[self.hook_path], self.env, "{::::")
|
||||
|
||||
self.assertEqual(1, returncode, stderr)
|
||||
Reference in New Issue
Block a user