Break out signalling heat-config-notify command

Currently signalling heat is done in a chunk of python at the end
of 55-heat-config. This change moves the signalling to its own
command heat-config-notify, which can be called from 55-heat-config
or anything else which wants to signal heat.

This change also adds proper test coverage to the signalling logic.

Change-Id: I6ba7468b46ca8cfad1e58207cd3f814a178ff6f1
This commit is contained in:
Steve Baker 2014-12-11 16:28:11 +13:00
parent 2b33ca539f
commit 25c4759fe0
5 changed files with 332 additions and 61 deletions

View File

@ -0,0 +1,98 @@
#!/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.
import json
import logging
import os
import sys
import requests
try:
from heatclient import client as heatclient
except ImportError:
heatclient = None
try:
from keystoneclient.v3 import client as ksclient
except ImportError:
ksclient = None
def init_logging():
log = logging.getLogger('heat-config-notify')
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(
logging.Formatter(
'[%(asctime)s] (%(name)s) [%(levelname)s] %(message)s'))
log.addHandler(handler)
log.setLevel('DEBUG')
def main(argv=sys.argv, stdin=sys.stdin):
log = init_logging()
usage = ('Usage:\n heat-config-notify /path/to/config.json '
'< /path/to/signal_data.json')
if len(argv) < 2:
log.error(usage)
return 1
try:
signal_data = json.load(stdin)
except ValueError:
log.warn('No valid json found on stdin')
signal_data = {}
conf_file = argv[1]
if not os.path.exists(conf_file):
log.error('No config file %s' % conf_file)
log.error(usage)
return 1
c = json.load(open(conf_file))
iv = dict((i['name'], i['value']) for i in c['inputs'])
if 'deploy_signal_id' in iv:
sigurl = iv.get('deploy_signal_id')
signal_data = json.dumps(signal_data)
log.debug('Signalling to %s' % sigurl)
r = requests.post(sigurl, data=signal_data,
headers={'content-type': None})
log.debug('Response %s ' % r)
if 'deploy_auth_url' in iv:
ks = ksclient.Client(
auth_url=iv['deploy_auth_url'],
user_id=iv['deploy_user_id'],
password=iv['deploy_password'],
project_id=iv['deploy_project_id'])
endpoint = ks.service_catalog.url_for(
service_type='orchestration', endpoint_type='publicURL')
log.debug('Signalling to %s' % endpoint)
heat = heatclient.Client(
'1', endpoint, token=ks.auth_token)
r = heat.resources.signal(
iv.get('deploy_stack_id'),
iv.get('deploy_resource_name'),
data=signal_data)
log.debug('Response %s ' % r)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv, sys.stdin))

View File

