From 11dfd95c469eea456841a9f6c7e04f34d62077c2 Mon Sep 17 00:00:00 2001 From: Bartlomiej Biernacki Date: Thu, 12 Nov 2015 09:43:23 +0100 Subject: [PATCH 001/374] Fill status code for every HTTPException Change-Id: Ib73398f0a555c9ac4534fd3ce4a1c06e51bc5962 Closes-Bug: #1515519 --- heatclient/exc.py | 6 ++++-- heatclient/tests/unit/test_common_http.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/heatclient/exc.py b/heatclient/exc.py index 4f8a0623..d42ca0dd 100644 --- a/heatclient/exc.py +++ b/heatclient/exc.py @@ -44,7 +44,7 @@ class HTTPException(BaseException): """Base exception for all HTTP-derived exceptions.""" code = 'N/A' - def __init__(self, message=None): + def __init__(self, message=None, code=None): super(HTTPException, self).__init__(message) try: self.error = jsonutils.loads(message) @@ -59,6 +59,8 @@ def __init__(self, message=None): except Exception: self.error = {'error': {'message': self.message or self.__class__.__doc__}} + if self.code == "N/A" and code is not None: + self.code = code def __str__(self): message = self.error['error'].get('message', 'Internal Error') @@ -179,7 +181,7 @@ class HTTPServiceUnavailable(ServiceUnavailable): def from_response(response): """Return an instance of an HTTPException based on requests response.""" cls = _code_map.get(response.status_code, HTTPException) - return cls(response.content) + return cls(response.content, response.status_code) class NoTokenLookupException(Exception): diff --git a/heatclient/tests/unit/test_common_http.py b/heatclient/tests/unit/test_common_http.py index bb2fa20b..13c6e221 100644 --- a/heatclient/tests/unit/test_common_http.py +++ b/heatclient/tests/unit/test_common_http.py @@ -818,6 +818,22 @@ def test_300_error_response(self): # Assert that the raised exception can be converted to string self.assertIsNotNone(six.text_type(e)) + def test_504_error_response(self): + # for 504 we don't have specific exception type + fake = fakes.FakeHTTPResponse( + 504, + 'FAIL', + {'content-type': 'application/octet-stream'}, + '') + self.request.return_value = (fake, '') + + client = http.SessionClient(session=mock.ANY, + auth=mock.ANY) + e = self.assertRaises(exc.HTTPException, + client.request, '', 'GET') + + self.assertEqual(504, e.code) + def test_kwargs(self): fake = fakes.FakeHTTPResponse( 200, From 9bbdc4f7159934bb9e8a27c1d2be23d475fcb85c Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Fri, 26 Feb 2016 16:58:11 -0600 Subject: [PATCH 002/374] Basic set of stack functional tests for openstack client A few more tests that exercise the new openstack client stack interfaces. Blueprint: heat-support-python-openstackclient Change-Id: I132be3fb4068426890c7f9b4c0d8a903788ef3d6 --- heatclient/tests/functional/base.py | 3 - heatclient/tests/functional/osc/v1/base.py | 116 ++++++++++++++++++ .../tests/functional/osc/v1/test_readonly.py | 9 +- .../tests/functional/osc/v1/test_stack.py | 56 +++++++++ 4 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 heatclient/tests/functional/osc/v1/base.py create mode 100644 heatclient/tests/functional/osc/v1/test_stack.py diff --git a/heatclient/tests/functional/base.py b/heatclient/tests/functional/base.py index b0d8a8ed..44718cad 100644 --- a/heatclient/tests/functional/base.py +++ b/heatclient/tests/functional/base.py @@ -40,6 +40,3 @@ def _get_clients(self): def heat(self, *args, **kwargs): return self.clients.heat(*args, **kwargs) - - def openstack(self, *args, **kwargs): - return self.clients.openstack(*args, **kwargs) diff --git a/heatclient/tests/functional/osc/v1/base.py b/heatclient/tests/functional/osc/v1/base.py new file mode 100644 index 00000000..a2948a81 --- /dev/null +++ b/heatclient/tests/functional/osc/v1/base.py @@ -0,0 +1,116 @@ +# 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 os +import six + +from tempest_lib.cli import base +from tempest_lib.cli import output_parser + + +class OpenStackClientTestBase(base.ClientTestBase): + """Command line client base functions.""" + + def setUp(self): + super(OpenStackClientTestBase, self).setUp() + self.parser = output_parser + + def _get_clients(self): + cli_dir = os.environ.get( + 'OS_HEATCLIENT_EXEC_DIR', + os.path.join(os.path.abspath('.'), '.tox/functional/bin')) + + return base.CLIClient( + username=os.environ.get('OS_USERNAME'), + password=os.environ.get('OS_PASSWORD'), + tenant_name=os.environ.get('OS_TENANT_NAME'), + uri=os.environ.get('OS_AUTH_URL'), + cli_dir=cli_dir) + + def openstack(self, *args, **kwargs): + return self.clients.openstack(*args, **kwargs) + + def get_template_path(self, templ_name): + return os.path.join(os.path.dirname(os.path.realpath(__file__)), + '../../templates/%s' % templ_name) + + def show_to_dict(self, output): + obj = {} + items = self.parser.listing(output) + for item in items: + obj[item['Field']] = six.text_type(item['Value']) + return dict((self._key_name(k), v) for k, v in obj.iteritems()) + + def _key_name(self, key): + return key.lower().replace(' ', '_') + + def list_to_dict(self, output, id): + obj = {} + items = self.parser.listing(output) + for item in items: + if item['ID'] == id: + obj = item + break + return dict((self._key_name(k), v) for k, v in obj.iteritems()) + + def _stack_create(self, name, template, parameters=[], wait=True): + cmd = 'stack create ' + name + if template: + cmd += ' -t ' + template + if wait: + cmd += ' --wait' + + for parameter in parameters: + cmd += ' --parameter ' + parameter + stack_raw = self.openstack(cmd) + stack = self.show_to_dict(stack_raw) + self.addCleanup(self._stack_delete, stack['id']) + return stack + + def _stack_delete(self, id, wait=False): + cmd = 'stack delete ' + id + ' --yes' + if wait: + cmd += ' --wait' + if id in self.openstack('stack list --short'): + self.openstack(cmd) + + def _stack_suspend(self, id, wait=True): + cmd = 'stack suspend ' + id + if wait: + cmd += ' --wait' + stack_raw = self.openstack(cmd) + stack = self.list_to_dict(stack_raw, id) + return stack + + def _stack_resume(self, id, wait=True): + cmd = 'stack resume ' + id + if wait: + cmd += ' --wait' + stack_raw = self.openstack(cmd) + stack = self.list_to_dict(stack_raw, id) + return stack + + def _stack_snapshot_create(self, id, name): + cmd = 'stack snapshot create ' + id + ' --name ' + name + snapshot_raw = self.openstack(cmd) + snapshot = self.show_to_dict(snapshot_raw) + self.addCleanup(self._stack_snapshot_delete, id, snapshot['id']) + return snapshot + + def _stack_snapshot_delete(self, id, snapshot_id): + cmd = 'stack snapshot delete ' + id + ' ' + snapshot_id + if snapshot_id in self.openstack('stack snapshot list ' + id): + self.openstack(cmd) + + def _stack_snapshot_restore(self, id, snapshot_id): + cmd = 'stack snapshot restore ' + id + ' ' + snapshot_id + self.openstack(cmd) diff --git a/heatclient/tests/functional/osc/v1/test_readonly.py b/heatclient/tests/functional/osc/v1/test_readonly.py index 988823e6..2b0930e2 100644 --- a/heatclient/tests/functional/osc/v1/test_readonly.py +++ b/heatclient/tests/functional/osc/v1/test_readonly.py @@ -10,15 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -import os import yaml from tempest_lib import exceptions -from heatclient.tests.functional import base +from heatclient.tests.functional.osc.v1 import base -class SimpleReadOnlyOpenStackClientTest(base.ClientTestBase): +class SimpleReadOnlyOpenStackClientTest(base.OpenStackClientTestBase): """Basic, read-only tests for Openstack CLI client heat plugin. Basic smoke test for the openstack CLI commands which do not require @@ -92,9 +91,7 @@ def test_openstack_resource_type_show(self): self.assertIsInstance(yaml.load(rsrc_schema), dict) def _template_validate(self, templ_name, parms): - heat_template_path = os.path.join( - os.path.dirname(os.path.realpath(__file__)), - '../../templates/%s' % templ_name) + heat_template_path = self.get_template_path(templ_name) cmd = 'stack create test-stack --dry-run --template %s'\ % heat_template_path for parm in parms: diff --git a/heatclient/tests/functional/osc/v1/test_stack.py b/heatclient/tests/functional/osc/v1/test_stack.py new file mode 100644 index 00000000..7ea4668d --- /dev/null +++ b/heatclient/tests/functional/osc/v1/test_stack.py @@ -0,0 +1,56 @@ +# 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 logging + +from heatclient.tests.functional.osc.v1 import base +from tempest_lib.common.utils import data_utils as utils + +LOG = logging.getLogger(__name__) + + +class OpenStackClientStackTest(base.OpenStackClientTestBase): + """Basic stack operation tests for Openstack CLI client heat plugin. + + Basic smoke test for the openstack CLI stack commands. + """ + + def setUp(self): + super(OpenStackClientStackTest, self).setUp() + self.stack_name = utils.rand_name(name='test-stack') + + def _stack_create_minimal(self): + template = self.get_template_path('heat_minimal_hot.yaml') + parameters = ['test_client_name=test_client_name'] + return self._stack_create(self.stack_name, template, parameters) + + def test_stack_create_minimal(self): + stack = self._stack_create_minimal() + self.assertEqual(self.stack_name, stack['stack_name']) + self.assertEqual("CREATE_COMPLETE", stack['stack_status']) + + def test_stack_suspend_resume(self): + stack = self._stack_create_minimal() + stack = self._stack_suspend(stack['id']) + self.assertEqual(self.stack_name, stack['stack_name']) + self.assertEqual("SUSPEND_COMPLETE", stack['stack_status']) + stack = self._stack_resume(stack['id']) + self.assertEqual(self.stack_name, stack['stack_name']) + self.assertEqual("RESUME_COMPLETE", stack['stack_status']) + + def test_stack_snapshot_create_restore(self): + snapshot_name = utils.rand_name(name='test-stack-snapshot') + stack = self._stack_create_minimal() + snapshot = self._stack_snapshot_create(stack['id'], snapshot_name) + self.assertEqual(snapshot_name, snapshot['name']) + self._stack_snapshot_restore(stack['id'], snapshot['id']) From 0b2a87a405c5d9fb9495f67e71c2ab5124c2cb75 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Thu, 25 Feb 2016 14:06:13 +1300 Subject: [PATCH 003/374] Register a log formatter for stack event list Currently "stack event list" overrides the value formatter to use for the default log formatting style. This change creates a formatter namespace for event list so that it can have its own custom list of formatters. This means that the 'log' formatter is now registered and is the default. The formatter expects event objects rather than dicts, and so ignores the columns argument. The intention is that this will be the first of more custom formatters for event list, making the effort to establish this pattern worth it. Change-Id: I22f884f871d7467c8abadec477b3e732eb8c2150 --- heatclient/osc/v1/event.py | 27 +++++++++++++++++----- heatclient/tests/unit/osc/v1/test_event.py | 9 ++++---- setup.cfg | 8 +++++++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/heatclient/osc/v1/event.py b/heatclient/osc/v1/event.py index feee81d5..233b3ed7 100644 --- a/heatclient/osc/v1/event.py +++ b/heatclient/osc/v1/event.py @@ -15,6 +15,7 @@ import logging import time +from cliff.formatters import base from cliff import lister from cliff import show from openstackclient.common import utils @@ -88,7 +89,11 @@ class ListEvent(lister.Lister): @property def formatter_default(self): - return 'value' + return 'log' + + @property + def formatter_namespace(self): + return 'heatclient.event.formatter.list' def get_parser(self, prog_name): parser = super(ListEvent, self).get_parser(prog_name) @@ -173,8 +178,8 @@ def take_action(self, parsed_args): nested_depth = 0 if parsed_args.follow: - if parsed_args.formatter != 'value': - msg = _('--follow can only be specified with --format value') + if parsed_args.formatter != 'log': + msg = _('--follow can only be specified with --format log') raise exc.CommandError(msg) marker = parsed_args.marker @@ -205,9 +210,8 @@ def take_action(self, parsed_args): if parsed_args.sort: events = utils.sort_items(events, ','.join(parsed_args.sort)) - if parsed_args.formatter == 'value': - events = heat_utils.event_log_formatter(events).split('\n') - return [], [e.split(' ') for e in events] + if parsed_args.formatter == 'log': + return [], events if len(events): if hasattr(events[0], 'resource_name'): @@ -220,3 +224,14 @@ def take_action(self, parsed_args): columns, (utils.get_item_properties(s, columns) for s in events) ) + + +class LogFormatter(base.ListFormatter): + """A formatter which prints event objects in a log style""" + + def add_argument_group(self, parser): + pass + + def emit_list(self, column_names, data, stdout, parsed_args): + stdout.write(heat_utils.event_log_formatter(data)) + stdout.write('\n') diff --git a/heatclient/tests/unit/osc/v1/test_event.py b/heatclient/tests/unit/osc/v1/test_event.py index 668a62e6..62bef59e 100644 --- a/heatclient/tests/unit/osc/v1/test_event.py +++ b/heatclient/tests/unit/osc/v1/test_event.py @@ -225,14 +225,13 @@ def test_event_list_follow(self, sleep): self.assertEqual([], data) self.assertEqual(expected, self.fake_stdout.make_string()) - def test_event_list_value_format(self): + def test_event_list_log_format(self): arglist = ['my_stack'] expected = ('2015-11-13 10:02:17 [resource1]: CREATE_COMPLETE ' - 'state changed') + 'state changed\n') parsed_args = self.check_parser(self.cmd, arglist, []) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.run(parsed_args) self.event_client.list.assert_called_with(**self.defaults) - self.assertEqual([], columns) - self.assertEqual([expected.split(' ')], data) + self.assertEqual(expected, self.fake_stdout.make_string()) diff --git a/setup.cfg b/setup.cfg index 57fd8a07..8c5bd2ff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -72,6 +72,14 @@ openstack.orchestration.v1 = stack_template_show = heatclient.osc.v1.stack:TemplateShowStack stack_update = heatclient.osc.v1.stack:UpdateStack +heatclient.event.formatter.list = + log = heatclient.osc.v1.event:LogFormatter + table = cliff.formatters.table:TableFormatter + csv = cliff.formatters.commaseparated:CSVLister + value = cliff.formatters.value:ValueFormatter + yaml = cliff.formatters.yaml_format:YAMLFormatter + json = cliff.formatters.json_format:JSONFormatter + [global] setup-hooks = pbr.hooks.setup_hook From bd9008ed5bda3e3ac9bfda536241ae02713c5b86 Mon Sep 17 00:00:00 2001 From: Clenimar Filemon Date: Mon, 7 Mar 2016 00:52:45 -0300 Subject: [PATCH 004/374] Add an example on keystoneauth Sessions to the doc keystoneauth Session object brings an unified interface of authentication to a variety of OpenStack services. Some components (e.g. Nova, Glance) already have an example on their Python API docs. This patch adds an example on how to create a heat Client instance using keystoneauth Session API. Change-Id: Iaa51052ccb4c66aafa11e9bfd6befbd831715110 --- doc/source/index.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index ec0bdc5e..cd3fcff4 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -20,6 +20,24 @@ Once you have done so, you can use the API like so:: >>> from heatclient.client import Client >>> heat = Client('1', endpoint=heat_url, token=auth_token) +Alternatively, you can create a client instance using the keystoneauth session API:: + + >>> from keystoneauth1 import session + >>> from keystoneauth1.identity import v3 + >>> from heatclient import client + >>> password = v3.PasswordMethod(username=USERNAME, + ... password=PASSWORD, + ... user_domain_name=DEFAULT) + >>> auth = v3.Auth(auth_url=AUTH_URL, auth_methods=[password], + ... project_id=PROJECT_ID) + >>> sess = session.Session(auth=auth) + >>> heat = client.Client('1', endpoint=heat_url, session=sess) + >>> heat.stacks.list() + +For more information on keystoneauth API, see `Using Sessions`_. + +.. _Using Sessions: http://docs.openstack.org/developer/keystoneauth/using-sessions.html + Reference --------- From 618a8924afcf029c251a4fe754547c229f393acf Mon Sep 17 00:00:00 2001 From: Bo Wang Date: Thu, 10 Mar 2016 14:39:57 +0800 Subject: [PATCH 005/374] Replace ConfigParser with six.moves.configparser ConfigParser is available in python2 but not in python3. ref:http://www.diveintopython3.net/porting-code-to-python-3-with-2to3.html Change-Id: I537a13a26aa3ab936ff23af1cf37a1a18baccd8e --- tools/install_venv.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/install_venv.py b/tools/install_venv.py index 1dab63e8..795c2eb2 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -18,10 +18,11 @@ # License for the specific language governing permissions and limitations # under the License. -import ConfigParser import os import sys +from six.moves import configparser + import install_venv_common as install_venv @@ -57,7 +58,7 @@ def main(argv): pip_requires = os.path.join(root, 'requirements.txt') test_requires = os.path.join(root, 'test-requirements.txt') py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) - setup_cfg = ConfigParser.ConfigParser() + setup_cfg = configparser.ConfigParser() setup_cfg.read('setup.cfg') project = setup_cfg.get('metadata', 'name') From 9d01be24b7ee0f4468b44b218fbbfaa390e0b291 Mon Sep 17 00:00:00 2001 From: Clayton O'Neill Date: Thu, 10 Mar 2016 14:29:58 +0000 Subject: [PATCH 006/374] OSC plugin should be region/interface aware This fixes the OSC plugin so that if region is specified or if the interface is specified (public, internal, admin) then those will be passed into the heat_client when instantiated. Without this the client will pick the endpoint for the first region returned instead of the one specified. Closes-Bug: #1556263 Change-Id: I9c15719e7711c93e7fd076d38d66011bb20f988d --- heatclient/osc/plugin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/heatclient/osc/plugin.py b/heatclient/osc/plugin.py index dd980efc..7939c79e 100644 --- a/heatclient/osc/plugin.py +++ b/heatclient/osc/plugin.py @@ -33,8 +33,14 @@ def make_client(instance): API_VERSIONS) LOG.debug('Instantiating orchestration client: %s', heat_client) + endpoint = instance.get_endpoint_for_service_type( + API_NAME, + region_name=instance._region_name, + interface=instance._interface, + ) + client = heat_client( - endpoint=instance.get_endpoint_for_service_type('orchestration'), + endpoint=endpoint, session=instance.session, auth_url=instance._auth_url, username=instance._username, From 5f7510b03172e83eb94061340701da6a3f1f22df Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 14 Mar 2016 10:22:21 +1300 Subject: [PATCH 007/374] Don't use print() from event_utils.poll_for_events Non-cli users of this library function may want to capture the event log, so an 'out' argument has been added to this function. Change-Id: Ic0c82d5a0c74c527f0a05afa197b8d5845dedfca Closes-Bug: #1556677 --- heatclient/common/event_utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/heatclient/common/event_utils.py b/heatclient/common/event_utils.py index 1b8b1794..df720d83 100644 --- a/heatclient/common/event_utils.py +++ b/heatclient/common/event_utils.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import sys import time from heatclient.common import utils @@ -133,7 +134,8 @@ def _get_stack_events(hc, stack_id, event_args): return events -def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None): +def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None, + out=None): """Continuously poll events and logs for performed action on stack.""" if action: @@ -144,6 +146,8 @@ def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None): no_event_polls = 0 msg_template = _("\n Stack %(name)s %(status)s \n") + if not out: + out = sys.stdout while True: events = get_events(hc, stack_id=stack_name, event_args={'sort_dir': 'asc', @@ -156,7 +160,8 @@ def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None): # set marker to last event that was received. marker = getattr(events[-1], 'id', None) events_log = utils.event_log_formatter(events) - print(events_log) + out.write(events_log) + out.write('\n') for event in events: # check if stack event was also received From 1b889140d9dc6eba4a86fdf3e2a835dafa29b0cb Mon Sep 17 00:00:00 2001 From: Dmitriy Uvarenkov Date: Fri, 11 Mar 2016 11:38:29 +0200 Subject: [PATCH 008/374] Added deprecation warnings for heat commands Users need to use openstack commands instead of heat commands. Change-Id: Ifc79dd3f81c7735ab61d8d072ff0fea1976eed98 --- heatclient/tests/unit/test_shell.py | 1 + heatclient/v1/shell.py | 120 ++++++++++++++++++++++++++-- 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 899f3110..89857c86 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -79,6 +79,7 @@ def setUp(self): # variable is set. self.useFixture(fixtures.EnvironmentVariable('http_proxy')) self.useFixture(fixtures.EnvironmentVariable('https_proxy')) + self.patch('heatclient.v1.shell.show_deprecated') def set_fake_env(self, fake_env): client_env = ('OS_USERNAME', 'OS_PASSWORD', 'OS_TENANT_ID', diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index a61c0bc8..35540d81 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -52,6 +52,14 @@ def _do(*args, **kwargs): return _do +def show_deprecated(deprecated, recommended): + logger.warning(_LW('"%(old)s" is deprecated, ' + 'please use "%(new)s" instead'), + {'old': deprecated, + 'new': recommended} + ) + + @utils.arg('-f', '--template-file', metavar='', help=_('Path to the template.')) @utils.arg('-e', '--environment-file', metavar='', @@ -100,6 +108,8 @@ def _do(*args, **kwargs): help=_('A list of tags to associate with the stack.')) def do_stack_create(hc, args): '''Create the stack.''' + show_deprecated('heat stack-create', 'openstack stack create') + tpl_files, template = template_utils.get_template_contents( args.template_file, args.template_url, @@ -181,6 +191,8 @@ def do_stack_create(hc, args): help=_('Name of the stack to adopt.')) def do_stack_adopt(hc, args): '''Adopt a stack.''' + show_deprecated('heat stack-adopt', 'openstack stack adopt') + env_files, env = template_utils.process_multiple_environments_and_files( env_paths=args.environment_file) @@ -251,6 +263,8 @@ def do_stack_adopt(hc, args): help=_('A list of tags to associate with the stack.')) def do_stack_preview(hc, args): '''Preview the stack.''' + show_deprecated('heat stack-preview', 'openstack stack update --dry-run') + tpl_files, template = template_utils.get_template_contents( args.template_file, args.template_url, @@ -299,6 +313,8 @@ def do_stack_preview(hc, args): help=_('Skip yes/no prompt (assume yes).')) def do_stack_delete(hc, args): '''Delete the stack(s).''' + show_deprecated('heat stack-delete', 'openstack stack delete') + failure_count = 0 try: @@ -347,6 +363,8 @@ def do_stack_abandon(hc, args): any of the underlying resources. Prints an adoptable JSON representation of the stack to stdout or a file on success. ''' + show_deprecated('heat stack-abandon', 'openstack stack abandon') + fields = {'stack_id': args.id} try: stack = hc.stacks.abandon(**fields) @@ -369,6 +387,8 @@ def do_stack_abandon(hc, args): help=_('Name or ID of stack to suspend.')) def do_action_suspend(hc, args): '''Suspend the stack.''' + show_deprecated('heat action-suspend', 'openstack stack suspend') + fields = {'stack_id': args.id} try: hc.actions.suspend(**fields) @@ -382,6 +402,8 @@ def do_action_suspend(hc, args): help=_('Name or ID of stack to resume.')) def do_action_resume(hc, args): '''Resume the stack.''' + show_deprecated('heat action-resume', 'openstack stack resume') + fields = {'stack_id': args.id} try: hc.actions.resume(**fields) @@ -395,6 +417,8 @@ def do_action_resume(hc, args): help=_('Name or ID of stack to check.')) def do_action_check(hc, args): '''Check that stack resources are in expected states.''' + show_deprecated('heat action-check', 'openstack stack check') + fields = {'stack_id': args.id} try: hc.actions.check(**fields) @@ -410,6 +434,8 @@ def do_action_check(hc, args): help='Do not resolve outputs of the stack.') def do_stack_show(hc, args): '''Describe the stack.''' + show_deprecated('heat stack-show', 'openstack stack show') + fields = {'stack_id': args.id, 'resolve_outputs': not args.no_resolve_outputs} _do_stack_show(hc, fields) @@ -486,6 +512,7 @@ def do_stack_show(hc, args): help=_('An updated list of tags to associate with the stack.')) def do_stack_update(hc, args): '''Update the stack.''' + show_deprecated('heat stack-update', 'openstack stack update') tpl_files, template = template_utils.get_template_contents( args.template_file, @@ -568,6 +595,8 @@ def do_stack_update(hc, args): help=_('Name or ID of stack to cancel update for.')) def do_stack_cancel_update(hc, args): '''Cancel currently running update of the stack.''' + show_deprecated('heat stack-cancel-update', 'openstack stack cancel') + fields = {'stack_id': args.id} try: hc.actions.cancel_update(**fields) @@ -622,6 +651,8 @@ def do_stack_cancel_update(hc, args): 'enabled when using %(arg)s.') % {'arg': '--global-tenant'}) def do_stack_list(hc, args=None): '''List the user's stacks.''' + show_deprecated('heat stack-list', 'openstack stack list') + kwargs = {} fields = ['id', 'stack_name', 'stack_status', 'creation_time', 'updated_time'] @@ -679,6 +710,8 @@ def do_stack_list(hc, args=None): help=_('Name or ID of stack to query.')) def do_output_list(hc, args): """Show available outputs.""" + show_deprecated('heat output-list', 'openstack stack output list') + try: outputs = hc.stacks.output_list(args.id) except exc.HTTPNotFound: @@ -710,6 +743,8 @@ def do_output_list(hc, args): 'key and description.')) def do_output_show(hc, args): """Show a specific stack output.""" + show_deprecated('heat output-show', 'openstack stack output show') + def resolve_output(output_key): try: output = hc.stacks.output_show(args.id, output_key) @@ -781,6 +816,9 @@ def show_output(output): action='append') def do_resource_type_list(hc, args): '''List the available resource types.''' + show_deprecated('heat resource-type-list', + 'openstack orchestration resource type list') + types = hc.resource_types.list( filters=utils.format_parameters(args.filters)) utils.print_list(types, ['resource_type'], sortby_index=0) @@ -790,6 +828,9 @@ def do_resource_type_list(hc, args): help=_('Resource type to get the details for.')) def do_resource_type_show(hc, args): '''Show the resource type.''' + show_deprecated('heat resource-type-show', + 'openstack orchestration resource type show') + try: resource_type = hc.resource_types.get(args.resource_type) except exc.HTTPNotFound: @@ -809,6 +850,9 @@ def do_resource_type_show(hc, args): % ', '.join(utils.supported_formats.keys())) def do_resource_type_template(hc, args): '''Generate a template based on a resource type.''' + show_deprecated('heat resource-type-template', + 'openstack orchestration resource ' + 'type show --template-type hot') fields = {'resource_type': args.resource_type, 'template_type': args.template_type} try: @@ -827,6 +871,8 @@ def do_resource_type_template(hc, args): help=_('Name or ID of stack to get the template for.')) def do_template_show(hc, args): '''Get the template for the specified stack.''' + show_deprecated('heat template-show', 'openstack stack template show') + fields = {'stack_id': args.id} try: template = hc.stacks.template(**fields) @@ -907,6 +953,8 @@ def do_template_validate(hc, args): action='append') def do_resource_list(hc, args): '''Show list of resources belonging to a stack.''' + show_deprecated('heat resource-list', 'openstack stack resource list') + fields = { 'stack_id': args.id, 'nested_depth': args.nested_depth, @@ -941,6 +989,8 @@ def do_resource_list(hc, args): action='append') def do_resource_show(hc, args): '''Describe the resource.''' + show_deprecated('heat resource-show', 'openstack stack resource show') + fields = {'stack_id': args.id, 'resource_name': args.resource} if args.with_attr: @@ -969,9 +1019,7 @@ def do_resource_show(hc, args): help=_("The template output format, one of: %s.") % ', '.join(utils.supported_formats.keys())) def do_resource_template(hc, args): - '''DEPRECATED! Use resource-type-template instead.''' - logger.warning(_LW('DEPRECATED! Use %(cmd)s instead.'), - {'cmd': 'resource-type-template'}) + '''DEPRECATED!''' do_resource_type_template(hc, args) @@ -981,6 +1029,9 @@ def do_resource_template(hc, args): help=_('Name of the resource to show the metadata for.')) def do_resource_metadata(hc, args): '''List resource metadata.''' + show_deprecated('heat resource-metadata', + 'openstack stack resource metadata') + fields = {'stack_id': args.id, 'resource_name': args.resource} try: @@ -1003,6 +1054,8 @@ def do_resource_metadata(hc, args): help=_('File containing JSON data to send to the signal handler.')) def do_resource_signal(hc, args): '''Send a signal to a resource.''' + show_deprecated('heat resource-signal', 'openstack stack resource signal') + fields = {'stack_id': args.id, 'resource_name': args.resource} data = args.data @@ -1040,6 +1093,9 @@ def do_resource_signal(hc, args): help=_('Set the resource as healthy.')) def do_resource_mark_unhealthy(hc, args): '''Set resource's health.''' + show_deprecated('heat resource-mark-unhealthy', + 'openstack stack resource mark unhealthy') + fields = {'stack_id': args.id, 'resource_name': args.resource, 'mark_unhealthy': not args.reset, @@ -1066,6 +1122,8 @@ def do_resource_mark_unhealthy(hc, args): 'nested_stack/an*/*_resource')) def do_hook_clear(hc, args): '''Clear hooks on a given stack.''' + show_deprecated('heat hook-clear', 'openstack stack hook clear') + if args.pre_create: hook_type = 'pre-create' elif args.pre_update: @@ -1103,6 +1161,8 @@ def do_hook_clear(hc, args): default='table') def do_event_list(hc, args): '''List events for a stack.''' + show_deprecated('heat event-list', 'openstack stack event list') + display_fields = ['id', 'resource_status_reason', 'resource_status', 'event_time'] event_args = {'resource_name': args.resource, @@ -1153,6 +1213,7 @@ def do_event_list(hc, args): help=_('Depth of nested stacks from which to display hooks.')) def do_hook_poll(hc, args): '''List resources with pending hook for a stack.''' + show_deprecated('heat hook-poll', 'openstack stack hook poll') # There are a few steps to determining if a stack has pending hooks # 1. The stack is IN_PROGRESS status (otherwise, by definition no hooks @@ -1198,9 +1259,7 @@ def do_hook_poll(hc, args): @utils.arg('event', metavar='', help=_('ID of event to display details for.')) def do_event(hc, args): - '''DEPRECATED! Use event-show instead.''' - logger.warning(_LW('DEPRECATED! Use %(cmd)s instead.'), - {'cmd': 'event-show'}) + '''DEPRECATED!''' do_event_show(hc, args) @@ -1212,6 +1271,8 @@ def do_event(hc, args): help=_('ID of event to display details for.')) def do_event_show(hc, args): '''Describe the event.''' + show_deprecated('heat event-show', 'openstack stack event show') + fields = {'stack_id': args.id, 'resource_name': args.resource, 'event_id': args.event} @@ -1240,6 +1301,8 @@ def do_event_show(hc, args): help=_('Name of the configuration to create.')) def do_config_create(hc, args): '''Create a software configuration.''' + show_deprecated('heat config-create', 'openstack software config create') + config = { 'group': args.group, 'config': '' @@ -1285,6 +1348,8 @@ def do_config_create(hc, args): help=_('Return configs that appear after the given config ID.')) def do_config_list(hc, args): '''List software configs.''' + show_deprecated('heat config-list', 'openstack software config list') + kwargs = {} if args.limit: kwargs['limit'] = args.limit @@ -1301,6 +1366,8 @@ def do_config_list(hc, args): help=_('Only display the value of the property.')) def do_config_show(hc, args): '''View details of a software configuration.''' + show_deprecated('heat config-show', 'openstack software config show') + try: sc = hc.software_configs.get(config_id=args.id) except exc.HTTPNotFound: @@ -1316,6 +1383,8 @@ def do_config_show(hc, args): help=_('ID of the configuration(s) to delete.')) def do_config_delete(hc, args): '''Delete the software configuration(s).''' + show_deprecated('heat config-delete', 'openstack software config delete') + failure_count = 0 for config_id in args.id: @@ -1365,6 +1434,9 @@ def do_config_delete(hc, args): 'list of configurations currently deployed to the server.')) def do_deployment_create(hc, args): '''Create a software deployment.''' + show_deprecated('heat deployment-create', + 'openstack software deployment create') + config = {} if args.config: try: @@ -1398,6 +1470,9 @@ def do_deployment_create(hc, args): help=_('ID of the server to fetch deployments for.')) def do_deployment_list(hc, args): '''List software deployments.''' + show_deprecated('heat deployment-list', + 'openstack software deployment list') + kwargs = {'server_id': args.server} if args.server else {} deployments = hc.software_deployments.list(**kwargs) fields = ['id', 'config_id', 'server_id', 'action', 'status', @@ -1409,6 +1484,9 @@ def do_deployment_list(hc, args): help=_('ID of the deployment.')) def do_deployment_show(hc, args): '''Show the details of a software deployment.''' + show_deprecated('heat deployment-show', + 'openstack software deployment show') + try: sd = hc.software_deployments.get(deployment_id=args.id) except exc.HTTPNotFound: @@ -1421,6 +1499,9 @@ def do_deployment_show(hc, args): help=_('ID of the server to fetch deployments for.')) def do_deployment_metadata_show(hc, args): '''Get deployment configuration metadata for the specified server.''' + show_deprecated('heat deployment-metadata-show', + 'openstack software deployment metadata show') + md = hc.software_deployments.metadata(server_id=args.id) print(jsonutils.dumps(md, indent=2)) @@ -1429,6 +1510,9 @@ def do_deployment_metadata_show(hc, args): help=_('ID of the deployment(s) to delete.')) def do_deployment_delete(hc, args): '''Delete the software deployment(s).''' + show_deprecated('heat deployment-delete', + 'openstack software deployment delete') + failure_count = 0 for deploy_id in args.id: @@ -1467,6 +1551,9 @@ def do_deployment_delete(hc, args): default='raw') def do_deployment_output_show(hc, args): '''Show a specific deployment output.''' + show_deprecated('heat deployment-output-show', + 'openstack software deployment output show') + if (not args.all and args.output is None or args.all and args.output is not None): raise exc.CommandError( @@ -1497,6 +1584,8 @@ def do_deployment_output_show(hc, args): def do_build_info(hc, args): '''Retrieve build information.''' + show_deprecated('heat build-info', 'openstack orchestration build info') + result = hc.build_info.build_info() formatters = { 'api': utils.json_formatter, @@ -1511,6 +1600,8 @@ def do_build_info(hc, args): help=_('If specified, the name given to the snapshot.')) def do_stack_snapshot(hc, args): '''Make a snapshot of a stack.''' + show_deprecated('heat stack-snapshot', 'openstack stack snapshot create') + fields = {'stack_id': args.id} if args.name: fields['name'] = args.name @@ -1528,6 +1619,8 @@ def do_stack_snapshot(hc, args): help=_('The ID of the snapshot to show.')) def do_snapshot_show(hc, args): '''Show a snapshot of a stack.''' + show_deprecated('heat snapshot-show', 'openstack stack snapshot show') + fields = {'stack_id': args.id, 'snapshot_id': args.snapshot} try: snapshot = hc.stacks.snapshot_show(**fields) @@ -1543,6 +1636,8 @@ def do_snapshot_show(hc, args): help=_('The ID of the snapshot to delete.')) def do_snapshot_delete(hc, args): '''Delete a snapshot of a stack.''' + show_deprecated('heat snapshot-delete', 'openstack stack snapshot delete') + fields = {'stack_id': args.id, 'snapshot_id': args.snapshot} try: hc.stacks.snapshot_delete(**fields) @@ -1556,6 +1651,8 @@ def do_snapshot_delete(hc, args): help=_('The ID of the snapshot to restore.')) def do_stack_restore(hc, args): '''Restore a snapshot of a stack.''' + show_deprecated('heat stack-restore', 'openstack stack snapshot restore') + fields = {'stack_id': args.id, 'snapshot_id': args.snapshot} try: hc.stacks.restore(**fields) @@ -1567,6 +1664,8 @@ def do_stack_restore(hc, args): help=_('Name or ID of the stack containing the snapshots.')) def do_snapshot_list(hc, args): '''List the snapshots of a stack.''' + show_deprecated('heat snapshot-list', 'openstack stack snapshot list') + fields = {'stack_id': args.id} try: snapshots = hc.stacks.snapshot_list(**fields) @@ -1586,6 +1685,9 @@ def do_snapshot_list(hc, args): def do_service_list(hc, args=None): '''List the Heat engines.''' + show_deprecated('heat service-list', + 'openstack orchestration service list') + fields = ['hostname', 'binary', 'engine_id', 'host', 'topic', 'updated_at', 'status'] services = hc.services.list() @@ -1594,6 +1696,9 @@ def do_service_list(hc, args=None): def do_template_version_list(hc, args): '''List the available template versions.''' + show_deprecated('heat template-version-list', + 'openstack orchestration template version list') + versions = hc.template_versions.list() fields = ['version', 'type'] utils.print_list(versions, fields, sortby_index=1) @@ -1603,6 +1708,9 @@ def do_template_version_list(hc, args): help=_('Template version to get the functions for.')) def do_template_function_list(hc, args): '''List the available functions.''' + show_deprecated('heat template-function-list', + 'openstack orchestration template function list') + try: functions = hc.template_versions.get(args.template_version) except exc.HTTPNotFound: From 4bf7e55aa369e7694d560284d767257abdf12895 Mon Sep 17 00:00:00 2001 From: PanFengyun Date: Thu, 17 Mar 2016 12:59:07 +0800 Subject: [PATCH 009/374] stack-delete command should not call do_stack_list. the stack-delete operation is asynchronous, and should not return anything. Change-Id: I75287b612358c5e76e47a9e53200c23946f01d8f Closes-Bug: #1556407 --- heatclient/tests/unit/test_shell.py | 69 +++++++---------------------- heatclient/v1/shell.py | 3 +- 2 files changed, 19 insertions(+), 53 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 89857c86..ba68277f 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -2486,26 +2486,18 @@ def test_stack_delete_prompt_with_tty(self, ms): else: self.client.raw_request( 'DELETE', '/stacks/teststack2/2').AndReturn((resp, None)) - fakes.script_heat_list(client=self.client) self.m.ReplayAll() - delete_text = self.shell('stack-delete teststack2/2') - self.assertRegexpMatches( - delete_text, "Are you sure you want to delete this stack") - self.assertNotRegexpMatches(delete_text, "teststack") + resp = self.shell('stack-delete teststack2/2') + resp_text = 'Are you sure you want to delete this stack(s) [y/N]? ' + self.assertEqual(resp_text, resp) self.m.ReplayAll() mock_stdin.readline.return_value = 'Y' - delete_text = self.shell('stack-delete teststack2/2') - required = [ - 'stack_name', - 'id', - 'teststack', - '1' - ] - for r in required: - self.assertRegexpMatches(delete_text, r) + resp = self.shell('stack-delete teststack2/2') + msg = 'Request to delete stack teststack2/2 has been accepted.' + self.assertRegexpMatches(resp, msg) # the main thing this @mock.patch is doing here is keeping # sys.stdin untouched for later tests @@ -2530,21 +2522,12 @@ def test_stack_delete_prompt_with_tty_y(self, ms): else: self.client.raw_request( 'DELETE', '/stacks/teststack2/2').AndReturn((resp, None)) - fakes.script_heat_list(client=self.client) self.m.ReplayAll() # -y from the shell should skip the n/y prompt - delete_text = self.shell('stack-delete -y teststack2/2') - self.assertNotRegexpMatches( - delete_text, "Are you sure you want to delete this stack") - required = [ - 'stack_name', - 'id', - 'teststack', - '1' - ] - for r in required: - self.assertRegexpMatches(delete_text, r) + resp = self.shell('stack-delete -y teststack2/2') + msg = 'Request to delete stack teststack2/2 has been accepted.' + self.assertRegexpMatches(resp, msg) def test_stack_delete(self): self.register_keystone_auth_fixture() @@ -2559,20 +2542,11 @@ def test_stack_delete(self): else: self.client.raw_request( 'DELETE', '/stacks/teststack2/2').AndReturn((resp, None)) - fakes.script_heat_list(client=self.client) self.m.ReplayAll() - - delete_text = self.shell('stack-delete teststack2/2') - - required = [ - 'stack_name', - 'id', - 'teststack', - '1' - ] - for r in required: - self.assertRegexpMatches(delete_text, r) + resp = self.shell('stack-delete teststack2/2') + msg = 'Request to delete stack teststack2/2 has been accepted.' + self.assertRegexpMatches(resp, msg) def test_stack_delete_multiple(self): self.register_keystone_auth_fixture() @@ -2591,22 +2565,13 @@ def test_stack_delete_multiple(self): 'DELETE', '/stacks/teststack/1').AndReturn((resp, None)) self.client.raw_request( 'DELETE', '/stacks/teststack2/2').AndReturn((resp, None)) - fakes.script_heat_list(client=self.client) self.m.ReplayAll() - - delete_text = self.shell('stack-delete teststack/1 teststack2/2') - - required = [ - 'stack_name', - 'id', - 'teststack', - '1', - 'teststack2', - '2' - ] - for r in required: - self.assertRegexpMatches(delete_text, r) + resp = self.shell('stack-delete teststack/1 teststack2/2') + msg1 = 'Request to delete stack teststack/1 has been accepted.' + msg2 = 'Request to delete stack teststack2/2 has been accepted.' + self.assertRegexpMatches(resp, msg1) + self.assertRegexpMatches(resp, msg2) def test_stack_delete_failed_on_notfound(self): self.register_keystone_auth_fixture() diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 35540d81..98cc4275 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -339,6 +339,8 @@ def do_stack_delete(hc, args): fields = {'stack_id': sid} try: hc.stacks.delete(**fields) + success_msg = _("Request to delete stack %s has been accepted.") + print(success_msg % sid) except (exc.HTTPNotFound, exc.Forbidden) as e: failure_count += 1 print(e) @@ -347,7 +349,6 @@ def do_stack_delete(hc, args): "stacks.") % {'count': failure_count, 'total': len(args.id)}) - do_stack_list(hc) @utils.arg('-O', '--output-file', metavar='', From f13d36ae0114e159663ea2a3fa07260df9d9407d Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Mon, 21 Mar 2016 17:03:43 +0000 Subject: [PATCH 010/374] Fix regression handling relative paths in environment 7627e1bae53747e1471caf632dfc1b6edf2e26cf introduced new code to add the environment to the files mapping, so server side resolution of multiple environments is possible. However, the environment added is wrong when you're handling those with relative paths inside, we must add the resolved version or the keys don't map to keys in the files map, an you get an error when we try to locate the file on the server. Evidently we lack adequate functional test coverage of this, which can be addressed via a heat patch, but I added a unit test illustrating the problem. Change-Id: I3bd2cd48623e4ad1b4067f4cefc7ddff30b9e88d Closes-Bug: #1560106 --- heatclient/common/template_utils.py | 6 +-- heatclient/tests/unit/test_template_utils.py | 42 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index fae54efe..a1b176c3 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -289,9 +289,6 @@ def process_environment_and_files(env_path=None, env_base_url = utils.base_url_for_url(env_url) raw_env = request.urlopen(env_url).read() - if include_env_in_files: - files[env_url] = raw_env - env = environment_format.parse(raw_env) resolve_environment_urls( @@ -299,6 +296,9 @@ def process_environment_and_files(env_path=None, files, env_base_url) + if include_env_in_files: + files[env_url] = jsonutils.dumps(env) + return files, env diff --git a/heatclient/tests/unit/test_template_utils.py b/heatclient/tests/unit/test_template_utils.py index 920573d6..551f4636 100644 --- a/heatclient/tests/unit/test_template_utils.py +++ b/heatclient/tests/unit/test_template_utils.py @@ -409,6 +409,48 @@ def test_process_multiple_environments_and_files_tracker(self): files['file:///home/b/a.yaml']) self.assertEqual(['file:///home/my/dir/env1.yaml'], env_file_list) + self.assertEqual(json.dumps(expected_env), + files['file:///home/my/dir/env1.yaml']) + + def test_process_environment_relative_file_tracker(self): + + self.m.StubOutWithMock(request, 'urlopen') + env_file = '/home/my/dir/env.yaml' + env_url = 'file:///home/my/dir/env.yaml' + env = b''' + resource_registry: + "OS::Thingy": a.yaml + ''' + + request.urlopen(env_url).AndReturn( + six.BytesIO(env)) + request.urlopen('file:///home/my/dir/a.yaml').AndReturn( + six.BytesIO(self.template_a)) + request.urlopen('file:///home/my/dir/a.yaml').AndReturn( + six.BytesIO(self.template_a)) + self.m.ReplayAll() + + self.assertEqual( + env_url, + utils.normalise_file_path_to_url(env_file)) + self.assertEqual( + 'file:///home/my/dir', + utils.base_url_for_url(env_url)) + + env_file_list = [] + files, env = template_utils.process_multiple_environments_and_files( + [env_file], env_list_tracker=env_file_list) + + # Verify + expected_env = {'resource_registry': + {'OS::Thingy': 'file:///home/my/dir/a.yaml'}} + self.assertEqual(expected_env, env) + + self.assertEqual(self.template_a.decode('utf-8'), + files['file:///home/my/dir/a.yaml']) + self.assertEqual(['file:///home/my/dir/env.yaml'], env_file_list) + self.assertEqual(json.dumps(expected_env), + files['file:///home/my/dir/env.yaml']) def test_global_files(self): url = 'file:///home/b/a.yaml' From fc4ff3875a4f06fe2f596d268ae8cf87db748226 Mon Sep 17 00:00:00 2001 From: Oleksii Chuprykov Date: Tue, 22 Mar 2016 20:50:52 +0200 Subject: [PATCH 011/374] Use stack.get for resolving outputs for old API If we use new heatclient and heat with old RPC API (v < 1.19), we doesn't have output_show/output_list endpoints. Use stack.get in this case and get outputs from it. Change-Id: I697ff2002fa123fce9a0f294b74c9fa0c92796a4 --- heatclient/osc/v1/stack.py | 20 ++++++++++++--- heatclient/tests/unit/osc/v1/test_stack.py | 29 ++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index e60b6e20..6c4cdad5 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -862,7 +862,17 @@ def take_action(self, parsed_args): except heat_exc.HTTPNotFound: msg = _('Stack %(id)s or output %(out)s not found.') % { 'id': parsed_args.stack, 'out': parsed_args.output} - raise exc.CommandError(msg) + try: + output = None + stack = client.stacks.get(parsed_args.stack).to_dict() + for o in stack.get('outputs', []): + if o['output_key'] == parsed_args.output: + output = o + break + if output is None: + raise exc.CommandError(msg) + except heat_exc.HTTPNotFound: + raise exc.CommandError(msg) if 'output_error' in output: msg = _('Output error: %s') % output['output_error'] @@ -898,8 +908,12 @@ def take_action(self, parsed_args): try: outputs = client.stacks.output_list(parsed_args.stack)['outputs'] except heat_exc.HTTPNotFound: - msg = _('Stack not found: %s') % parsed_args.stack - raise exc.CommandError(msg) + try: + outputs = client.stacks.get( + parsed_args.stack).to_dict()['outputs'] + except heat_exc.HTTPNotFound: + msg = _('Stack not found: %s') % parsed_args.stack + raise exc.CommandError(msg) columns = ['output_key', 'description'] diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index 788414b7..dbad67d1 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -846,6 +846,7 @@ def test_stack_output_show_output_error(self): def test_stack_output_show_bad_output(self): arglist = ['my_stack', 'output3'] self.stack_client.output_show.side_effect = heat_exc.HTTPNotFound + self.stack_client.get.side_effect = heat_exc.HTTPNotFound parsed_args = self.check_parser(self.cmd, arglist, []) error = self.assertRaises(exc.CommandError, @@ -854,15 +855,32 @@ def test_stack_output_show_bad_output(self): str(error)) self.stack_client.output_show.assert_called_with('my_stack', 'output3') + def test_stack_output_show_old_api(self): + arglist = ['my_stack', 'output1'] + self.stack_client.output_show.side_effect = heat_exc.HTTPNotFound + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, outputs = self.cmd.take_action(parsed_args) + + self.stack_client.get.assert_called_with('my_stack') + self.assertEqual(('output_key', 'output_value'), columns) + self.assertEqual(('output1', 'value1'), outputs) + class TestStackOutputList(TestStack): response = {'outputs': [{'output_key': 'key1', 'description': 'desc1'}, {'output_key': 'key2', 'description': 'desc2'}]} + stack_response = { + 'stack_name': 'my_stack', + 'outputs': response['outputs'] + } def setUp(self): super(TestStackOutputList, self).setUp() self.cmd = stack.OutputListStack(self.app, None) + self.stack_client.get = mock.MagicMock( + return_value=stacks.Stack(None, self.response)) def test_stack_output_list(self): arglist = ['my_stack'] @@ -877,12 +895,23 @@ def test_stack_output_list(self): def test_stack_output_list_not_found(self): arglist = ['my_stack'] self.stack_client.output_list.side_effect = heat_exc.HTTPNotFound + self.stack_client.get.side_effect = heat_exc.HTTPNotFound parsed_args = self.check_parser(self.cmd, arglist, []) error = self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) self.assertEqual('Stack not found: my_stack', str(error)) + def test_stack_output_list_old_api(self): + arglist = ['my_stack'] + self.stack_client.output_list.side_effect = heat_exc.HTTPNotFound + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, outputs = self.cmd.take_action(parsed_args) + + self.stack_client.get.assert_called_with('my_stack') + self.assertEqual(['output_key', 'description'], columns) + class TestStackTemplateShow(TestStack): From 1d4c407fc53ed2b57377095f0f5d99079afcf51f Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Fri, 4 Mar 2016 14:36:13 -0600 Subject: [PATCH 012/374] OSC plugin for orchestration template validate This change implements "openstack orchestration template validate" command Based from the existing heat commands: heat template-validate This is different from the stack create/update --dry-run which used the preview api, this uses the validate api. Change-Id: Icf5794ad6bb35574a060f095d7ef10e6a46ca2fe Blueprint: heat-support-python-openstackclient --- heatclient/common/http.py | 13 +++ heatclient/osc/v1/stack.py | 15 +--- heatclient/osc/v1/template.py | 83 +++++++++++++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 2 - heatclient/tests/unit/osc/v1/test_template.py | 73 ++++++++++++++++ heatclient/v1/shell.py | 23 ++--- setup.cfg | 1 + 7 files changed, 178 insertions(+), 32 deletions(-) diff --git a/heatclient/common/http.py b/heatclient/common/http.py index 4b46c71d..8a799b69 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -39,6 +39,19 @@ osprofiler_web = importutils.try_import("osprofiler.web") +def authenticated_fetcher(hc): + """A wrapper around the heat client object to fetch a template.""" + + def _do(*args, **kwargs): + if isinstance(hc.http_client, SessionClient): + method, url = args + return hc.http_client.request(url, method, **kwargs).content + else: + return hc.http_client.raw_request(*args, **kwargs).content + + return _do + + def get_system_ca_file(): """Return path to system default CA file.""" # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index e60b6e20..e9059df8 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -37,17 +37,6 @@ from heatclient.openstack.common._i18n import _LI -def _authenticated_fetcher(client): - def _do(*args, **kwargs): - if isinstance(client.http_client, http.SessionClient): - method, url = args - return client.http_client.request(url, method, **kwargs).content - else: - return client.http_client.raw_request(*args, **kwargs).content - - return _do - - class CreateStack(show.ShowOne): """Create a stack.""" @@ -136,7 +125,7 @@ def take_action(self, parsed_args): tpl_files, template = template_utils.process_template_path( parsed_args.template, - object_request=_authenticated_fetcher(client)) + object_request=http.authenticated_fetcher(client)) env_files, env = ( template_utils.process_multiple_environments_and_files( @@ -297,7 +286,7 @@ def take_action(self, parsed_args): tpl_files, template = template_utils.process_template_path( parsed_args.template, - object_request=_authenticated_fetcher(client), + object_request=http.authenticated_fetcher(client), existing=parsed_args.existing) env_files, env = ( diff --git a/heatclient/osc/v1/template.py b/heatclient/osc/v1/template.py index 28a3f5b1..2d3d7d1a 100644 --- a/heatclient/osc/v1/template.py +++ b/heatclient/osc/v1/template.py @@ -13,10 +13,16 @@ # Copyright 2015 IBM Corp. import logging +import six + from cliff import lister from openstackclient.common import utils +from heatclient.common import format_utils +from heatclient.common import http +from heatclient.common import template_utils +from heatclient.common import utils as heat_utils from heatclient import exc from heatclient.openstack.common._i18n import _ @@ -72,3 +78,80 @@ def take_action(self, parsed_args): fields, (utils.get_item_properties(s, fields) for s in functions) ) + + +class Validate(format_utils.YamlFormat): + """Validate a template""" + + log = logging.getLogger(__name__ + ".Validate") + + def get_parser(self, prog_name): + parser = super(Validate, self).get_parser(prog_name) + parser.add_argument( + '-t', '--template', + metavar='