diff --git a/os_collect_config/collect.py b/os_collect_config/collect.py index 5a6df48..8da3809 100644 --- a/os_collect_config/collect.py +++ b/os_collect_config/collect.py @@ -35,8 +35,11 @@ from os_collect_config import local from os_collect_config.openstack.common import log from os_collect_config import request from os_collect_config import version +from os_collect_config import zaqar + +DEFAULT_COLLECTORS = ['heat_local', 'ec2', 'cfn', 'heat', 'request', 'local', + 'zaqar'] -DEFAULT_COLLECTORS = ['heat_local', 'ec2', 'cfn', 'heat', 'request', 'local'] opts = [ cfg.StrOpt('command', short='c', help='Command to run on metadata changes. If specified,' @@ -92,7 +95,8 @@ COLLECTORS = {ec2.name: ec2, heat.name: heat, heat_local.name: heat_local, local.name: local, - request.name: request} + request.name: request, + zaqar.name: zaqar} def setup_conf(): @@ -111,6 +115,9 @@ def setup_conf(): heat_group = cfg.OptGroup(name='heat', title='Heat Metadata options') + zaqar_group = cfg.OptGroup(name='zaqar', + title='Zaqar queue options') + request_group = cfg.OptGroup(name='request', title='Request Metadata options') @@ -124,6 +131,7 @@ def setup_conf(): CONF.register_group(heat_group) CONF.register_group(request_group) CONF.register_group(keystone_group) + CONF.register_group(zaqar_group) CONF.register_cli_opts(ec2.opts, group='ec2') CONF.register_cli_opts(cfn.opts, group='cfn') CONF.register_cli_opts(heat_local.opts, group='heat_local') @@ -131,6 +139,7 @@ def setup_conf(): CONF.register_cli_opts(heat.opts, group='heat') CONF.register_cli_opts(request.opts, group='request') CONF.register_cli_opts(keystone.opts, group='keystone') + CONF.register_cli_opts(zaqar.opts, group='zaqar') CONF.register_cli_opts(opts) diff --git a/os_collect_config/exc.py b/os_collect_config/exc.py index 78c9f19..73a9687 100644 --- a/os_collect_config/exc.py +++ b/os_collect_config/exc.py @@ -58,5 +58,13 @@ class RequestMetadataNotConfigured(SourceNotAvailable): """The request metadata is not fully configured.""" +class ZaqarMetadataNotConfigured(SourceNotConfigured): + """The zaqar metadata service is not fully configured.""" + + +class ZaqarMetadataNotAvailable(SourceNotAvailable): + """The Zaqar metadata is not available.""" + + class InvalidArguments(ValueError): """Invalid arguments.""" diff --git a/os_collect_config/tests/test_collect.py b/os_collect_config/tests/test_collect.py index 6653cab..64c0ad3 100644 --- a/os_collect_config/tests/test_collect.py +++ b/os_collect_config/tests/test_collect.py @@ -37,6 +37,7 @@ from os_collect_config.tests import test_heat from os_collect_config.tests import test_heat_local from os_collect_config.tests import test_local from os_collect_config.tests import test_request +from os_collect_config.tests import test_zaqar def _setup_heat_local_metadata(test_case): @@ -76,6 +77,10 @@ class TestCollect(testtools.TestCase): 'heatclient': test_heat.FakeHeatClient(self) }, 'request': {'requests_impl': test_request.FakeRequests}, + 'zaqar': { + 'keystoneclient': test_zaqar.FakeKeystoneClient(self), + 'zaqarclient': test_zaqar.FakeZaqarClient(self) + }, } return collect.__main__(args=fake_args, collector_kwargs_map=collector_kwargs_map) @@ -352,6 +357,11 @@ class TestCollectAll(testtools.TestCase): cfg.CONF.heat.resource_name = 'server' cfg.CONF.local.path = [_setup_local_metadata(self)] cfg.CONF.request.metadata_url = 'http://127.0.0.1:8000/my_metadata/' + cfg.CONF.zaqar.auth_url = 'http://127.0.0.1:5000/v3' + cfg.CONF.zaqar.user_id = '0123456789ABCDEF' + cfg.CONF.zaqar.password = 'FEDCBA9876543210' + cfg.CONF.zaqar.project_id = '9f6b09df-4d7f-4a33-8ec3-9924d8f46f10' + cfg.CONF.zaqar.queue_id = '4f3f46d3-09f1-42a7-8c13-f91a5457192c' @mock.patch.object(ks_discover.Discover, '__init__') @mock.patch.object(ks_discover.Discover, 'url_for') @@ -368,6 +378,10 @@ class TestCollectAll(testtools.TestCase): 'heatclient': test_heat.FakeHeatClient(self) }, 'request': {'requests_impl': test_request.FakeRequests}, + 'zaqar': { + 'keystoneclient': test_zaqar.FakeKeystoneClient(self), + 'zaqarclient': test_zaqar.FakeZaqarClient(self) + }, } if collectors is None: collectors = cfg.CONF.collectors @@ -382,7 +396,7 @@ class TestCollectAll(testtools.TestCase): store=True, collector_kwargs_map=collector_kwargs_map) if expected_changed is None: expected_changed = set(['heat_local', 'cfn', 'ec2', - 'heat', 'local', 'request']) + 'heat', 'local', 'request', 'zaqar']) self.assertEqual(expected_changed, changed_keys) self.assertThat(paths, matchers.IsInstance(list)) for path in paths: @@ -402,10 +416,14 @@ class TestCollectAll(testtools.TestCase): 'heatclient': test_heat.FakeHeatClient(self) }, 'request': {'requests_impl': test_request.FakeRequests}, + 'zaqar': { + 'keystoneclient': test_zaqar.FakeKeystoneClient(self), + 'zaqarclient': test_zaqar.FakeZaqarClient(self) + }, } expected_changed = set(( 'heat_local', 'ec2', 'cfn', 'heat', 'local', 'request', - 'dep-name1', 'dep-name2', 'dep-name3')) + 'dep-name1', 'dep-name2', 'dep-name3', 'zaqar')) self._test_collect_all_store(collector_kwargs_map=soft_config_map, expected_changed=expected_changed) @@ -441,6 +459,10 @@ class TestCollectAll(testtools.TestCase): 'heatclient': test_heat.FakeHeatClient(self) }, 'request': {'requests_impl': test_request.FakeRequests}, + 'zaqar': { + 'keystoneclient': test_zaqar.FakeKeystoneClient(self), + 'zaqarclient': test_zaqar.FakeZaqarClient(self) + }, } (changed_keys, paths) = self._call_collect_all( store=True, collector_kwargs_map=soft_config_map) diff --git a/os_collect_config/tests/test_zaqar.py b/os_collect_config/tests/test_zaqar.py new file mode 100644 index 0000000..be3acd0 --- /dev/null +++ b/os_collect_config/tests/test_zaqar.py @@ -0,0 +1,133 @@ +# +# 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 fixtures +from keystoneclient import discover as ks_discover +import mock +from oslo_config import cfg +import testtools +from testtools import matchers +from zaqarclient.queues.v1 import message + +from os_collect_config import collect +from os_collect_config import exc +from os_collect_config.tests import test_heat +from os_collect_config import zaqar + + +class FakeKeystoneClient(test_heat.FakeKeystoneClient): + + def url_for(self, service_type, endpoint_type): + self._test.assertEqual('messaging', service_type) + self._test.assertEqual('publicURL', endpoint_type) + return 'http://127.0.0.1:8888/' + + +class FakeZaqarClient(object): + + def __init__(self, testcase): + self._test = testcase + + def Client(self, endpoint, conf, version): + self._test.assertEqual(1.1, version) + self._test.assertEqual('http://127.0.0.1:8888/', endpoint) + return self + + def queue(self, queue_id): + self._test.assertEqual( + '4f3f46d3-09f1-42a7-8c13-f91a5457192c', queue_id) + return FakeQueue() + + +class FakeQueue(object): + + def pop(self): + return iter([message.Message( + queue=self, ttl=10, age=10, body=test_heat.META_DATA, href='')]) + + +class TestZaqar(testtools.TestCase): + def setUp(self): + super(TestZaqar, self).setUp() + self.log = self.useFixture(fixtures.FakeLogger()) + self.useFixture(fixtures.NestedTempfile()) + collect.setup_conf() + cfg.CONF.zaqar.auth_url = 'http://127.0.0.1:5000/v3' + cfg.CONF.zaqar.user_id = '0123456789ABCDEF' + cfg.CONF.zaqar.password = 'FEDCBA9876543210' + cfg.CONF.zaqar.project_id = '9f6b09df-4d7f-4a33-8ec3-9924d8f46f10' + cfg.CONF.zaqar.queue_id = '4f3f46d3-09f1-42a7-8c13-f91a5457192c' + + @mock.patch.object(ks_discover.Discover, '__init__') + @mock.patch.object(ks_discover.Discover, 'url_for') + def test_collect_zaqar(self, mock_url_for, mock___init__): + mock___init__.return_value = None + mock_url_for.return_value = cfg.CONF.zaqar.auth_url + zaqar_md = zaqar.Collector( + keystoneclient=FakeKeystoneClient(self, cfg.CONF.zaqar), + zaqarclient=FakeZaqarClient(self)).collect() + self.assertThat(zaqar_md, matchers.IsInstance(list)) + self.assertEqual('zaqar', zaqar_md[0][0]) + zaqar_md = zaqar_md[0][1] + + for k in ('int1', 'strfoo', 'map_ab'): + self.assertIn(k, zaqar_md) + self.assertEqual(zaqar_md[k], test_heat.META_DATA[k]) + + @mock.patch.object(ks_discover.Discover, '__init__') + @mock.patch.object(ks_discover.Discover, 'url_for') + def test_collect_zaqar_fail(self, mock_url_for, mock___init__): + mock___init__.return_value = None + mock_url_for.return_value = cfg.CONF.zaqar.auth_url + zaqar_collect = zaqar.Collector( + keystoneclient=test_heat.FakeFailKeystoneClient( + self, cfg.CONF.zaqar), + zaqarclient=FakeZaqarClient(self)) + self.assertRaises(exc.ZaqarMetadataNotAvailable, zaqar_collect.collect) + self.assertIn('Forbidden', self.log.output) + + def test_collect_zaqar_no_auth_url(self): + cfg.CONF.zaqar.auth_url = None + zaqar_collect = zaqar.Collector() + self.assertRaises( + exc.ZaqarMetadataNotConfigured, zaqar_collect.collect) + self.assertIn('No auth_url configured', self.log.output) + + def test_collect_zaqar_no_password(self): + cfg.CONF.zaqar.password = None + zaqar_collect = zaqar.Collector() + self.assertRaises( + exc.ZaqarMetadataNotConfigured, zaqar_collect.collect) + self.assertIn('No password configured', self.log.output) + + def test_collect_zaqar_no_project_id(self): + cfg.CONF.zaqar.project_id = None + zaqar_collect = zaqar.Collector() + self.assertRaises( + exc.ZaqarMetadataNotConfigured, zaqar_collect.collect) + self.assertIn('No project_id configured', self.log.output) + + def test_collect_zaqar_no_user_id(self): + cfg.CONF.zaqar.user_id = None + zaqar_collect = zaqar.Collector() + self.assertRaises( + exc.ZaqarMetadataNotConfigured, zaqar_collect.collect) + self.assertIn('No user_id configured', self.log.output) + + def test_collect_zaqar_no_queue_id(self): + cfg.CONF.zaqar.queue_id = None + zaqar_collect = zaqar.Collector() + self.assertRaises( + exc.ZaqarMetadataNotConfigured, zaqar_collect.collect) + self.assertIn('No queue_id configured', self.log.output) diff --git a/os_collect_config/zaqar.py b/os_collect_config/zaqar.py new file mode 100644 index 0000000..0e70fe2 --- /dev/null +++ b/os_collect_config/zaqar.py @@ -0,0 +1,94 @@ +# +# 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. + +from keystoneclient.v3 import client as keystoneclient +from oslo_config import cfg +import six +from zaqarclient.queues.v1 import client as zaqarclient + +from os_collect_config import exc +from os_collect_config import keystone +from os_collect_config.openstack.common import log + +CONF = cfg.CONF +logger = log.getLogger(__name__) + +opts = [ + cfg.StrOpt('user-id', + help='User ID for API authentication'), + cfg.StrOpt('password', + help='Password for API authentication'), + cfg.StrOpt('project-id', + help='ID of project for API authentication'), + cfg.StrOpt('auth-url', + help='URL for API authentication'), + cfg.StrOpt('queue-id', + help='ID of the queue to be checked'), +] +name = 'zaqar' + + +class Collector(object): + def __init__(self, + keystoneclient=keystoneclient, + zaqarclient=zaqarclient): + self.keystoneclient = keystoneclient + self.zaqarclient = zaqarclient + + def collect(self): + if CONF.zaqar.auth_url is None: + logger.warn('No auth_url configured.') + raise exc.ZaqarMetadataNotConfigured() + if CONF.zaqar.password is None: + logger.warn('No password configured.') + raise exc.ZaqarMetadataNotConfigured() + if CONF.zaqar.project_id is None: + logger.warn('No project_id configured.') + raise exc.ZaqarMetadataNotConfigured() + if CONF.zaqar.user_id is None: + logger.warn('No user_id configured.') + raise exc.ZaqarMetadataNotConfigured() + if CONF.zaqar.queue_id is None: + logger.warn('No queue_id configured.') + raise exc.ZaqarMetadataNotConfigured() + + try: + ks = keystone.Keystone( + auth_url=CONF.zaqar.auth_url, + user_id=CONF.zaqar.user_id, + password=CONF.zaqar.password, + project_id=CONF.zaqar.project_id, + keystoneclient=self.keystoneclient).client + endpoint = ks.service_catalog.url_for( + service_type='messaging', endpoint_type='publicURL') + logger.debug('Fetching metadata from %s' % endpoint) + conf = { + 'auth_opts': { + 'backend': 'keystone', + 'options': { + 'os_auth_token': ks.auth_token, + 'os_project_id': CONF.zaqar.project_id + } + } + } + + zaqar = self.zaqarclient.Client(endpoint, conf=conf, version=1.1) + + queue = zaqar.queue(CONF.zaqar.queue_id) + r = six.next(queue.pop()) + + return [('zaqar', r.body)] + except Exception as e: + logger.warn(str(e)) + raise exc.ZaqarMetadataNotAvailable() diff --git a/requirements.txt b/requirements.txt index f24a0c5..535f942 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ argparse eventlet>=0.17.3 python-keystoneclient>=1.6.0 python-heatclient>=0.3.0 +python-zaqarclient>=0.1.1 requests>=2.5.2 iso8601>=0.1.9 lxml>=2.3