@ -26,6 +26,8 @@ CONF_FILE = os.environ.get('HEAT_SHELL_CONFIG',
'/var/run/heat-config/heat-config') '/var/run/heat-config/heat-config')
DEPLOYED_DIR = os.environ.get('HEAT_CONFIG_DEPLOYED', DEPLOYED_DIR = os.environ.get('HEAT_CONFIG_DEPLOYED',
'/var/run/heat-config/deployed') '/var/run/heat-config/deployed')
HEAT_CONFIG_NOTIFY = os.environ.get('HEAT_CONFIG_NOTIFY',
'heat-config-notify')
def main(argv=sys.argv): def main(argv=sys.argv):
@ -100,11 +102,11 @@ def invoke_hook(c, log):
x for x in c['group'] if x == '-' or x == '_' or x.isalnum()) x for x in c['group'] if x == '-' or x == '_' or x.isalnum())
hook_path = os.path.join(HOOKS_DIR, hook) hook_path = os.path.join(HOOKS_DIR, hook)
signal_data = None signal_data = {}
if not os.path.exists(hook_path): if not os.path.exists(hook_path):
log.warn('Skipping group %s with no hook script %s' % ( log.warn('Skipping group %s with no hook script %s' % (
c['group'], hook_path)) c['group'], hook_path))
else: return
# write out config, which indicates it is deployed regardless of # write out config, which indicates it is deployed regardless of
# subsequent hook success # subsequent hook success
@ -138,32 +140,28 @@ def invoke_hook(c, log):
'deploy_status_code': subproc.returncode, 'deploy_status_code': subproc.returncode,
} }
if signal_data: signal_data_path = os.path.join(DEPLOYED_DIR, '%s.notify.json' % c['id'])
if 'deploy_signal_id' in iv: # write out notify data for debugging
sigurl = iv.get('deploy_signal_id') with os.fdopen(os.open(
signal_data = json.dumps(signal_data) signal_data_path, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f:
log.debug('Signalling to %s with %s' % (sigurl, signal_data)) json.dump(signal_data, f, indent=2)
r = requests.post(sigurl, data=signal_data,
headers={'content-type': None}) log.debug('Running %s %s < %s' % (
log.debug('Response %s ' % r) HEAT_CONFIG_NOTIFY, deployed_path, signal_data_path))
if 'deploy_auth_url' in iv: subproc = subprocess.Popen([HEAT_CONFIG_NOTIFY, deployed_path],
from heatclient import client as heatclient stdin=subprocess.PIPE,
from keystoneclient.v3 import client as ksclient stdout=subprocess.PIPE,
ks = ksclient.Client( stderr=subprocess.PIPE)
auth_url=iv['deploy_auth_url'], stdout, stderr = subproc.communicate(input=json.dumps(signal_data))
user_id=iv['deploy_user_id'],
password=iv['deploy_password'], log.info(stdout)
project_id=iv['deploy_project_id'])
endpoint = ks.service_catalog.url_for( if subproc.returncode:
service_type='orchestration', endpoint_type='publicURL') log.error(
log.debug('Signalling to %s' % endpoint) "Error running heat-config-notify. [%s]\n" % subproc.returncode)
heat = heatclient.Client( log.error(stderr)
'1', endpoint, token=ks.auth_token) else:
r = heat.resources.signal( log.debug(stderr)
iv.get('deploy_stack_id'),
iv.get('deploy_resource_name'),
data=signal_data)
log.debug('Response %s ' % r)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -0,0 +1 @@
../../hot/software-config/elements/heat-config/bin/heat-config-notify

View File

@ -17,7 +17,6 @@ import os
import tempfile import tempfile
import fixtures import fixtures
import requests_mock
from testtools import matchers from testtools import matchers
from tests.software_config import common from tests.software_config import common
@ -154,10 +153,7 @@ class HeatConfigTest(common.RunScriptTest):
hook_path = self.hooks_dir.join(hook) hook_path = self.hooks_dir.join(hook)
self.assertThat(hook_path, matchers.FileExists()) self.assertThat(hook_path, matchers.FileExists())
@requests_mock.Mocker(kw='mock_request') def test_run_heat_config(self):
def test_run_heat_config(self, mock_request):
mock_request.register_uri('POST', 'mock://192.0.2.2/foo')
mock_request.register_uri('POST', 'mock://192.0.2.3/foo')
self.run_heat_config(self.data) self.run_heat_config(self.data)

View File

@ -0,0 +1,178 @@
#
# 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 cStringIO
import json
import tempfile
import fixtures
import mock
from tests.software_config import common
from tests.software_config import heat_config_notify as hcn
class HeatConfigNotifyTest(common.RunScriptTest):
data_signal_id = {
'id': '5555',
'group': 'script',
'inputs': [{
'name': 'deploy_signal_id',
'value': 'mock://192.0.2.3/foo'
}],
'config': 'five'
}
data_heat_signal = {
'id': '5555',
'group': 'script',
'inputs': [{
'name': 'deploy_auth_url',
'value': 'mock://192.0.2.3/auth'
}, {
'name': 'deploy_user_id',
'value': 'aaaa'
}, {
'name': 'deploy_password',
'value': 'password'
}, {
'name': 'deploy_project_id',
'value': 'bbbb'
}, {
'name': 'deploy_stack_id',
'value': 'cccc'
}, {
'name': 'deploy_resource_name',
'value': 'the_resource'
}],
'config': 'five'
}
def setUp(self):
super(HeatConfigNotifyTest, self).setUp()
self.deployed_dir = self.useFixture(fixtures.TempDir())
hcn.init_logging = mock.MagicMock()
def write_config_file(self, data):
config_file = tempfile.NamedTemporaryFile()
config_file.write(json.dumps(data))
config_file.flush()
return config_file
def test_notify_missing_file(self):
signal_data = json.dumps({'foo': 'bar'})
stdin = cStringIO.StringIO(signal_data)
with self.write_config_file(self.data_signal_id) as config_file:
config_file_name = config_file.name
self.assertEqual(
1, hcn.main(['heat-config-notify', config_file_name], stdin))
def test_notify_missing_file_arg(self):
signal_data = json.dumps({'foo': 'bar'})
stdin = cStringIO.StringIO(signal_data)
self.assertEqual(
1, hcn.main(['heat-config-notify'], stdin))
def test_notify_signal_id(self):
requests = mock.MagicMock()
hcn.requests = requests
requests.post.return_value = '[200]'
signal_data = json.dumps({'foo': 'bar'})
stdin = cStringIO.StringIO(signal_data)
with self.write_config_file(self.data_signal_id) as config_file:
self.assertEqual(
0, hcn.main(['heat-config-notify', config_file.name], stdin))
requests.post.assert_called_once_with(
'mock://192.0.2.3/foo',
data=signal_data,
headers={'content-type': None})
def test_notify_signal_id_empty_data(self):
requests = mock.MagicMock()
hcn.requests = requests
requests.post.return_value = '[200]'
stdin = cStringIO.StringIO()
with self.write_config_file(self.data_signal_id) as config_file:
self.assertEqual(
0, hcn.main(['heat-config-notify', config_file.name], stdin))
requests.post.assert_called_once_with(
'mock://192.0.2.3/foo',
data='{}',
headers={'content-type': None})
def test_notify_signal_id_invalid_json_data(self):
requests = mock.MagicMock()
hcn.requests = requests
requests.post.return_value = '[200]'
stdin = cStringIO.StringIO('{{{"hi')
with self.write_config_file(self.data_signal_id) as config_file:
self.assertEqual(
0, hcn.main(['heat-config-notify', config_file.name], stdin))
requests.post.assert_called_once_with(
'mock://192.0.2.3/foo',
data='{}',
headers={'content-type': None})
def test_notify_heat_signal(self):
ksclient = mock.MagicMock()
hcn.ksclient = ksclient
ks = mock.MagicMock()
ksclient.Client.return_value = ks
heatclient = mock.MagicMock()
hcn.heatclient = heatclient
heat = mock.MagicMock()
heatclient.Client.return_value = heat
signal_data = json.dumps({'foo': 'bar'})
stdin = cStringIO.StringIO(signal_data)
ks.service_catalog.url_for.return_value = 'mock://192.0.2.3/heat'
heat.resources.signal.return_value = 'all good'
with self.write_config_file(self.data_heat_signal) as config_file:
self.assertEqual(
0, hcn.main(['heat-config-notify', config_file.name], stdin))
ksclient.Client.assert_called_once_with(
auth_url='mock://192.0.2.3/auth',
user_id='aaaa',
password='password',
project_id='bbbb')
ks.service_catalog.url_for.assert_called_once_with(
service_type='orchestration', endpoint_type='publicURL')
heatclient.Client.assert_called_once_with(
'1', 'mock://192.0.2.3/heat', token=ks.auth_token)
heat.resources.signal.assert_called_once_with(
'cccc',
'the_resource',
data={'foo': 'bar'})