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:
Steve Baker
2014-10-13 12:19:06 +13:00
parent 2030bb9957
commit d86eec6b73
3 changed files with 215 additions and 7 deletions
@@ -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())
+49
View File
@@ -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))
+112
View File
@@ -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)