From 1f7c44517d292912c68bf14e0c9ac96c340be79a Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 22 Jun 2015 12:16:17 +1200 Subject: [PATCH 001/553] resource-list --nested-depth stack_name column For resource-list --nested-depth an extra column parent_resource is added to the result. This has the following issues: - parent_resource values cannot be used as the stack argument to resource-show - templates with more than one ResourceGroup will have more than one parent_resource called "0", so there is no way of inferring the actual parent, and it is not possible to guess the correct parent resource - parent_resource is inconsistent with event-list --nested-depth, which adds a stack_name column that doesn't have the above disadvantages. This change replaces the parent_resource column with a stack_name column for resource-list --nested-depth. Change-Id: I6b4bc306db48e08931ff294bcbefeb6bbb195663 Closes-bug: #1467332 --- heatclient/tests/unit/test_resources.py | 17 +++++++++++++++++ heatclient/tests/unit/test_shell.py | 10 ++++++++-- heatclient/v1/resources.py | 8 ++++++++ heatclient/v1/shell.py | 2 +- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/heatclient/tests/unit/test_resources.py b/heatclient/tests/unit/test_resources.py index c3ce71dd..12a7ccc4 100644 --- a/heatclient/tests/unit/test_resources.py +++ b/heatclient/tests/unit/test_resources.py @@ -175,3 +175,20 @@ def test_signal(self): manager = self._base_test(expect, key) manager.signal(**fields) self.m.VerifyAll() + + +class ResourceStackNameTest(testtools.TestCase): + + def test_stack_name(self): + resource = resources.Resource(None, {"links": [{ + "href": "http://heat.example.com:8004/foo/12/resources/foobar", + "rel": "self" + }, { + "href": "http://heat.example.com:8004/foo/12", + "rel": "stack" + }]}) + self.assertEqual('foo', resource.stack_name) + + def test_stack_name_no_links(self): + resource = resources.Resource(None, {}) + self.assertIsNone(resource.stack_name) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index c27b907c..b2efa2ab 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -2848,7 +2848,13 @@ def test_resource_list_nested(self): self.register_keystone_auth_fixture() resp_dict = {"resources": [{ "resource_name": "foobar", - "parent_resource": "my_parent_resource", + "links": [{ + "href": "http://heat.example.com:8004/foo/12/resources/foobar", + "rel": "self" + }, { + "href": "http://heat.example.com:8004/foo/12", + "rel": "stack" + }], }]} resp = fakes.FakeHTTPResponse( 200, @@ -2867,7 +2873,7 @@ def test_resource_list_nested(self): required = [ 'resource_name', 'foobar', - 'parent_resource', 'my_parent_resource', + 'stack_name', 'foo', ] for field in required: self.assertRegexpMatches(resource_list_text, field) diff --git a/heatclient/v1/resources.py b/heatclient/v1/resources.py index 02941f7f..e84c4912 100644 --- a/heatclient/v1/resources.py +++ b/heatclient/v1/resources.py @@ -36,6 +36,14 @@ def delete(self): def data(self, **kwargs): return self.manager.data(self, **kwargs) + @property + def stack_name(self): + if not hasattr(self, 'links'): + return + for l in self.links: + if l['rel'] == 'stack': + return l['href'].split('/')[-2] + class ResourceManager(stacks.StackChildManager): resource_class = Resource diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 84d751f2..a8f515c2 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -750,7 +750,7 @@ def do_resource_list(hc, args): fields.insert(0, 'resource_name') if args.nested_depth: - fields.append('parent_resource') + fields.append('stack_name') utils.print_list(resources, fields, sortby_index=4) From 0349863b05904397bd2cbfbefa5dd246474c86bf Mon Sep 17 00:00:00 2001 From: Peter Razumovsky Date: Wed, 11 Mar 2015 18:30:18 +0300 Subject: [PATCH 002/553] Move SessionClient from HTTPClient to adapter Many projects use keystoneclient adapter for SessionClient realisation, but heatclient use HTTPClient, which is break SessionClient logic (e.g. using credentials and endpoint). Change-Id: I90bc9d9fcd1058c3eca288f08dd134810f1b0000 Closes-bug: #1427596 --- heatclient/common/http.py | 99 +- heatclient/tests/unit/fakes.py | 25 +- heatclient/tests/unit/test_common_http.py | 31 +- heatclient/tests/unit/test_shell.py | 1074 +++++++++++++++------ heatclient/v1/shell.py | 7 +- 5 files changed, 832 insertions(+), 404 deletions(-) diff --git a/heatclient/common/http.py b/heatclient/common/http.py index 678081bb..978cf75d 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -30,6 +30,7 @@ from heatclient import exc from heatclient.openstack.common._i18n import _ from heatclient.openstack.common._i18n import _LW +from keystoneclient import adapter LOG = logging.getLogger(__name__) USER_AGENT = 'python-heatclient' @@ -295,87 +296,57 @@ def patch(self, url, **kwargs): return self.client_request("PATCH", url, **kwargs) -class SessionClient(HTTPClient): +class SessionClient(adapter.LegacyJsonAdapter): """HTTP client based on Keystone client session.""" - # NOTE(dhu): Will eventually move to a common session client. - # https://bugs.launchpad.net/python-keystoneclient/+bug/1332337 - def __init__(self, session, auth, endpoint=None, **kwargs): - self.session = session - self.auth = auth - - if endpoint is not None: - self.endpoint = endpoint - else: - self.endpoint = kwargs.get('endpoint') - self.auth_url = kwargs.get('auth_url') - self.region_name = kwargs.get('region_name') - self.interface = kwargs.get('interface', - kwargs.get('endpoint_type', 'public')) - self.service_type = kwargs.get('service_type') - - self.include_pass = kwargs.get('include_pass') - self.username = kwargs.get('username') - self.password = kwargs.get('password') - # see if we can get the auth_url from auth plugin if one is not - # provided from kwargs - if not self.auth_url and hasattr(self.auth, 'auth_url'): - self.auth_url = self.auth.auth_url - - def _http_request(self, url, method, **kwargs): + def request(self, url, method, **kwargs): kwargs.setdefault('user_agent', USER_AGENT) - kwargs.setdefault('auth', self.auth) - - endpoint_filter = kwargs.setdefault('endpoint_filter', {}) - endpoint_filter.setdefault('interface', self.interface) - endpoint_filter.setdefault('service_type', self.service_type) - endpoint_filter.setdefault('region_name', self.region_name) - - # TODO(gyee): what are these headers for? - if self.auth_url: - kwargs['headers'].setdefault('X-Auth-Url', self.auth_url) - if self.region_name: - kwargs['headers'].setdefault('X-Region-Name', self.region_name) - if self.include_pass and 'X-Auth-Key' not in kwargs['headers']: - kwargs['headers'].update(self.credentials_headers()) - # Allow caller to specify not to follow redirects, in which case we - # just return the redirect response. Useful for using stacks:lookup. - follow_redirects = kwargs.pop('follow_redirects', True) - - # If the endpoint is passed in, make sure keystone uses it - # instead of looking up the endpoint in the auth plugin. - if self.endpoint is not None: - kwargs['endpoint_override'] = self.endpoint + try: + kwargs.setdefault('json', kwargs.pop('data')) + except KeyError: + pass - resp = self.session.request(url, method, redirect=follow_redirects, - raise_exc=False, **kwargs) + resp, body = super(SessionClient, self).request(url, method, + raise_exc=False, + **kwargs) if 400 <= resp.status_code < 600: raise exc.from_response(resp) elif resp.status_code in (301, 302, 305): - # Redirected. Reissue the request to the new location, - # unless caller specified follow_redirects=False - if follow_redirects: - location = resp.headers.get('location') - if location is None: - message = _("Location not returned with 302") - raise exc.InvalidEndpoint(message=message) - elif (self.endpoint is not None and - location.lower().startswith(self.endpoint.lower())): - location = location[len(self.endpoint):] - resp = self._http_request(location, method, **kwargs) + location = resp.headers.get('location') + path = self.strip_endpoint(location) + resp = self.request(path, method, **kwargs) elif resp.status_code == 300: raise exc.from_response(resp) return resp + def credentials_headers(self): + return {} + + def strip_endpoint(self, location): + if location is None: + message = _("Location not returned with 302") + raise exc.InvalidEndpoint(message=message) + if (self.endpoint_override is not None and + location.lower().startswith(self.endpoint_override.lower())): + return location[len(self.endpoint_override):] + else: + return location + -def _construct_http_client(*args, **kwargs): +def _construct_http_client(endpoint=None, username=None, password=None, + include_pass=None, endpoint_type=None, + auth_url=None, **kwargs): session = kwargs.pop('session', None) auth = kwargs.pop('auth', None) if session: - return SessionClient(session, auth, *args, **kwargs) + kwargs['endpoint_override'] = endpoint + return SessionClient(session, auth=auth, **kwargs) else: - return HTTPClient(*args, **kwargs) + return HTTPClient(endpoint=endpoint, username=username, + password=password, include_pass=include_pass, + endpoint_type=endpoint_type, auth_url=auth_url, + **kwargs) diff --git a/heatclient/tests/unit/fakes.py b/heatclient/tests/unit/fakes.py index 0ae20734..4a65a995 100644 --- a/heatclient/tests/unit/fakes.py +++ b/heatclient/tests/unit/fakes.py @@ -17,7 +17,7 @@ from heatclient import exc -def script_heat_list(url=None, show_nested=False): +def script_heat_list(url=None, show_nested=False, client=http.HTTPClient): if url is None: url = '/stacks?' @@ -52,7 +52,10 @@ def script_heat_list(url=None, show_nested=False): 'success, you', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request('GET', url).AndReturn((resp, resp_dict)) + if client == http.SessionClient: + client.request(url, 'GET').AndReturn(resp) + else: + client.json_request('GET', url).AndReturn((resp, resp_dict)) def mock_script_heat_list(show_nested=False): @@ -90,7 +93,7 @@ def mock_script_heat_list(show_nested=False): return resp, resp_dict -def script_heat_normal_error(): +def script_heat_normal_error(client=http.HTTPClient): resp_dict = { "explanation": "The resource could not be found.", "code": 404, @@ -105,17 +108,23 @@ def script_heat_normal_error(): 'The resource could not be found', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request('GET', '/stacks/bad').AndRaise( - exc.from_response(resp)) + if client == http.SessionClient: + client.request('/stacks/bad', 'GET').AndRaise(exc.from_response(resp)) + else: + client.json_request('GET', + '/stacks/bad').AndRaise(exc.from_response(resp)) -def script_heat_error(resp_string): +def script_heat_error(resp_string, client=http.HTTPClient): resp = FakeHTTPResponse(400, 'The resource could not be found', {'content-type': 'application/json'}, resp_string) - http.HTTPClient.json_request('GET', '/stacks/bad').AndRaise( - exc.from_response(resp)) + if client == http.SessionClient: + client.request('/stacks/bad', 'GET').AndRaise(exc.from_response(resp)) + else: + client.json_request('GET', + '/stacks/bad').AndRaise(exc.from_response(resp)) def fake_headers(): diff --git a/heatclient/tests/unit/test_common_http.py b/heatclient/tests/unit/test_common_http.py index 6a045165..d1e288f3 100644 --- a/heatclient/tests/unit/test_common_http.py +++ b/heatclient/tests/unit/test_common_http.py @@ -20,9 +20,11 @@ import testtools from heatclient.common import http +from heatclient.common import utils from heatclient import exc from heatclient.tests.unit import fakes from heatclient.tests.unit import test_shell +from keystoneclient import adapter from keystoneclient.auth.identity import v2 as ks_v2_auth from keystoneclient import session from mox3 import mox @@ -665,37 +667,40 @@ def setUp(self): super(SessionClientTest, self).setUp() self.register_keystone_auth_fixture() self.auth_session = session.Session() - self.auth_session.request = mock.Mock() self.auth_plugin = ks_v2_auth.Password(test_shell.V2_URL, 'xx', 'xx') - def test_session_raw_request(self): - self.auth_session.request.return_value = fakes.FakeHTTPResponse( - 200, 'OK', {'content-type': 'application/octet-stream'}, '') + @mock.patch.object(adapter.LegacyJsonAdapter, 'request') + def test_session_raw_request(self, mock_request): + mock_request.return_value = (fakes.FakeHTTPResponse( + 200, 'OK', {'content-type': 'application/octet-stream'}, ''), '') client = http.SessionClient(session=self.auth_session, auth=self.auth_plugin) - resp = client.raw_request(method='GET', url='') + resp = client.request(method='GET', url='') self.assertEqual(200, resp.status_code) self.assertEqual('', ''.join([x for x in resp.content])) - def test_session_json_request(self): - self.auth_session.request.return_value = fakes.FakeHTTPResponse( - 200, 'OK', {'content-type': 'application/json'}, '{}') + @mock.patch.object(adapter.LegacyJsonAdapter, 'request') + def test_session_json_request(self, mock_request): + mock_request.return_value = (fakes.FakeHTTPResponse( + 200, 'OK', {'content-type': 'application/json'}, '{}'), {}) client = http.SessionClient(session=self.auth_session, auth=self.auth_plugin) - resp, body = client.json_request('GET', '') + resp = client.request('', 'GET') + body = utils.get_response_body(resp) self.assertEqual(200, resp.status_code) self.assertEqual({}, body) - def test_404_error_response(self): - self.auth_session.request.return_value = fakes.FakeHTTPResponse( - 404, 'OK', {'content-type': 'application/octet-stream'}, '') + @mock.patch.object(adapter.LegacyJsonAdapter, 'request') + def test_404_error_response(self, mock_request): + mock_request.return_value = (fakes.FakeHTTPResponse( + 404, 'OK', {'content-type': 'application/octet-stream'}, ''), '') client = http.SessionClient(session=self.auth_session, auth=self.auth_plugin) e = self.assertRaises(exc.HTTPNotFound, - client.raw_request, 'GET', '') + client.request, '', 'GET') # Assert that the raised exception can be converted to string self.assertIsNotNone(str(e)) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index c27b907c..462c63d7 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -248,10 +248,10 @@ def setUp(self): def test_failed_auth(self): self.register_keystone_auth_fixture() - self.m.StubOutWithMock(http.HTTPClient, 'json_request') + self.m.StubOutWithMock(http.SessionClient, 'request') failed_msg = 'Unable to authenticate user with credentials provided' - http.HTTPClient.json_request( - 'GET', '/stacks?').AndRaise(exc.Unauthorized(failed_msg)) + http.SessionClient.request( + '/stacks?', 'GET').AndRaise(exc.Unauthorized(failed_msg)) self.m.ReplayAll() self.set_fake_env(FAKE_ENV_KEYSTONE_V2) @@ -296,6 +296,8 @@ def setUp(self): self.m = mox.Mox() self.m.StubOutWithMock(http.HTTPClient, 'json_request') self.m.StubOutWithMock(http.HTTPClient, 'raw_request') + self.m.StubOutWithMock(http.SessionClient, 'request') + self.client = http.SessionClient self.addCleanup(self.m.VerifyAll) self.addCleanup(self.m.UnsetStubs) @@ -328,7 +330,17 @@ class ShellTestNoMox(TestCase): # use requests_mock to expose errors from json_request. def setUp(self): super(ShellTestNoMox, self).setUp() - self.set_fake_env(FAKE_ENV_KEYSTONE_V2) + self._set_fake_env() + + def _set_fake_env(self): + self.set_fake_env({ + 'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + 'HEAT_URL': 'http://heat.example.com', + 'OS_AUTH_URL': BASE_URL, + 'OS_NO_CLIENT_AUTH': 'True' + }) def shell(self, argstr): orig = sys.stdout @@ -433,7 +445,10 @@ def test_event_list(self): class ShellTestNoMoxV3(ShellTestNoMox): def _set_fake_env(self): - self.set_fake_env(FAKE_ENV_KEYSTONE_V3) + fake_env_kwargs = {'OS_NO_CLIENT_AUTH': 'True', + 'HEAT_URL': 'http://heat.example.com'} + fake_env_kwargs.update(FAKE_ENV_KEYSTONE_V3) + self.set_fake_env(fake_env_kwargs) class ShellTestEndpointType(TestCase): @@ -511,6 +526,7 @@ class ShellTestCommon(ShellBase): def setUp(self): super(ShellTestCommon, self).setUp() + self.client = http.SessionClient self.set_fake_env(FAKE_ENV_KEYSTONE_V2) def test_help_unknown_command(self): @@ -540,8 +556,12 @@ def test_command_help(self): def test_debug_switch_raises_error(self): self.register_keystone_auth_fixture() - http.HTTPClient.json_request( - 'GET', '/stacks?').AndRaise(exc.Unauthorized("FAIL")) + if self.client == http.SessionClient: + self.client.request( + '/stacks?', 'GET').AndRaise(exc.Unauthorized("FAIL")) + else: + self.client.json_request( + 'GET', '/stacks?').AndRaise(exc.Unauthorized("FAIL")) self.m.ReplayAll() @@ -550,8 +570,12 @@ def test_debug_switch_raises_error(self): def test_dash_d_switch_raises_error(self): self.register_keystone_auth_fixture() - http.HTTPClient.json_request( - 'GET', '/stacks?').AndRaise(exc.CommandError("FAIL")) + if self.client == http.SessionClient: + self.client.request( + '/stacks?', 'GET').AndRaise(exc.CommandError("FAIL")) + else: + self.client.json_request( + 'GET', '/stacks?').AndRaise(exc.CommandError("FAIL")) self.m.ReplayAll() @@ -560,8 +584,12 @@ def test_dash_d_switch_raises_error(self): def test_no_debug_switch_no_raises_errors(self): self.register_keystone_auth_fixture() - http.HTTPClient.json_request( - 'GET', '/stacks?').AndRaise(exc.Unauthorized("FAIL")) + if self.client == http.SessionClient: + self.client.request( + '/stacks?', 'GET').AndRaise(exc.Unauthorized("FAIL")) + else: + self.client.json_request( + 'GET', '/stacks?').AndRaise(exc.Unauthorized("FAIL")) self.m.ReplayAll() @@ -586,6 +614,8 @@ class ShellTestUserPass(ShellBase): def setUp(self): super(ShellTestUserPass, self).setUp() + if self.client is None: + self.client = http.SessionClient self._set_fake_env() def _set_fake_env(self): @@ -593,7 +623,7 @@ def _set_fake_env(self): def test_stack_list(self): self.register_keystone_auth_fixture() - fakes.script_heat_list() + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -617,7 +647,8 @@ def test_stack_list_show_nested(self): expected_url = '/stacks?%s' % parse.urlencode({ 'show_nested': True, }, True) - fakes.script_heat_list(expected_url, show_nested=True) + fakes.script_heat_list(expected_url, show_nested=True, + client=self.client) self.m.ReplayAll() @@ -636,7 +667,7 @@ def test_stack_list_show_nested(self): def test_stack_list_show_owner(self): self.register_keystone_auth_fixture() - fakes.script_heat_list() + fakes.script_heat_list(client=self.client) self.m.ReplayAll() list_text = self.shell('stack-list --show-owner') @@ -662,7 +693,8 @@ def test_parsable_error(self): "title": "Not Found" } - fakes.script_heat_error(jsonutils.dumps(resp_dict)) + fakes.script_heat_error(jsonutils.dumps(resp_dict), + client=self.client) self.m.ReplayAll() @@ -683,7 +715,7 @@ def test_parsable_verbose(self): "title": "Not Found" } - fakes.script_heat_error(jsonutils.dumps(resp_dict)) + fakes.script_heat_error(jsonutils.dumps(resp_dict), self.client) self.m.ReplayAll() @@ -695,7 +727,7 @@ def test_parsable_verbose(self): def test_parsable_malformed_error(self): self.register_keystone_auth_fixture() invalid_json = "ERROR: {Invalid JSON Error." - fakes.script_heat_error(invalid_json) + fakes.script_heat_error(invalid_json, client=self.client) self.m.ReplayAll() e = self.assertRaises(exc.HTTPException, self.shell, "stack-show bad") self.assertEqual("ERROR: " + invalid_json, str(e)) @@ -712,7 +744,8 @@ def test_parsable_malformed_error_missing_message(self): "title": "Not Found" } - fakes.script_heat_error(jsonutils.dumps(missing_message)) + fakes.script_heat_error(jsonutils.dumps(missing_message), + client=self.client) self.m.ReplayAll() e = self.assertRaises(exc.HTTPException, self.shell, "stack-show bad") @@ -731,7 +764,7 @@ def test_parsable_malformed_error_missing_traceback(self): "title": "Not Found" } - fakes.script_heat_error(jsonutils.dumps(resp_dict)) + fakes.script_heat_error(jsonutils.dumps(resp_dict), client=self.client) self.m.ReplayAll() exc.verbose = 1 @@ -753,8 +786,12 @@ def test_stack_show(self): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', 'GET').AndReturn(resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) self.m.ReplayAll() @@ -805,9 +842,14 @@ def _output_fake_response(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').MultipleTimes().AndReturn( - (resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', 'GET').MultipleTimes().AndReturn( + resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack/1').MultipleTimes().AndReturn( + (resp, resp_dict)) self.m.ReplayAll() @@ -836,8 +878,12 @@ def _error_output_fake_response(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', 'GET').AndReturn(resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) self.m.ReplayAll() @@ -905,8 +951,13 @@ def test_template_show_cfn(self): {'content-type': 'application/json'}, template_data) resp_dict = jsonutils.loads(template_data) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/template').AndReturn((resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/template', 'GET').AndReturn(resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack/template').AndReturn((resp, + resp_dict)) self.m.ReplayAll() @@ -934,8 +985,13 @@ def test_template_show_cfn_unicode(self): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/template').AndReturn((resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/template', 'GET').AndReturn(resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack/template').AndReturn((resp, + resp_dict)) self.m.ReplayAll() @@ -963,8 +1019,13 @@ def test_template_show_hot(self): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/template').AndReturn((resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/template', 'GET').AndReturn(resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack/template').AndReturn((resp, + resp_dict)) self.m.ReplayAll() @@ -995,10 +1056,20 @@ def _test_stack_preview(self, timeout=None, enable_rollback=False): {'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2', 'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'POST', '/stacks/preview', data=mox.IgnoreArg(), - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, resp_dict)) + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks/preview', 'POST', data=mox.IgnoreArg(), + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'POST', '/stacks/preview', data=mox.IgnoreArg(), + headers=headers + ).AndReturn((resp, resp_dict)) self.m.ReplayAll() @@ -1040,11 +1111,20 @@ def test_stack_create(self): 'Created', {'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'}, None) - http.HTTPClient.json_request( - 'POST', '/stacks', data=mox.IgnoreArg(), - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks', 'POST', data=mox.IgnoreArg(), + headers=headers).AndReturn(resp) + else: + self.client.json_request( + 'POST', '/stacks', data=mox.IgnoreArg(), + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') @@ -1072,11 +1152,21 @@ def test_stack_create_param_file(self): 'Created', {'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'}, None) - http.HTTPClient.json_request( - 'POST', '/stacks', data=mox.IgnoreArg(), - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks', 'POST', data=mox.IgnoreArg(), + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'POST', '/stacks', data=mox.IgnoreArg(), + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.StubOutWithMock(utils, 'read_url_content') url = 'file://%s/private_key.env' % TEST_VAR_DIR @@ -1109,11 +1199,21 @@ def test_stack_create_only_param_file(self): 'Created', {'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'}, None) - http.HTTPClient.json_request( - 'POST', '/stacks', data=mox.IgnoreArg(), - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks', 'POST', data=mox.IgnoreArg(), + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'POST', '/stacks', data=mox.IgnoreArg(), + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.StubOutWithMock(utils, 'read_url_content') url = 'file://%s/private_key.env' % TEST_VAR_DIR @@ -1158,11 +1258,21 @@ def test_stack_create_timeout(self): 'environment': {}, 'template': jsonutils.loads(template_data), 'timeout_mins': 123} - http.HTTPClient.json_request( - 'POST', '/stacks', data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks', 'POST', data=expected_data, + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'POST', '/stacks', data=expected_data, + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1205,12 +1315,23 @@ def test_stack_update_timeout(self): 'DBPassword': 'verybadpassword'}, 'timeout_mins': 123, 'disable_rollback': True} - http.HTTPClient.json_request( - 'PUT', '/stacks/teststack2/2', - data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2', 'PUT', + data=expected_data, + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'PUT', '/stacks/teststack2/2', + data=expected_data, + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1255,11 +1376,20 @@ def test_stack_create_url(self): '"InstanceType': 'm1.large', 'DBPassword': 'verybadpassword'}} - http.HTTPClient.json_request( - 'POST', '/stacks', data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks', 'POST', data=expected_data, + headers=headers).AndReturn(resp) + else: + self.client.json_request( + 'POST', '/stacks', data=expected_data, + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1290,22 +1420,38 @@ def test_stack_create_object(self): {}, template_data) - http.HTTPClient.raw_request( - 'GET', - 'http://no.where/container/minimal.template', - ).AndReturn(raw_resp) + if self.client == http.SessionClient: + self.client.request( + 'http://no.where/container/minimal.template', + 'GET' + ).AndReturn(raw_resp) + else: + self.client.raw_request( + 'GET', + 'http://no.where/container/minimal.template', + ).AndReturn(raw_resp) resp = fakes.FakeHTTPResponse( 201, 'Created', {'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'}, None) - http.HTTPClient.json_request( - 'POST', '/stacks', data=mox.IgnoreArg(), - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks', 'POST', data=mox.IgnoreArg(), + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'POST', '/stacks', data=mox.IgnoreArg(), + headers=headers + ).AndReturn((resp, None)) - fakes.script_heat_list() + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1346,11 +1492,16 @@ def test_stack_create_with_tags(self): 'environment': {}, 'template': jsonutils.loads(template_data), 'tags': 'tag1,tag2'} - http.HTTPClient.json_request( - 'POST', '/stacks', data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client == http.SessionClient: + self.client.request( + '/stacks', 'POST', data=expected_data, + headers={}).AndReturn(resp) + else: + http.HTTPClient.json_request( + 'POST', '/stacks', data=expected_data, + headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1409,10 +1560,18 @@ def test_stack_abandon(self): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(abandoned_stack)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) - http.HTTPClient.raw_request( - 'DELETE', '/stacks/teststack/1/abandon').AndReturn(abandoned_resp) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', 'GET').AndReturn(resp) + self.client.request( + '/stacks/teststack/1/abandon', + 'DELETE').AndReturn(abandoned_resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) + http.HTTPClient.raw_request( + 'DELETE', '/stacks/teststack/1/abandon').AndReturn( + abandoned_resp) self.m.ReplayAll() abandon_resp = self.shell('stack-abandon teststack/1') @@ -1455,10 +1614,18 @@ def test_stack_abandon_with_outputfile(self): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(abandoned_stack)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) - http.HTTPClient.raw_request( - 'DELETE', '/stacks/teststack/1/abandon').AndReturn(abandoned_resp) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', 'GET').AndReturn(resp) + self.client.request( + '/stacks/teststack/1/abandon', + 'DELETE').AndReturn(abandoned_resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) + http.HTTPClient.raw_request( + 'DELETE', '/stacks/teststack/1/abandon').AndReturn( + abandoned_resp) self.m.ReplayAll() @@ -1474,11 +1641,20 @@ def test_stack_adopt(self): 'Created', {'location': 'http://no.where/v1/tenant_id/stacks/teststack/1'}, None) - http.HTTPClient.json_request( - 'POST', '/stacks', data=mox.IgnoreArg(), - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks', 'POST', data=mox.IgnoreArg(), + headers=headers).AndReturn(resp) + else: + self.client.json_request( + 'POST', '/stacks', data=mox.IgnoreArg(), + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1522,12 +1698,23 @@ def test_stack_update_enable_rollback(self): 'Accepted', {}, 'The request is accepted for processing.') - http.HTTPClient.json_request( - 'PUT', '/stacks/teststack2/2', - data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2', 'PUT', + data=expected_data, + headers=headers + ).AndReturn((resp, None)) + else: + self.client.json_request( + 'PUT', '/stacks/teststack2/2', + data=expected_data, + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() update_text = self.shell( @@ -1563,12 +1750,24 @@ def test_stack_update_disable_rollback(self): 'Accepted', {}, 'The request is accepted for processing.') - http.HTTPClient.json_request( - 'PUT', '/stacks/teststack2', - data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2', 'PUT', + data=expected_data, + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'PUT', '/stacks/teststack2', + data=expected_data, + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() update_text = self.shell( @@ -1613,12 +1812,23 @@ def test_stack_update_rollback_default(self): 'Accepted', {}, 'The request is accepted for processing.') - http.HTTPClient.json_request( - 'PUT', '/stacks/teststack2', - data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp_update, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2', 'PUT', + data=expected_data, + headers=headers + ).AndReturn(resp_update) + else: + self.client.json_request( + 'PUT', '/stacks/teststack2', + data=expected_data, + headers=headers + ).AndReturn((resp_update, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() update_text = self.shell( @@ -1652,12 +1862,23 @@ def test_stack_update_with_existing_parameters(self): 'template': jsonutils.loads(template_data), 'parameters': {}, 'disable_rollback': False} - http.HTTPClient.json_request( - 'PATCH', '/stacks/teststack2/2', - data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2', 'PATCH', + data=expected_data, + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'PATCH', '/stacks/teststack2/2', + data=expected_data, + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1691,12 +1912,23 @@ def test_stack_update_with_patched_existing_parameters(self): 'template': jsonutils.loads(template_data), 'parameters': {'"KeyPairName': 'updated_key"'}, 'disable_rollback': False} - http.HTTPClient.json_request( - 'PATCH', '/stacks/teststack2/2', - data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2', 'PATCH', + data=expected_data, + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'PATCH', '/stacks/teststack2/2', + data=expected_data, + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1734,12 +1966,23 @@ def test_stack_update_with_existing_and_default_parameters(self): 'DBPassword', 'KeyPairName', 'LinuxDistribution'], 'disable_rollback': False} - http.HTTPClient.json_request( - 'PATCH', '/stacks/teststack2/2', - data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2', 'PATCH', + data=expected_data, + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'PATCH', '/stacks/teststack2/2', + data=expected_data, + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1781,12 +2024,23 @@ def test_stack_update_with_patched_and_default_parameters(self): 'DBPassword', 'KeyPairName', 'LinuxDistribution'], 'disable_rollback': False} - http.HTTPClient.json_request( - 'PATCH', '/stacks/teststack2/2', - data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2', 'PATCH', + data=expected_data, + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'PATCH', '/stacks/teststack2/2', + data=expected_data, + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1827,12 +2081,17 @@ def test_stack_update_with_tags(self): 'parameters': {'"KeyPairName': 'updated_key"'}, 'disable_rollback': False, 'tags': 'tag1,tag2'} - http.HTTPClient.json_request( - 'PATCH', '/stacks/teststack2/2', - data=expected_data, - headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2', 'PATCH', + data=expected_data, headers={}).AndReturn(resp) + else: + http.HTTPClient.json_request( + 'PATCH', '/stacks/teststack2/2', + data=expected_data, + headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1860,10 +2119,13 @@ def test_stack_delete(self): 'No Content', {}, None) - http.HTTPClient.raw_request( - 'DELETE', '/stacks/teststack2/2', - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2', 'DELETE').AndReturn(resp) + else: + self.client.raw_request( + 'DELETE', '/stacks/teststack2/2').AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) self.m.ReplayAll() @@ -1885,13 +2147,17 @@ def test_stack_delete_multiple(self): 'No Content', {}, None) - http.HTTPClient.raw_request( - 'DELETE', '/stacks/teststack1/1', - ).AndReturn((resp, None)) - http.HTTPClient.raw_request( - 'DELETE', '/stacks/teststack2/2', - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack1/1', 'DELETE').AndReturn(resp) + self.client.request( + '/stacks/teststack2/2', 'DELETE').AndReturn(resp) + else: + self.client.raw_request( + 'DELETE', '/stacks/teststack1/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() @@ -1918,7 +2184,10 @@ def test_build_info(self): headers = {'content-type': 'application/json'} http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) response = (http_resp, resp_dict) - http.HTTPClient.json_request('GET', '/build_info').AndReturn(response) + if self.client == http.SessionClient: + self.client.request('/build_info', 'GET').AndReturn(http_resp) + else: + self.client.json_request('GET', '/build_info').AndReturn(response) self.m.ReplayAll() @@ -1954,16 +2223,29 @@ def test_stack_snapshot(self): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request('GET', '/stacks/teststack/1').AndReturn( - (fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)), stack_dict)) - http.HTTPClient.json_request( - 'POST', - '/stacks/teststack/1/snapshots', - data={}).AndReturn((resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request('/stacks/teststack/1', 'GET').AndReturn( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict))) + self.client.request( + '/stacks/teststack/1/snapshots', + 'POST', + data={}).AndReturn(resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn( + (fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict)), stack_dict)) + http.HTTPClient.json_request( + 'POST', + '/stacks/teststack/1/snapshots', + data={}).AndReturn((resp, resp_dict)) self.m.ReplayAll() resp = self.shell('stack-snapshot teststack/1') @@ -1998,11 +2280,20 @@ def test_snapshot_list(self): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((stack_resp, stack_dict)) - http.HTTPClient.json_request( - 'GET', - '/stacks/teststack/1/snapshots').AndReturn((resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', + 'GET').AndReturn(stack_resp) + self.client.request( + '/stacks/teststack/1/snapshots', + 'GET').AndReturn(resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn((stack_resp, + stack_dict)) + http.HTTPClient.json_request( + 'GET', + '/stacks/teststack/1/snapshots').AndReturn((resp, resp_dict)) self.m.ReplayAll() list_text = self.shell('snapshot-list teststack/1') @@ -2042,15 +2333,27 @@ def test_snapshot_show(self): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request('GET', '/stacks/teststack/1').AndReturn(( - fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)), stack_dict)) - http.HTTPClient.json_request( - 'GET', - '/stacks/teststack/1/snapshots/2').AndReturn((resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request('/stacks/teststack/1', 'GET').AndReturn( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict))) + self.client.request( + '/stacks/teststack/1/snapshots/2', + 'GET').AndReturn(resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn(( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict)), stack_dict)) + http.HTTPClient.json_request( + 'GET', + '/stacks/teststack/1/snapshots/2').AndReturn((resp, resp_dict)) self.m.ReplayAll() resp = self.shell('snapshot-show teststack/1 2') @@ -2081,11 +2384,18 @@ def test_snapshot_delete(self): 'No Content', {}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((resp, stack_dict)) - http.HTTPClient.raw_request( - 'DELETE', - '/stacks/teststack/1/snapshots/2').AndReturn(second_resp) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', 'GET').AndReturn(resp) + self.client.request( + '/stacks/teststack/1/snapshots/2', + 'DELETE').AndReturn(second_resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn((resp, stack_dict)) + http.HTTPClient.raw_request( + 'DELETE', + '/stacks/teststack/1/snapshots/2').AndReturn(second_resp) self.m.ReplayAll() resp = self.shell('snapshot-delete teststack/1 2') @@ -2111,11 +2421,20 @@ def test_stack_restore(self): 'No Content', {'content-type': 'application/json'}, jsonutils.dumps({})) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((stack_resp, stack_dict)) - http.HTTPClient.json_request( - 'POST', - '/stacks/teststack/1/snapshots/2/restore').AndReturn((no_resp, {})) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', 'GET').AndReturn(stack_resp) + self.client.request( + '/stacks/teststack/1/snapshots/2/restore', + 'POST').AndReturn(no_resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn((stack_resp, + stack_dict)) + http.HTTPClient.json_request( + 'POST', + '/stacks/teststack/1/snapshots/2/restore').AndReturn((no_resp, + {})) self.m.ReplayAll() resp = self.shell('stack-restore teststack/1 2') @@ -2136,11 +2455,17 @@ def test_stack_cancel_update(self): 'Accepted', {}, 'The request is accepted for processing.') - http.HTTPClient.json_request( - 'POST', '/stacks/teststack2/actions', - data=expected_data - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/actions', 'POST', + data=expected_data).AndReturn(resp) + fakes.script_heat_list(client=self.client) + else: + http.HTTPClient.json_request( + 'POST', '/stacks/teststack2/actions', + data=expected_data + ).AndReturn((resp, None)) + fakes.script_heat_list() self.m.ReplayAll() @@ -2163,11 +2488,17 @@ def test_stack_check(self): 'Accepted', {}, 'The request is accepted for processing.') - http.HTTPClient.json_request( - 'POST', '/stacks/teststack2/actions', - data=expected_data - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/actions', 'POST', + data=expected_data).AndReturn(resp) + fakes.script_heat_list(client=self.client) + else: + http.HTTPClient.json_request( + 'POST', '/stacks/teststack2/actions', + data=expected_data + ).AndReturn((resp, None)) + fakes.script_heat_list() self.m.ReplayAll() @@ -2190,11 +2521,18 @@ def test_stack_suspend(self): 'Accepted', {}, 'The request is accepted for processing.') - http.HTTPClient.json_request( - 'POST', '/stacks/teststack2/actions', - data=expected_data - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/actions', 'POST', + data=expected_data + ).AndReturn(resp) + fakes.script_heat_list(client=self.client) + else: + http.HTTPClient.json_request( + 'POST', '/stacks/teststack2/actions', + data=expected_data + ).AndReturn((resp, None)) + fakes.script_heat_list() self.m.ReplayAll() @@ -2217,11 +2555,18 @@ def test_stack_resume(self): 'Accepted', {}, 'The request is accepted for processing.') - http.HTTPClient.json_request( - 'POST', '/stacks/teststack2/actions', - data=expected_data - ).AndReturn((resp, None)) - fakes.script_heat_list() + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/actions', 'POST', + data=expected_data + ).AndReturn(resp) + fakes.script_heat_list(client=self.client) + else: + http.HTTPClient.json_request( + 'POST', '/stacks/teststack2/actions', + data=expected_data + ).AndReturn((resp, None)) + fakes.script_heat_list() self.m.ReplayAll() @@ -2288,11 +2633,11 @@ def test_event_list(self): jsonutils.dumps(resp_dict)) stack_id = 'teststack/1' resource_name = 'testresource/1' - http.HTTPClient.json_request( - 'GET', '/stacks/%s/resources/%s/events?sort_dir=asc' % ( + http.SessionClient.request( + '/stacks/%s/resources/%s/events?sort_dir=asc' % ( parse.quote(stack_id, ''), parse.quote(encodeutils.safe_encode( - resource_name), ''))).AndReturn((resp, resp_dict)) + resource_name), '')), 'GET').AndReturn(resp) self.m.ReplayAll() @@ -2353,9 +2698,15 @@ def test_stack_event_list_log(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) stack_id = 'teststack/1' - http.HTTPClient.json_request('GET', - '/stacks/%s/events?sort_dir=asc' % - stack_id).AndReturn((resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/%s/events?sort_dir=asc' % stack_id, + 'GET').AndReturn(resp) + else: + http.HTTPClient.json_request( + 'GET', + '/stacks/%s/events?sort_dir=asc' % + stack_id).AndReturn((resp, resp_dict)) self.m.ReplayAll() @@ -2396,14 +2747,14 @@ def test_event_show(self): jsonutils.dumps(resp_dict)) stack_id = 'teststack/1' resource_name = 'testresource/1' - http.HTTPClient.json_request( - 'GET', '/stacks/%s/resources/%s/events/%s' % + http.SessionClient.request( + '/stacks/%s/resources/%s/events/%s' % ( parse.quote(stack_id, ''), parse.quote(encodeutils.safe_encode( resource_name), ''), parse.quote(self.event_id_one, '') - )).AndReturn((resp, resp_dict)) + ), 'GET').AndReturn(resp) self.m.ReplayAll() @@ -2477,9 +2828,17 @@ def test_shell_nested_depth_zero(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) stack_id = 'teststack/1' - http.HTTPClient.json_request( - 'GET', '/stacks/%s/events?sort_dir=asc' % ( - stack_id)).AndReturn((resp, resp_dict)) + + if self.client == http.SessionClient: + self.client.request( + '/stacks/%s/events?sort_dir=asc' % stack_id, + 'GET' + ).AndReturn(resp) + else: + http.HTTPClient.json_request( + 'GET', + '/stacks/%s/events?sort_dir=asc' % stack_id + ).AndReturn((resp, resp_dict)) self.m.ReplayAll() list_text = self.shell('event-list %s --nested-depth 0' % stack_id) required = ['id', 'eventid1', 'eventid2'] @@ -2497,9 +2856,13 @@ def _stub_event_list_response(self, stack_id, nested_id, timestamps): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(ev_resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/%s/events?sort_dir=asc' % ( - stack_id)).AndReturn((ev_resp, ev_resp_dict)) + if self.client == http.SessionClient: + self.client.request('/stacks/%s/events?sort_dir=asc' % stack_id, + 'GET').AndReturn(ev_resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/%s/events?sort_dir=asc' % ( + stack_id)).AndReturn((ev_resp, ev_resp_dict)) # Stub resources for parent, including one nested res_resp_dict = {"resources": [ @@ -2514,9 +2877,15 @@ def _stub_event_list_response(self, stack_id, nested_id, timestamps): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(res_resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/%s/resources' % ( - stack_id)).AndReturn((res_resp, res_resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/%s/resources' % ( + stack_id), 'GET').AndReturn(res_resp) + + else: + http.HTTPClient.json_request( + 'GET', '/stacks/%s/resources' % ( + stack_id)).AndReturn((res_resp, res_resp_dict)) # Stub the events for the nested stack nev_resp_dict = {"events": [{"id": 'n_eventid1', @@ -2528,9 +2897,14 @@ def _stub_event_list_response(self, stack_id, nested_id, timestamps): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(nev_resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/%s/events?sort_dir=asc' % ( - nested_id)).AndReturn((nev_resp, nev_resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/%s/events?sort_dir=asc' % ( + nested_id), 'GET').AndReturn(nev_resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/%s/events?sort_dir=asc' % ( + nested_id)).AndReturn((nev_resp, nev_resp_dict)) def test_shell_nested_depth(self): self.register_keystone_auth_fixture() @@ -2616,8 +2990,12 @@ def _stub_stack_response(self, stack_id, action='CREATE', 'OK', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', 'GET').AndReturn(resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) def _stub_responses(self, stack_id, nested_id, action='CREATE'): action_reason = 'Stack %s started' % action @@ -2641,9 +3019,14 @@ def _stub_responses(self, stack_id, nested_id, action='CREATE'): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(ev_resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/%s/events?sort_dir=asc' % ( - stack_id)).AndReturn((ev_resp, ev_resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/%s/events?sort_dir=asc' % (stack_id), + 'GET').AndReturn(ev_resp) + else: + self.client.json_request( + 'GET', '/stacks/%s/events?sort_dir=asc' % ( + stack_id)).AndReturn((ev_resp, ev_resp_dict)) # Stub resources for parent, including one nested res_resp_dict = {"resources": [ @@ -2658,9 +3041,14 @@ def _stub_responses(self, stack_id, nested_id, action='CREATE'): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(res_resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/%s/resources' % ( - stack_id)).AndReturn((res_resp, res_resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/%s/resources' % (stack_id), + 'GET').AndReturn(res_resp) + else: + self.client.json_request( + 'GET', '/stacks/%s/resources' % ( + stack_id)).AndReturn((res_resp, res_resp_dict)) # Stub the events for the nested stack nev_resp_dict = {"events": [{"id": 'n_eventid1', @@ -2677,9 +3065,14 @@ def _stub_responses(self, stack_id, nested_id, action='CREATE'): 'OK', {'content-type': 'application/json'}, jsonutils.dumps(nev_resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/%s/events?sort_dir=asc' % ( - nested_id)).AndReturn((nev_resp, nev_resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/%s/events?sort_dir=asc' % (nested_id), + 'GET').AndReturn(nev_resp) + else: + self.client.json_request( + 'GET', '/stacks/%s/events?sort_dir=asc' % ( + nested_id)).AndReturn((nev_resp, nev_resp_dict)) def test_hook_poll_pre_create(self): self.register_keystone_auth_fixture() @@ -2782,9 +3175,9 @@ def _test_resource_list(self, with_resource_name): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) stack_id = 'teststack/1' - http.HTTPClient.json_request( - 'GET', '/stacks/%s/resources' % ( - stack_id)).AndReturn((resp, resp_dict)) + http.SessionClient.request( + '/stacks/%s/resources' % ( + stack_id), 'GET').AndReturn(resp) self.m.ReplayAll() @@ -2825,9 +3218,9 @@ def test_resource_list_empty(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) stack_id = 'teststack/1' - http.HTTPClient.json_request( - 'GET', '/stacks/%s/resources' % ( - stack_id)).AndReturn((resp, resp_dict)) + http.SessionClient.request( + '/stacks/%s/resources' % ( + stack_id), 'GET').AndReturn(resp) self.m.ReplayAll() @@ -2856,9 +3249,9 @@ def test_resource_list_nested(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) stack_id = 'teststack/1' - http.HTTPClient.json_request( - 'GET', '/stacks/%s/resources?nested_depth=99' % ( - stack_id)).AndReturn((resp, resp_dict)) + http.SessionClient.request( + '/stacks/%s/resources?nested_depth=99' % ( + stack_id), 'GET').AndReturn(resp) self.m.ReplayAll() @@ -2896,13 +3289,13 @@ def test_resource_show(self): jsonutils.dumps(resp_dict)) stack_id = 'teststack/1' resource_name = 'aResource' - http.HTTPClient.json_request( - 'GET', '/stacks/%s/resources/%s' % + http.SessionClient.request( + '/stacks/%s/resources/%s' % ( parse.quote(stack_id, ''), parse.quote(encodeutils.safe_encode( resource_name), '') - )).AndReturn((resp, resp_dict)) + ), 'GET').AndReturn(resp) self.m.ReplayAll() @@ -2941,14 +3334,15 @@ def test_resource_signal(self): '') stack_id = 'teststack/1' resource_name = 'aResource' - http.HTTPClient.json_request( - 'POST', '/stacks/%s/resources/%s/signal' % + http.SessionClient.request( + '/stacks/%s/resources/%s/signal' % ( parse.quote(stack_id, ''), parse.quote(encodeutils.safe_encode( resource_name), '') ), - data={'message': 'Content'}).AndReturn((resp, '')) + 'POST', + data={'message': 'Content'}).AndReturn(resp) self.m.ReplayAll() @@ -2966,13 +3360,13 @@ def test_resource_signal_no_data(self): '') stack_id = 'teststack/1' resource_name = 'aResource' - http.HTTPClient.json_request( - 'POST', '/stacks/%s/resources/%s/signal' % + http.SessionClient.request( + '/stacks/%s/resources/%s/signal' % ( parse.quote(stack_id, ''), parse.quote(encodeutils.safe_encode( resource_name), '') - ), data=None).AndReturn((resp, '')) + ), 'POST', data=None).AndReturn(resp) self.m.ReplayAll() @@ -3029,14 +3423,15 @@ def test_resource_signal_data_file(self): '') stack_id = 'teststack/1' resource_name = 'aResource' - http.HTTPClient.json_request( - 'POST', '/stacks/%s/resources/%s/signal' % + http.SessionClient.request( + '/stacks/%s/resources/%s/signal' % ( parse.quote(stack_id, ''), parse.quote(encodeutils.safe_encode( resource_name), '') ), - data={'message': 'Content'}).AndReturn((resp, '')) + 'POST', + data={'message': 'Content'}).AndReturn(resp) self.m.ReplayAll() @@ -3066,10 +3461,10 @@ def test_resource_type_template_yaml(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'GET', '/resource_types/OS%3A%3ANova%3A%3AKeyPair/template' - '?template_type=hot' - ).AndReturn((resp, resp_dict)) + http.SessionClient.request( + '/resource_types/OS%3A%3ANova%3A%3AKeyPair/template' + '?template_type=hot', 'GET' + ).AndReturn(resp) self.m.ReplayAll() @@ -3096,10 +3491,10 @@ def test_resource_type_template_json(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) - http.HTTPClient.json_request( - 'GET', '/resource_types/OS%3A%3ANova%3A%3AKeyPair/template' - '?template_type=cfn' - ).AndReturn((resp, resp_dict)) + http.SessionClient.request( + '/resource_types/OS%3A%3ANova%3A%3AKeyPair/template' + '?template_type=cfn', 'GET' + ).AndReturn(resp) self.m.ReplayAll() @@ -3193,7 +3588,6 @@ def test_config_create(self): resp_string = jsonutils.dumps(resp_dict) headers = {'content-type': 'application/json'} http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) - response = (http_resp, resp_dict) self.m.StubOutWithMock(request, 'urlopen') request.urlopen('file:///tmp/defn').AndReturn( @@ -3201,10 +3595,10 @@ def test_config_create(self): request.urlopen('file:///tmp/config_script').AndReturn( six.StringIO('the config script')) - http.HTTPClient.json_request( - 'POST', '/validate', data=validate_template).AndReturn(response) - http.HTTPClient.json_request( - 'POST', '/software_configs', data=create_dict).AndReturn(response) + http.SessionClient.request( + '/validate', 'POST', data=validate_template).AndReturn(http_resp) + http.SessionClient.request( + '/software_configs', 'POST', data=create_dict).AndReturn(http_resp) self.m.ReplayAll() @@ -3226,13 +3620,12 @@ def test_config_show(self): resp_string = jsonutils.dumps(resp_dict) headers = {'content-type': 'application/json'} http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) - response = (http_resp, resp_dict) - http.HTTPClient.json_request( - 'GET', '/software_configs/abcd').AndReturn(response) - http.HTTPClient.json_request( - 'GET', '/software_configs/abcd').AndReturn(response) - http.HTTPClient.json_request( - 'GET', '/software_configs/abcde').AndRaise(exc.HTTPNotFound()) + http.SessionClient.request( + '/software_configs/abcd', 'GET').AndReturn(http_resp) + http.SessionClient.request( + '/software_configs/abcd', 'GET').AndReturn(http_resp) + http.SessionClient.request( + '/software_configs/abcde', 'GET').AndRaise(exc.HTTPNotFound()) self.m.ReplayAll() @@ -3259,15 +3652,14 @@ def test_config_delete(self): self.register_keystone_auth_fixture() headers = {'content-type': 'application/json'} http_resp = fakes.FakeHTTPResponse(204, 'OK', headers, None) - response = (http_resp, '') - http.HTTPClient.raw_request( - 'DELETE', '/software_configs/abcd').AndReturn(response) - http.HTTPClient.raw_request( - 'DELETE', '/software_configs/qwer').AndReturn(response) - http.HTTPClient.raw_request( - 'DELETE', '/software_configs/abcd').AndRaise(exc.HTTPNotFound()) - http.HTTPClient.raw_request( - 'DELETE', '/software_configs/qwer').AndRaise(exc.HTTPNotFound()) + http.SessionClient.request( + '/software_configs/abcd', 'DELETE').AndReturn(http_resp) + http.SessionClient.request( + '/software_configs/qwer', 'DELETE').AndReturn(http_resp) + http.SessionClient.request( + '/software_configs/abcd', 'DELETE').AndRaise(exc.HTTPNotFound()) + http.SessionClient.request( + '/software_configs/qwer', 'DELETE').AndRaise(exc.HTTPNotFound()) self.m.ReplayAll() @@ -3280,6 +3672,7 @@ class ShellTestDeployment(ShellBase): def setUp(self): super(ShellTestDeployment, self).setUp() + self.client = http.SessionClient self._set_fake_env() def _set_fake_env(self): @@ -3308,10 +3701,18 @@ def test_deploy_show(self): headers = {'content-type': 'application/json'} http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) response = (http_resp, resp_dict) - http.HTTPClient.json_request( - 'GET', '/software_deployments/defg').AndReturn(response) - http.HTTPClient.json_request( - 'GET', '/software_deployments/defgh').AndRaise(exc.HTTPNotFound()) + if self.client == http.SessionClient: + self.client.request( + '/software_deployments/defg', 'GET').AndReturn(http_resp) + self.client.request( + '/software_deployments/defgh', 'GET').AndRaise( + exc.HTTPNotFound()) + else: + self.client.json_request( + 'GET', '/software_deployments/defg').AndReturn(response) + self.client.json_request( + 'GET', '/software_deployments/defgh').AndRaise( + exc.HTTPNotFound()) self.m.ReplayAll() @@ -3337,16 +3738,30 @@ def test_deploy_delete(self): headers = {'content-type': 'application/json'} http_resp = fakes.FakeHTTPResponse(204, 'OK', headers, None) response = (http_resp, '') - http.HTTPClient.raw_request( - 'DELETE', '/software_deployments/defg').AndReturn(response) - http.HTTPClient.raw_request( - 'DELETE', '/software_deployments/qwer').AndReturn(response) - http.HTTPClient.raw_request( - 'DELETE', - '/software_deployments/defg').AndRaise(exc.HTTPNotFound()) - http.HTTPClient.raw_request( - 'DELETE', - '/software_deployments/qwer').AndRaise(exc.HTTPNotFound()) + if self.client == http.SessionClient: + self.client.request( + '/software_deployments/defg', + 'DELETE').AndReturn(http_resp) + self.client.request( + '/software_deployments/qwer', + 'DELETE').AndReturn(http_resp) + self.client.request( + '/software_deployments/defg', + 'DELETE').AndRaise(exc.HTTPNotFound()) + self.client.request( + '/software_deployments/qwer', + 'DELETE').AndRaise(exc.HTTPNotFound()) + else: + self.client.raw_request( + 'DELETE', '/software_deployments/defg').AndReturn(response) + self.client.raw_request( + 'DELETE', '/software_deployments/qwer').AndReturn(response) + self.client.raw_request( + 'DELETE', + '/software_deployments/defg').AndRaise(exc.HTTPNotFound()) + self.client.raw_request( + 'DELETE', + '/software_deployments/qwer').AndRaise(exc.HTTPNotFound()) self.m.ReplayAll() @@ -3365,8 +3780,14 @@ def test_deploy_metadata(self): headers = {'content-type': 'application/json'} http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) response = (http_resp, resp_dict) - http.HTTPClient.json_request( - 'GET', '/software_deployments/metadata/aaaa').AndReturn(response) + if self.client == http.SessionClient: + self.client.request( + '/software_deployments/metadata/aaaa', + 'GET').AndReturn(http_resp) + else: + self.client.json_request( + 'GET', '/software_deployments/metadata/aaaa').AndReturn( + response) self.m.ReplayAll() @@ -3404,11 +3825,20 @@ def test_deploy_output_show(self): headers = {'content-type': 'application/json'} http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) response = (http_resp, resp_dict) - http.HTTPClient.json_request( - 'GET', '/software_deployments/defgh').AndRaise(exc.HTTPNotFound()) - http.HTTPClient.json_request( - 'GET', '/software_deployments/defg').MultipleTimes().AndReturn( - response) + if self.client == http.SessionClient: + self.client.request( + '/software_deployments/defgh', 'GET').AndRaise( + exc.HTTPNotFound()) + self.client.request( + '/software_deployments/defg', 'GET').MultipleTimes().AndReturn( + http_resp) + else: + self.client.json_request( + 'GET', '/software_deployments/defgh').AndRaise( + exc.HTTPNotFound()) + self.client.json_request( + 'GET', '/software_deployments/defg').MultipleTimes().AndReturn( + response) self.m.ReplayAll() @@ -3472,8 +3902,9 @@ def test_build_info(self): resp_string = jsonutils.dumps(resp_dict) headers = {'content-type': 'application/json'} http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) - response = (http_resp, resp_dict) - http.HTTPClient.json_request('GET', '/build_info').AndReturn(response) + response = http_resp + http.SessionClient.request( + '/build_info', 'GET').AndReturn(response) self.m.ReplayAll() @@ -3525,6 +3956,7 @@ class ShellTestStandaloneToken(ShellTestUserPass): def setUp(self): self.token = 'a_token' super(ShellTestStandaloneToken, self).setUp() + self.client = http.HTTPClient def _set_fake_env(self): fake_env = { @@ -3613,6 +4045,8 @@ def setUp(self): super(MockShellBase, self).setUp() self.jreq_mock = self.patch( 'heatclient.common.http.HTTPClient.json_request') + self.session_jreq_mock = self.patch( + 'heatclient.common.http.SessionClient.request') # Some tests set exc.verbose = 1, so reset on cleanup def unset_exc_verbose(): @@ -3650,6 +4084,7 @@ def _set_fake_env(self): def test_stack_list_with_args(self): self.register_keystone_auth_fixture() self.jreq_mock.return_value = fakes.mock_script_heat_list() + self.session_jreq_mock.return_value = fakes.mock_script_heat_list()[0] list_text = self.shell('stack-list' ' --limit 2' @@ -3675,8 +4110,12 @@ def test_stack_list_with_args(self): self.assertRegexpMatches(list_text, r) self.assertNotRegexpMatches(list_text, 'parent') - self.assertEqual(1, self.jreq_mock.call_count) - method, url = self.jreq_mock.call_args[0] + if self.jreq_mock.call_args is None: + self.assertEqual(1, self.session_jreq_mock.call_count) + url, method = self.session_jreq_mock.call_args[0] + else: + self.assertEqual(1, self.jreq_mock.call_count) + method, url = self.jreq_mock.call_args[0] self.assertEqual('GET', method) base_url, query_params = utils.parse_query_url(url) self.assertEqual('/stacks', base_url) @@ -3774,7 +4213,7 @@ def _test_error_case(self, code, message): 'test reason', {'content-type': 'application/json'}, resp_string) - (http.HTTPClient.json_request('GET', '/services'). + (http.SessionClient.request('/services', 'GET'). AndRaise(exc.from_response(resp))) exc.verbose = 1 @@ -3802,8 +4241,7 @@ def test_service_list(self): resp_string = jsonutils.dumps(resp_dict) headers = {} http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) - response = (http_resp, resp_dict) - http.HTTPClient.json_request('GET', '/services').AndReturn(response) + http.SessionClient.request('/services', 'GET').AndReturn(http_resp) self.m.ReplayAll() services_text = self.shell('service-list') diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 84d751f2..7ad0894c 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -24,6 +24,7 @@ from heatclient.common import deployment_utils from heatclient.common import event_utils +from heatclient.common import http from heatclient.common import template_format from heatclient.common import template_utils from heatclient.common import utils @@ -41,7 +42,11 @@ def _authenticated_fetcher(hc): """A wrapper around the heat client object to fetch a template. """ def _do(*args, **kwargs): - return hc.http_client.raw_request(*args, **kwargs).content + if isinstance(hc.http_client, http.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 From e7778a61b5a4db79ab1ea30a832520653f9dd956 Mon Sep 17 00:00:00 2001 From: Peter Razumovsky Date: Thu, 30 Apr 2015 12:17:36 +0300 Subject: [PATCH 003/553] Add tests for SessionClient Change-Id: I22913a7277a8eacfe6bfdb947ff7d6693f527330 Closes-bug: #1427596 --- heatclient/tests/unit/test_common_http.py | 205 ++++++++++++++++++---- 1 file changed, 172 insertions(+), 33 deletions(-) diff --git a/heatclient/tests/unit/test_common_http.py b/heatclient/tests/unit/test_common_http.py index d1e288f3..81b15999 100644 --- a/heatclient/tests/unit/test_common_http.py +++ b/heatclient/tests/unit/test_common_http.py @@ -16,17 +16,17 @@ import os import socket + +from oslo_serialization import jsonutils import requests +import six import testtools from heatclient.common import http from heatclient.common import utils from heatclient import exc from heatclient.tests.unit import fakes -from heatclient.tests.unit import test_shell from keystoneclient import adapter -from keystoneclient.auth.identity import v2 as ks_v2_auth -from keystoneclient import session from mox3 import mox @@ -662,45 +662,184 @@ def test_curl_log_i18n_headers(self): client.log_curl_request("GET", '', kwargs=kwargs) -class SessionClientTest(test_shell.TestCase, testtools.TestCase): +class SessionClientTest(testtools.TestCase): def setUp(self): super(SessionClientTest, self).setUp() - self.register_keystone_auth_fixture() - self.auth_session = session.Session() - self.auth_plugin = ks_v2_auth.Password(test_shell.V2_URL, 'xx', 'xx') - - @mock.patch.object(adapter.LegacyJsonAdapter, 'request') - def test_session_raw_request(self, mock_request): - mock_request.return_value = (fakes.FakeHTTPResponse( - 200, 'OK', {'content-type': 'application/octet-stream'}, ''), '') - - client = http.SessionClient(session=self.auth_session, - auth=self.auth_plugin) - resp = client.request(method='GET', url='') - self.assertEqual(200, resp.status_code) - self.assertEqual('', ''.join([x for x in resp.content])) + self.request = mock.patch.object(adapter.LegacyJsonAdapter, + 'request').start() + + def test_session_simple_request(self): + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/octet-stream'}, + '') + self.request.return_value = (resp, '') + + client = http.SessionClient(session=mock.ANY, + auth=mock.ANY) + response = client.request(method='GET', url='') + self.assertEqual(200, response.status_code) + self.assertEqual('', ''.join([x for x in response.content])) - @mock.patch.object(adapter.LegacyJsonAdapter, 'request') - def test_session_json_request(self, mock_request): - mock_request.return_value = (fakes.FakeHTTPResponse( - 200, 'OK', {'content-type': 'application/json'}, '{}'), {}) + def test_session_json_request(self): + fake = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps({'some': 'body'})) + self.request.return_value = (fake, {}) - client = http.SessionClient(session=self.auth_session, - auth=self.auth_plugin) + client = http.SessionClient(session=mock.ANY, + auth=mock.ANY) resp = client.request('', 'GET') - body = utils.get_response_body(resp) self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) + self.assertEqual({'some': 'body'}, resp.json()) - @mock.patch.object(adapter.LegacyJsonAdapter, 'request') - def test_404_error_response(self, mock_request): - mock_request.return_value = (fakes.FakeHTTPResponse( - 404, 'OK', {'content-type': 'application/octet-stream'}, ''), '') + def test_404_error_response(self): + fake = fakes.FakeHTTPResponse( + 404, + 'FAIL', + {'content-type': 'application/octet-stream'}, + '') + self.request.return_value = (fake, '') - client = http.SessionClient(session=self.auth_session, - auth=self.auth_plugin) + client = http.SessionClient(session=mock.ANY, + auth=mock.ANY) e = self.assertRaises(exc.HTTPNotFound, client.request, '', 'GET') # Assert that the raised exception can be converted to string - self.assertIsNotNone(str(e)) + self.assertIsNotNone(six.text_type(e)) + + def test_302_location(self): + fake1 = fakes.FakeHTTPResponse( + 302, + 'OK', + {'location': 'http://no.where/ishere'}, + '' + ) + fake2 = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps({'Mount': 'Fuji'}) + ) + self.request.side_effect = [ + (fake1, ''), (fake2, jsonutils.dumps({'Mount': 'Fuji'}))] + + client = http.SessionClient(session=mock.ANY, + auth=mock.ANY, + endpoint_override='http://no.where/') + resp = client.request('', 'GET') + + self.assertEqual(200, resp.status_code) + self.assertEqual({'Mount': 'Fuji'}, utils.get_response_body(resp)) + + self.assertEqual(('', 'GET'), self.request.call_args_list[0][0]) + self.assertEqual(('ishere', 'GET'), self.request.call_args_list[1][0]) + for call in self.request.call_args_list: + self.assertEqual({'user_agent': 'python-heatclient', + 'raise_exc': False}, call[1]) + + def test_302_location_no_endpoint(self): + fake1 = fakes.FakeHTTPResponse( + 302, + 'OK', + {'location': 'http://no.where/ishere'}, + '' + ) + fake2 = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps({'Mount': 'Fuji'}) + ) + self.request.side_effect = [ + (fake1, ''), (fake2, jsonutils.dumps({'Mount': 'Fuji'}))] + + client = http.SessionClient(session=mock.ANY, + auth=mock.ANY) + resp = client.request('', 'GET') + + self.assertEqual(200, resp.status_code) + self.assertEqual({'Mount': 'Fuji'}, utils.get_response_body(resp)) + + self.assertEqual(('', 'GET'), self.request.call_args_list[0][0]) + self.assertEqual(('http://no.where/ishere', + 'GET'), self.request.call_args_list[1][0]) + for call in self.request.call_args_list: + self.assertEqual({'user_agent': 'python-heatclient', + 'raise_exc': False}, call[1]) + + def test_302_no_location(self): + fake = fakes.FakeHTTPResponse( + 302, + 'OK', + {}, + '' + ) + self.request.side_effect = [(fake, '')] + + client = http.SessionClient(session=mock.ANY, + auth=mock.ANY) + e = self.assertRaises(exc.InvalidEndpoint, + client.request, '', 'GET') + self.assertEqual("Location not returned with 302", six.text_type(e)) + + def test_300_error_response(self): + fake = fakes.FakeHTTPResponse( + 300, + 'FAIL', + {'content-type': 'application/octet-stream'}, + '') + self.request.return_value = (fake, '') + + client = http.SessionClient(session=mock.ANY, + auth=mock.ANY) + e = self.assertRaises(exc.HTTPMultipleChoices, + client.request, '', 'GET') + # Assert that the raised exception can be converted to string + self.assertIsNotNone(six.text_type(e)) + + def test_kwargs(self): + fake = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + {} + ) + kwargs = dict(endpoint_override='http://no.where/', + data='some_data') + + client = http.SessionClient(mock.ANY) + + self.request.return_value = (fake, {}) + + resp = client.request('', 'GET', **kwargs) + + self.assertEqual({'endpoint_override': 'http://no.where/', + 'json': 'some_data', + 'user_agent': 'python-heatclient', + 'raise_exc': False}, self.request.call_args[1]) + self.assertEqual(200, resp.status_code) + + def test_methods(self): + fake = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + {} + ) + self.request.return_value = (fake, {}) + + client = http.SessionClient(mock.ANY) + methods = [client.get, client.put, client.post, client.patch, + client.delete, client.head] + for method in methods: + resp = method('') + self.assertEqual(200, resp.status_code) + + def test_credentials_headers(self): + client = http.SessionClient(mock.ANY) + self.assertEqual({}, client.credentials_headers()) From 20adfe37802992d07de54d36f480f050d2d618b4 Mon Sep 17 00:00:00 2001 From: Tetiana Lashchova Date: Wed, 17 Jun 2015 17:04:42 +0300 Subject: [PATCH 004/553] Add support for template-version-list command DocImpact Implements bp: template-version-list Change-Id: I80ae69ced9305f1e6883e6ca4902b5cc018b5e83 --- .../tests/functional/test_readonly_heat.py | 5 +++ .../tests/unit/test_template_versions.py | 40 +++++++++++++++++++ heatclient/v1/client.py | 3 ++ heatclient/v1/shell.py | 7 ++++ heatclient/v1/template_versions.py | 32 +++++++++++++++ 5 files changed, 87 insertions(+) create mode 100644 heatclient/tests/unit/test_template_versions.py create mode 100644 heatclient/v1/template_versions.py diff --git a/heatclient/tests/functional/test_readonly_heat.py b/heatclient/tests/functional/test_readonly_heat.py index ab04566c..63e25884 100644 --- a/heatclient/tests/functional/test_readonly_heat.py +++ b/heatclient/tests/functional/test_readonly_heat.py @@ -89,3 +89,8 @@ def test_heat_help_cmd(self): def test_heat_version(self): self.heat('', flags='--version') + + def test_heat_template_version_list(self): + ret = self.heat('template-version-list') + tmpl_types = self.parser.listing(ret) + self.assertTableStruct(tmpl_types, ['version', 'type']) diff --git a/heatclient/tests/unit/test_template_versions.py b/heatclient/tests/unit/test_template_versions.py new file mode 100644 index 00000000..0582f45e --- /dev/null +++ b/heatclient/tests/unit/test_template_versions.py @@ -0,0 +1,40 @@ +# +# 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 testtools + +from heatclient.v1 import template_versions + + +class TemplateVersionManagerTest(testtools.TestCase): + + def setUp(self): + super(TemplateVersionManagerTest, self).setUp() + + def test_list_versions(self): + expect = ('GET', '/template_versions') + + class FakeResponse(object): + def json(self): + return {'template_versions': [{'version': '2013-05-23', + 'type': 'hot'}]} + + class FakeClient(object): + def get(self, *args, **kwargs): + assert ('GET', args[0]) == expect + return FakeResponse() + + manager = template_versions.TemplateVersionManager(FakeClient()) + versions = manager.list() + self.assertEqual('2013-05-23', getattr(versions[0], 'version')) + self.assertEqual('hot', getattr(versions[0], 'type')) diff --git a/heatclient/v1/client.py b/heatclient/v1/client.py index 8299d9ed..09a5eb87 100644 --- a/heatclient/v1/client.py +++ b/heatclient/v1/client.py @@ -23,6 +23,7 @@ from heatclient.v1 import software_configs from heatclient.v1 import software_deployments from heatclient.v1 import stacks +from heatclient.v1 import template_versions class Client(object): @@ -51,3 +52,5 @@ def __init__(self, *args, **kwargs): self.software_configs = software_configs.SoftwareConfigManager( self.http_client) self.services = services.ServiceManager(self.http_client) + self.template_versions = template_versions.TemplateVersionManager( + self.http_client) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 84d751f2..1cf955f8 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -1392,3 +1392,10 @@ def do_service_list(hc, args=None): 'topic', 'updated_at', 'status'] services = hc.services.list() utils.print_list(services, fields, sortby_index=1) + + +def do_template_version_list(hc, args): + '''List the available template versions.''' + versions = hc.template_versions.list() + fields = ['version', 'type'] + utils.print_list(versions, fields, sortby_index=1) diff --git a/heatclient/v1/template_versions.py b/heatclient/v1/template_versions.py new file mode 100644 index 00000000..9b764b52 --- /dev/null +++ b/heatclient/v1/template_versions.py @@ -0,0 +1,32 @@ +# +# 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 heatclient.openstack.common.apiclient import base + + +class TemplateVersion(base.Resource): + def __repr__(self): + return "" % self._info + + def data(self, **kwargs): + return self.manager.data(self, **kwargs) + + +class TemplateVersionManager(base.BaseManager): + resource_class = TemplateVersion + + def list(self): + """Get a list of template versions. + :rtype: list of :class:`TemplateVersion` + """ + return self._list('/template_versions', 'template_versions') From 6c4d183f6eee902a01f671fc66f9a79d83ffbb87 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Mon, 6 Jul 2015 17:56:45 +0800 Subject: [PATCH 005/553] Fix TypeError when call keystoneclient session.request() A TypeError raised when run cmd 'heat resource-show': TypeError: request() got an unexpected keyword argument 'follow_redirects' This problem was introduced by the change I90bc9d9fcd1058c3eca288f08dd134810f1b0000 to move SessionClient from HTTPClient to adapter, due there is no keyword argument 'follow_redirects' in keystoneclient session.request() method. Change-Id: I91f13da740bf1e66dffd68cdf666a24d68aa543d Closes-Bug: #1471650 --- heatclient/common/http.py | 21 ++++++++------- heatclient/tests/unit/test_common_http.py | 32 +++++++++++++++++------ heatclient/v1/stacks.py | 5 ++-- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/heatclient/common/http.py b/heatclient/common/http.py index 978cf75d..3a268966 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -176,7 +176,7 @@ def _http_request(self, url, method, **kwargs): # Allow caller to specify not to follow redirects, in which case we # just return the redirect response. Useful for using stacks:lookup. - follow_redirects = kwargs.pop('follow_redirects', True) + redirect = kwargs.pop('redirect', True) # Since requests does not follow the RFC when doing redirection to sent # back the same method on a redirect we are simply bypassing it. For @@ -221,8 +221,8 @@ def _http_request(self, url, method, **kwargs): raise exc.from_response(resp) elif resp.status_code in (301, 302, 305): # Redirected. Reissue the request to the new location, - # unless caller specified follow_redirects=False - if follow_redirects: + # unless caller specified redirect=False + if redirect: location = resp.headers.get('location') path = self.strip_endpoint(location) resp = self._http_request(path, method, **kwargs) @@ -300,6 +300,7 @@ class SessionClient(adapter.LegacyJsonAdapter): """HTTP client based on Keystone client session.""" def request(self, url, method, **kwargs): + redirect = kwargs.get('redirect') kwargs.setdefault('user_agent', USER_AGENT) try: @@ -307,16 +308,18 @@ def request(self, url, method, **kwargs): except KeyError: pass - resp, body = super(SessionClient, self).request(url, method, - raise_exc=False, - **kwargs) + resp, body = super(SessionClient, self).request( + url, method, + raise_exc=False, + **kwargs) if 400 <= resp.status_code < 600: raise exc.from_response(resp) elif resp.status_code in (301, 302, 305): - location = resp.headers.get('location') - path = self.strip_endpoint(location) - resp = self.request(path, method, **kwargs) + if redirect: + location = resp.headers.get('location') + path = self.strip_endpoint(location) + resp = self.request(path, method, **kwargs) elif resp.status_code == 300: raise exc.from_response(resp) diff --git a/heatclient/tests/unit/test_common_http.py b/heatclient/tests/unit/test_common_http.py index 81b15999..bb2fa20b 100644 --- a/heatclient/tests/unit/test_common_http.py +++ b/heatclient/tests/unit/test_common_http.py @@ -1,4 +1,4 @@ -#-*- coding:utf-8 -*- +# -*- coding:utf-8 -*- # 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 @@ -712,7 +712,7 @@ def test_404_error_response(self): # Assert that the raised exception can be converted to string self.assertIsNotNone(six.text_type(e)) - def test_302_location(self): + def test_redirect_302_location(self): fake1 = fakes.FakeHTTPResponse( 302, 'OK', @@ -731,7 +731,7 @@ def test_302_location(self): client = http.SessionClient(session=mock.ANY, auth=mock.ANY, endpoint_override='http://no.where/') - resp = client.request('', 'GET') + resp = client.request('', 'GET', redirect=True) self.assertEqual(200, resp.status_code) self.assertEqual({'Mount': 'Fuji'}, utils.get_response_body(resp)) @@ -740,7 +740,8 @@ def test_302_location(self): self.assertEqual(('ishere', 'GET'), self.request.call_args_list[1][0]) for call in self.request.call_args_list: self.assertEqual({'user_agent': 'python-heatclient', - 'raise_exc': False}, call[1]) + 'raise_exc': False, + 'redirect': True}, call[1]) def test_302_location_no_endpoint(self): fake1 = fakes.FakeHTTPResponse( @@ -760,7 +761,7 @@ def test_302_location_no_endpoint(self): client = http.SessionClient(session=mock.ANY, auth=mock.ANY) - resp = client.request('', 'GET') + resp = client.request('', 'GET', redirect=True) self.assertEqual(200, resp.status_code) self.assertEqual({'Mount': 'Fuji'}, utils.get_response_body(resp)) @@ -770,9 +771,10 @@ def test_302_location_no_endpoint(self): 'GET'), self.request.call_args_list[1][0]) for call in self.request.call_args_list: self.assertEqual({'user_agent': 'python-heatclient', - 'raise_exc': False}, call[1]) + 'raise_exc': False, + 'redirect': True}, call[1]) - def test_302_no_location(self): + def test_redirect_302_no_location(self): fake = fakes.FakeHTTPResponse( 302, 'OK', @@ -784,9 +786,23 @@ def test_302_no_location(self): client = http.SessionClient(session=mock.ANY, auth=mock.ANY) e = self.assertRaises(exc.InvalidEndpoint, - client.request, '', 'GET') + client.request, '', 'GET', redirect=True) self.assertEqual("Location not returned with 302", six.text_type(e)) + def test_no_redirect_302_no_location(self): + fake = fakes.FakeHTTPResponse( + 302, + 'OK', + {'location': 'http://no.where/ishere'}, + '' + ) + self.request.side_effect = [(fake, '')] + + client = http.SessionClient(session=mock.ANY, + auth=mock.ANY) + + self.assertEqual(fake, client.request('', 'GET')) + def test_300_error_response(self): fake = fakes.FakeHTTPResponse( 300, diff --git a/heatclient/v1/stacks.py b/heatclient/v1/stacks.py index bf7a7ba3..35984523 100644 --- a/heatclient/v1/stacks.py +++ b/heatclient/v1/stacks.py @@ -235,9 +235,8 @@ def _resolve_stack_id(self, stack_id): # We want to capture the redirect, not actually get the stack, # since all we want is the stacks:lookup response to get the # fully qualified ID, and not all users are allowed to do the - # redirected stacks:show, so pass follow_redirects=False - resp = self.client.get('/stacks/%s' % stack_id, - follow_redirects=False) + # redirected stacks:show, so pass redirect=False + resp = self.client.get('/stacks/%s' % stack_id, redirect=False) location = resp.headers.get('location') path = self.client.strip_endpoint(location) return path[len('/stacks/'):] From a7067fbde78a4e3146bd8221d930c2a5406c7202 Mon Sep 17 00:00:00 2001 From: Tetiana Lashchova Date: Wed, 24 Jun 2015 16:06:43 +0300 Subject: [PATCH 006/553] Add support for template-function-list command Example: heat template-function-list DocImpact Implements bp: template-function-list Change-Id: I80fdd94c10be49f80bd8dab1288ad336d185f55d --- .../tests/functional/test_readonly_heat.py | 6 ++++++ heatclient/tests/unit/test_template_versions.py | 17 +++++++++++++++++ heatclient/v1/shell.py | 13 +++++++++++++ heatclient/v1/template_versions.py | 12 ++++++++++++ 4 files changed, 48 insertions(+) diff --git a/heatclient/tests/functional/test_readonly_heat.py b/heatclient/tests/functional/test_readonly_heat.py index 63e25884..e49c1e9a 100644 --- a/heatclient/tests/functional/test_readonly_heat.py +++ b/heatclient/tests/functional/test_readonly_heat.py @@ -94,3 +94,9 @@ def test_heat_template_version_list(self): ret = self.heat('template-version-list') tmpl_types = self.parser.listing(ret) self.assertTableStruct(tmpl_types, ['version', 'type']) + + def test_heat_template_function_list(self): + ret = self.heat('template-function-list ' + 'heat_template_version.2013-05-23') + tmpl_functions = self.parser.listing(ret) + self.assertTableStruct(tmpl_functions, ['functions', 'description']) diff --git a/heatclient/tests/unit/test_template_versions.py b/heatclient/tests/unit/test_template_versions.py index 0582f45e..fe3cd098 100644 --- a/heatclient/tests/unit/test_template_versions.py +++ b/heatclient/tests/unit/test_template_versions.py @@ -38,3 +38,20 @@ def get(self, *args, **kwargs): versions = manager.list() self.assertEqual('2013-05-23', getattr(versions[0], 'version')) self.assertEqual('hot', getattr(versions[0], 'type')) + + def test_get(self): + expect = ('GET', '/template_versions/heat_template_version.2015-04-30' + '/functions') + + class FakeResponse(object): + def json(self): + return {'template_functions': [{'function': 'get_attr'}]} + + class FakeClient(object): + def get(self, *args, **kwargs): + assert ('GET', args[0]) == expect + return FakeResponse() + + manager = template_versions.TemplateVersionManager(FakeClient()) + functions = manager.get('heat_template_version.2015-04-30') + self.assertEqual('get_attr', getattr(functions[0], 'function')) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index fd26316d..cde8d54a 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -1404,3 +1404,16 @@ def do_template_version_list(hc, args): versions = hc.template_versions.list() fields = ['version', 'type'] utils.print_list(versions, fields, sortby_index=1) + + +@utils.arg('template_version', metavar='', + help=_('Template version to get the functions for.')) +def do_template_function_list(hc, args): + '''List the available functions.''' + try: + functions = hc.template_versions.get(args.template_version) + except exc.HTTPNotFound: + raise exc.CommandError( + _('Template version not found: %s') % args.template_version) + else: + utils.print_list(functions, ['functions', 'description']) diff --git a/heatclient/v1/template_versions.py b/heatclient/v1/template_versions.py index 9b764b52..3ba4c630 100644 --- a/heatclient/v1/template_versions.py +++ b/heatclient/v1/template_versions.py @@ -11,6 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_utils import encodeutils +from six.moves.urllib import parse + from heatclient.openstack.common.apiclient import base @@ -30,3 +33,12 @@ def list(self): :rtype: list of :class:`TemplateVersion` """ return self._list('/template_versions', 'template_versions') + + def get(self, template_version): + """Get a list of functions for a specific resource_type. + + :param template_version: template version to get the functions for + """ + url_str = '/template_versions/%s/functions' % ( + parse.quote(encodeutils.safe_encode(template_version), '')) + return self._list(url_str, 'template_functions') From 69011b52937a9a75b0dcf771dc9d2de8e82020d3 Mon Sep 17 00:00:00 2001 From: Rabi Mishra Date: Fri, 3 Jul 2015 19:40:42 +0530 Subject: [PATCH 007/553] Fix error when creating deployment from cli This fixes the 'deployment-create' cli command and adds unit test. Change-Id: I3908e8bbad07b1fc372748b9adc8089ce14df015 Closes-Bug: #1464217 --- heatclient/common/deployment_utils.py | 10 +- .../tests/unit/test_deployment_utils.py | 5 +- heatclient/tests/unit/test_shell.py | 129 ++++++++++++++++++ heatclient/v1/shell.py | 13 +- 4 files changed, 144 insertions(+), 13 deletions(-) diff --git a/heatclient/common/deployment_utils.py b/heatclient/common/deployment_utils.py index a296a863..02a3f79b 100644 --- a/heatclient/common/deployment_utils.py +++ b/heatclient/common/deployment_utils.py @@ -25,7 +25,7 @@ def build_derived_config_params(action, source, name, input_values, server_id, signal_transport, signal_id=None): input_values = input_values or {} - inputs = copy.deepcopy(source.inputs) or [] + inputs = copy.deepcopy(source.get('inputs')) or [] for inp in inputs: input_key = inp['name'] @@ -79,11 +79,11 @@ def build_derived_config_params(action, source, name, input_values, _('Unsupported signal transport %s') % signal_transport) return { - 'group': source.group or 'Heat::Ungrouped', - 'config': source.config or '', - 'options': source.options, + 'group': source.get('group') or 'Heat::Ungrouped', + 'config': source.get('config') or '', + 'options': source.get('options') or {}, 'inputs': inputs, - 'outputs': source.outputs, + 'outputs': source.get('outputs') or [], 'name': name } diff --git a/heatclient/tests/unit/test_deployment_utils.py b/heatclient/tests/unit/test_deployment_utils.py index ed0cd987..69a2899a 100644 --- a/heatclient/tests/unit/test_deployment_utils.py +++ b/heatclient/tests/unit/test_deployment_utils.py @@ -19,7 +19,6 @@ from heatclient.common import deployment_utils from heatclient import exc -from heatclient.v1 import software_configs from testtools import matchers @@ -28,13 +27,13 @@ def mock_sc(group=None, config=None, options=None, inputs=None, outputs=None): - return software_configs.SoftwareConfig(None, { + return { 'group': group, 'config': config, 'options': options or {}, 'inputs': inputs or [], 'outputs': outputs or [], - }, True) + } class DerivedConfigTest(testtools.TestCase): diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 462c63d7..c0386ce4 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -3679,6 +3679,135 @@ def _set_fake_env(self): '''Patch os.environ to avoid required auth info.''' self.set_fake_env(FAKE_ENV_KEYSTONE_V2) + def test_deploy_create(self): + self.register_keystone_auth_fixture() + self.patch( + 'heatclient.common.deployment_utils.build_derived_config_params') + self.patch( + 'heatclient.common.deployment_utils.build_signal_id') + resp_dict = {'software_deployment': { + 'status': 'INPROGRESS', + 'server_id': '700115e5-0100-4ecc-9ef7-9e05f27d8803', + 'config_id': '18c4fc03-f897-4a1d-aaad-2b7622e60257', + 'output_values': { + 'deploy_stdout': '', + 'deploy_stderr': '', + 'deploy_status_code': 0, + 'result': 'The result value' + }, + 'input_values': {}, + 'action': 'UPDATE', + 'status_reason': 'Outputs received', + 'id': 'abcd' + }} + + config_dict = {'software_config': { + 'inputs': [], + 'group': 'script', + 'name': 'config_name', + 'outputs': [], + 'options': {}, + 'config': 'the config script', + 'id': 'defg'}} + + derived_dict = {'software_config': { + 'inputs': [], + 'group': 'script', + 'name': 'config_name', + 'outputs': [], + 'options': {}, + 'config': 'the config script', + 'id': 'abcd'}} + + deploy_data = {'action': 'UPDATE', + 'config_id': u'abcd', + 'server_id': 'inst01', + 'status': 'IN_PROGRESS', + 'tenant_id': 'asdf'} + + config_string = jsonutils.dumps(config_dict) + headers = {'content-type': 'application/json'} + http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, config_string) + response = (http_resp, config_dict) + if self.client == http.SessionClient: + http.SessionClient.request( + '/software_configs/defg', 'GET').AndReturn(http_resp) + else: + self.client.json_request( + 'GET', '/software_configs/defg').AndReturn(response) + + derived_string = jsonutils.dumps(derived_dict) + http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, derived_string) + response = (http_resp, derived_dict) + if self.client == http.SessionClient: + http.SessionClient.request( + '/software_configs', 'POST', data={}).AndReturn(http_resp) + else: + self.client.json_request( + 'POST', '/software_configs', data={}).AndReturn(response) + + resp_string = jsonutils.dumps(resp_dict) + http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) + response = (http_resp, resp_dict) + if self.client == http.SessionClient: + self.client.request( + '/software_deployments', 'POST', + data=deploy_data).AndReturn(http_resp) + else: + self.client.json_request( + 'POST', + '/software_deployments', data=deploy_data).AndReturn(response) + + http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, derived_string) + response = (http_resp, derived_dict) + if self.client == http.SessionClient: + http.SessionClient.request( + '/software_configs', 'POST', data={}).AndReturn(http_resp) + else: + self.client.json_request( + 'POST', '/software_configs', data={}).AndReturn(response) + + http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) + response = (http_resp, resp_dict) + if self.client == http.SessionClient: + self.client.request( + '/software_deployments', 'POST', + data=deploy_data).AndReturn(http_resp) + self.client.request( + '/software_configs/defgh', 'GET').AndRaise( + exc.HTTPNotFound()) + else: + self.client.json_request( + 'POST', '/software_deployments').AndReturn(response) + self.client.json_request( + 'GET', '/software_configs/defgh').AndRaise( + exc.HTTPNotFound()) + + self.m.ReplayAll() + + text = self.shell('deployment-create -c defg -sinst01 xxx') + + required = [ + 'status', + 'server_id', + 'config_id', + 'output_values', + 'input_values', + 'action', + 'status_reason', + 'id', + ] + for r in required: + self.assertRegexpMatches(text, r) + + text = self.shell('deployment-create -sinst01 xxx') + for r in required: + self.assertRegexpMatches(text, r) + + self.assertRaises(exc.CommandError, self.shell, + 'deployment-create -c defgh -s inst01 yyy') + self.m.VerifyAll() + def test_deploy_show(self): self.register_keystone_auth_fixture() resp_dict = {'software_deployment': { diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 2e97ba62..bebc5f6f 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -1174,7 +1174,7 @@ def do_config_delete(hc, args): 'CREATE, UPDATE, DELETE, SUSPEND, RESUME')) @utils.arg('-c', '--config', metavar='', help=_('ID of the configuration to deploy.')) -@utils.arg('-s', '--server', metavar='', +@utils.arg('-s', '--server', metavar='', required=True, help=_('ID of the server being deployed to.')) @utils.arg('-t', '--signal-transport', default='TEMP_URL_SIGNAL', @@ -1197,10 +1197,13 @@ def do_config_delete(hc, args): 'deployment. This is used to apply a sort order to the ' 'list of configurations currently deployed to the server.')) def do_deployment_create(hc, args): - try: - config = hc.software_configs.get(config_id=args.config) - except exc.HTTPNotFound: - raise exc.CommandError(_('Configuration not found: %s') % args.id) + config = {} + if args.config: + try: + config = hc.software_configs.get(config_id=args.config) + except exc.HTTPNotFound: + raise exc.CommandError( + _('Configuration not found: %s') % args.config) derrived_params = deployment_utils.build_derived_config_params( action=args.action, From be2c0eb101b14ab22890ab19c8e71ed913079807 Mon Sep 17 00:00:00 2001 From: "rico.lin" Date: Fri, 10 Jul 2015 21:33:36 +0800 Subject: [PATCH 008/553] New mock release(1.1.0) broke unit tests Change-Id: I8ef86383ecb5f76fe44da4c424b2ba7e7a88aed5 Closes-Bug: #1473448 --- heatclient/tests/unit/v1/test_hooks.py | 12 ++++++------ test-requirements.txt | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/heatclient/tests/unit/v1/test_hooks.py b/heatclient/tests/unit/v1/test_hooks.py index b797136a..45abb662 100644 --- a/heatclient/tests/unit/v1/test_hooks.py +++ b/heatclient/tests/unit/v1/test_hooks.py @@ -47,7 +47,7 @@ def test_create_hooks_in_args(self): return_value=['bp', 'another_bp']) shell.do_stack_create(self.client, self.args) - self.client.stacks.create.assert_called_once() + self.assertEqual(1, self.client.stacks.create.call_count) expected_hooks = { 'bp': {'hooks': 'pre-create'}, 'another_bp': {'hooks': 'pre-create'} @@ -61,7 +61,7 @@ def test_create_nested_hooks_in_args(self): return_value=['nested/bp', 'super/nested/bp']) shell.do_stack_create(self.client, self.args) - self.client.stacks.create.assert_called_once() + self.assertEqual(1, self.client.stacks.create.call_count) expected_hooks = { 'nested': { 'bp': {'hooks': 'pre-create'}, @@ -103,7 +103,7 @@ def test_create_hooks_in_env_and_args(self): mock.Mock(return_value=({}, env)) shell.do_stack_create(self.client, self.args) - self.client.stacks.create.assert_called_once() + self.assertEqual(1, self.client.stacks.create.call_count) actual_hooks = self.client.stacks.create.call_args[1][ 'environment']['resource_registry']['resources'] expected_hooks = { @@ -135,7 +135,7 @@ def test_update_hooks_in_args(self): return_value=['bp', 'another_bp']) shell.do_stack_update(self.client, self.args) - self.client.stacks.update.assert_called_once() + self.assertEqual(1, self.client.stacks.update.call_count) expected_hooks = { 'bp': {'hooks': 'pre-update'}, 'another_bp': {'hooks': 'pre-update'}, @@ -149,7 +149,7 @@ def test_update_nested_hooks_in_args(self): return_value=['nested/bp', 'super/nested/bp']) shell.do_stack_update(self.client, self.args) - self.client.stacks.update.assert_called_once() + self.assertEqual(1, self.client.stacks.update.call_count) expected_hooks = { 'nested': { 'bp': {'hooks': 'pre-update'} @@ -191,7 +191,7 @@ def test_update_hooks_in_env_and_args(self): mock.Mock(return_value=({}, env)) shell.do_stack_update(self.client, self.args) - self.client.stacks.update.assert_called_once() + self.assertEqual(1, self.client.stacks.update.call_count) actual_hooks = self.client.stacks.update.call_args[1][ 'environment']['resource_registry']['resources'] expected_hooks = { diff --git a/test-requirements.txt b/test-requirements.txt index b8f5604f..1a4bac8e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,8 @@ coverage>=3.6 discover fixtures>=1.3.1 requests-mock>=0.6.0 # Apache-2.0 -mock>=1.0 +mock>=1.1;python_version!='2.6' +mock==1.0.1;python_version=='2.6' mox3>=0.7.0 oslosphinx>=2.5.0 # Apache-2.0 oslotest>=1.5.1 # Apache-2.0 From b489a370354ed74decd7cde2e1222d3022fbd0c8 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 17 Jul 2015 16:17:39 +0000 Subject: [PATCH 009/553] Updated from global requirements Change-Id: I1e5ee5d4afe0ca4abcf33b2c661bf4556e2e4d83 --- requirements.txt | 4 ++-- setup.py | 2 +- test-requirements.txt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 72a83b1e..ad129789 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,13 +3,13 @@ # process, which may cause wedges in the gate later. Babel>=1.3 -pbr<2.0,>=0.11 +pbr<2.0,>=1.3 argparse iso8601>=0.1.9 PrettyTable<0.8,>=0.7 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=1.6.0 # Apache-2.0 +oslo.utils>=1.9.0 # Apache-2.0 python-keystoneclient>=1.6.0 python-swiftclient>=2.2.0 PyYAML>=3.1.0 diff --git a/setup.py b/setup.py index 056c16c2..d8080d05 100644 --- a/setup.py +++ b/setup.py @@ -25,5 +25,5 @@ pass setuptools.setup( - setup_requires=['pbr'], + setup_requires=['pbr>=1.3'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index 1a4bac8e..52bbeda9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,11 +8,11 @@ coverage>=3.6 discover fixtures>=1.3.1 requests-mock>=0.6.0 # Apache-2.0 -mock>=1.1;python_version!='2.6' +mock!=1.1.4,>=1.1;python_version!='2.6' mock==1.0.1;python_version=='2.6' mox3>=0.7.0 oslosphinx>=2.5.0 # Apache-2.0 -oslotest>=1.5.1 # Apache-2.0 +oslotest>=1.7.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 tempest-lib>=0.6.1 testrepository>=0.0.18 From 2cae5183fef485eb4224d8a3c957bc335cefc034 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Fri, 3 Jul 2015 18:00:23 +0800 Subject: [PATCH 010/553] Support 'with_attr' parameter for resource-show Support the 'with_attr' parameter when showing resource. Closes-Bug: #1428436 Change-Id: I430bf27ace4fedd55ee158f9f4fa3edc737892a4 --- heatclient/tests/unit/test_resources.py | 13 +++++++++++++ heatclient/tests/unit/test_shell.py | 16 +++++++++++----- heatclient/v1/resources.py | 7 ++++++- heatclient/v1/shell.py | 6 ++++++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/heatclient/tests/unit/test_resources.py b/heatclient/tests/unit/test_resources.py index 12a7ccc4..1444af90 100644 --- a/heatclient/tests/unit/test_resources.py +++ b/heatclient/tests/unit/test_resources.py @@ -80,6 +80,19 @@ def test_get(self): manager.get(**fields) self.m.VerifyAll() + def test_get_with_attr(self): + fields = {'stack_id': 'teststack', + 'resource_name': 'testresource', + 'with_attr': ['attr_a', 'attr_b']} + expect = ('GET', + '/stacks/teststack%2Fabcd1234/resources' + '/testresource?with_attr=attr_a&with_attr=attr_b') + key = 'resource' + + manager = self._base_test(expect, key) + manager.get(**fields) + self.m.VerifyAll() + def test_get_with_unicode_resource_name(self): fields = {'stack_id': 'teststack', 'resource_name': u'\u5de5\u4f5c'} diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index b4350532..c79af84f 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -3271,7 +3271,7 @@ def test_resource_list_nested(self): for field in required: self.assertRegexpMatches(resource_list_text, field) - def test_resource_show(self): + def test_resource_show_with_attrs(self): self.register_keystone_auth_fixture() resp_dict = {"resource": {"description": "", @@ -3287,7 +3287,11 @@ def test_resource_show(self): "resource_status": "CREATE_COMPLETE", "resource_status_reason": "state changed", "resource_type": "OS::Nova::Server", - "updated_time": "2014-01-06T16:14:26Z"}} + "updated_time": "2014-01-06T16:14:26Z", + "creation_time": "2014-01-06T16:14:26Z", + "attributes": { + "attr_a": "value_of_attr_a", + "attr_b": "value_of_attr_b"}}} resp = fakes.FakeHTTPResponse( 200, 'OK', @@ -3296,7 +3300,7 @@ def test_resource_show(self): stack_id = 'teststack/1' resource_name = 'aResource' http.SessionClient.request( - '/stacks/%s/resources/%s' % + '/stacks/%s/resources/%s?with_attr=attr_a&with_attr=attr_b' % ( parse.quote(stack_id, ''), parse.quote(encodeutils.safe_encode( @@ -3305,8 +3309,10 @@ def test_resource_show(self): self.m.ReplayAll() - resource_show_text = self.shell('resource-show {0} {1}'.format( - stack_id, resource_name)) + resource_show_text = self.shell( + 'resource-show {0} {1} --with-attr attr_a ' + '--with-attr attr_b'.format( + stack_id, resource_name)) required = [ 'description', diff --git a/heatclient/v1/resources.py b/heatclient/v1/resources.py index e84c4912..74987b11 100644 --- a/heatclient/v1/resources.py +++ b/heatclient/v1/resources.py @@ -57,16 +57,21 @@ def list(self, stack_id, nested_depth=0): url += '?nested_depth=%s' % nested_depth return self._list(url, "resources") - def get(self, stack_id, resource_name): + def get(self, stack_id, resource_name, with_attr=None): """Get the details for a specific resource. :param stack_id: ID of stack containing the resource :param resource_name: ID of resource to get the details for + :param with_attr: Attributes to show """ stack_id = self._resolve_stack_id(stack_id) url_str = '/stacks/%s/resources/%s' % ( parse.quote(stack_id, ''), parse.quote(encodeutils.safe_encode(resource_name), '')) + if with_attr: + params = {'with_attr': with_attr} + url_str += '?%s' % parse.urlencode(params, True) + resp = self.client.get(url_str) body = utils.get_response_body(resp) return Resource(self, body.get('resource')) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index a4d7af92..d0fa28a0 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -764,10 +764,16 @@ def do_resource_list(hc, args): help=_('Name or ID of stack to show the resource for.')) @utils.arg('resource', metavar='', help=_('Name of the resource to show the details for.')) +@utils.arg('-a', '--with-attr', metavar='', + help=_('Attribute to show, it can be specified ' + 'multiple times.'), + action='append') def do_resource_show(hc, args): '''Describe the resource.''' fields = {'stack_id': args.id, 'resource_name': args.resource} + if args.with_attr: + fields['with_attr'] = list(args.with_attr) try: resource = hc.resources.get(**fields) except exc.HTTPNotFound: From 40d7c78920dff8b61211202d697ab74b60de9a26 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 22 Jul 2015 04:59:32 +0000 Subject: [PATCH 011/553] Updated from global requirements Change-Id: Iec63c5630d11651b2ecd7bca16e2ee9b15c10792 --- test-requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 52bbeda9..4941e1d7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,8 +8,7 @@ coverage>=3.6 discover fixtures>=1.3.1 requests-mock>=0.6.0 # Apache-2.0 -mock!=1.1.4,>=1.1;python_version!='2.6' -mock==1.0.1;python_version=='2.6' +mock>=1.2 mox3>=0.7.0 oslosphinx>=2.5.0 # Apache-2.0 oslotest>=1.7.0 # Apache-2.0 From 997cbc63c987aceb346d48a26e07fc98fe0a4b33 Mon Sep 17 00:00:00 2001 From: Rabi Mishra Date: Tue, 7 Jul 2015 13:35:02 +0530 Subject: [PATCH 012/553] Add missing `deployment-list` to cli This patch adds `deployment-list` command to cli. $heat deployment-list $heat deployment-list -s Change-Id: Id2a21d3f3bb5e88bacbe30dc184e4cff131ab2e0 --- heatclient/tests/unit/test_shell.py | 59 +++++++++++++++++++++++++++++ heatclient/v1/shell.py | 11 ++++++ 2 files changed, 70 insertions(+) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index b4350532..5922f1e4 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -3814,6 +3814,65 @@ def test_deploy_create(self): 'deployment-create -c defgh -s inst01 yyy') self.m.VerifyAll() + def test_deploy_list(self): + self.register_keystone_auth_fixture() + + resp_dict = { + 'software_deployments': + [{'status': 'COMPLETE', + 'server_id': '123', + 'config_id': '18c4fc03-f897-4a1d-aaad-2b7622e60257', + 'output_values': { + 'deploy_stdout': '', + 'deploy_stderr': '', + 'deploy_status_code': 0, + 'result': 'The result value' + }, + 'input_values': {}, + 'action': 'CREATE', + 'status_reason': 'Outputs received', + 'id': 'defg'}, ] + } + resp_string = jsonutils.dumps(resp_dict) + headers = {'content-type': 'application/json'} + http_resp = fakes.FakeHTTPResponse(200, 'OK', headers, resp_string) + response = (http_resp, resp_dict) + if self.client == http.SessionClient: + self.client.request( + '/software_deployments?', 'GET').AndReturn(http_resp) + self.client.request( + '/software_deployments?server_id=123', + 'GET').AndReturn(http_resp) + else: + self.client.json_request( + 'GET', '/software_deployments?').AndReturn(response) + self.client.json_request( + 'GET', + '/software_deployments?server_id=123').AndReturn(response) + + self.m.ReplayAll() + + list_text = self.shell('deployment-list') + + required = [ + 'id', + 'config_id', + 'server_id', + 'action', + 'status', + 'creation_time', + 'status_reason', + ] + for r in required: + self.assertRegexpMatches(list_text, r) + self.assertNotRegexpMatches(list_text, 'parent') + + list_text = self.shell('deployment-list -s 123') + + for r in required: + self.assertRegexpMatches(list_text, r) + self.assertNotRegexpMatches(list_text, 'parent') + def test_deploy_show(self): self.register_keystone_auth_fixture() resp_dict = {'software_deployment': { diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index a4d7af92..cdf7ec06 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -1226,6 +1226,17 @@ def do_deployment_create(hc, args): print(jsonutils.dumps(sd.to_dict(), indent=2)) +@utils.arg('-s', '--server', metavar='', + help=_('ID of the server to fetch deployments for.')) +def do_deployment_list(hc, args): + '''List software deployments.''' + kwargs = {'server_id': args.server} if args.server else {} + deployments = hc.software_deployments.list(**kwargs) + fields = ['id', 'config_id', 'server_id', 'action', 'status', + 'creation_time', 'status_reason'] + utils.print_list(deployments, fields, sortby_index=5) + + @utils.arg('id', metavar='', help=_('ID of the deployment.')) def do_deployment_show(hc, args): From b874ebec1f76ca49386b50fba84923d918c82461 Mon Sep 17 00:00:00 2001 From: Tetiana Lashchova Date: Thu, 23 Jul 2015 05:47:34 -0400 Subject: [PATCH 013/553] Fix docstrings and add missing Change-Id: If5a4a14775bf74ad5db00e419b386b1647255c29 --- heatclient/v1/shell.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index a4d7af92..bf185c26 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -1150,7 +1150,7 @@ def do_config_show(hc, args): @utils.arg('id', metavar='', nargs='+', help=_('IDs of the configurations to delete.')) def do_config_delete(hc, args): - '''Delete software configurations.''' + '''Delete a software configuration.''' failure_count = 0 for config_id in args.id: @@ -1197,6 +1197,7 @@ def do_config_delete(hc, args): 'deployment. This is used to apply a sort order to the ' 'list of configurations currently deployed to the server.')) def do_deployment_create(hc, args): + '''Create a software deployment.''' config = {} if args.config: try: @@ -1249,7 +1250,7 @@ def do_deployment_metadata_show(hc, args): @utils.arg('id', metavar='', nargs='+', help=_('IDs of the deployments to delete.')) def do_deployment_delete(hc, args): - '''Delete software deployments.''' + '''Delete a software deployment.''' failure_count = 0 for deploy_id in args.id: @@ -1273,7 +1274,7 @@ def do_deployment_delete(hc, args): help=_('The output value format, one of: raw, json'), default='raw') def do_deployment_output_show(hc, args): - '''Show a specific stack output.''' + '''Show a specific deployment output.''' if (not args.all and args.output is None or args.all and args.output is not None): raise exc.CommandError( From 280737053522037e89c8f86576a9239e30bcd002 Mon Sep 17 00:00:00 2001 From: Rico Lin Date: Mon, 13 Jul 2015 11:01:48 +0800 Subject: [PATCH 014/553] add with_detail parameter in heat resource-list request DocImpact Change-Id: Ia8fad976819902c50df183925f410156395719db Closes-Bug: #1473086 --- heatclient/tests/unit/test_resources.py | 37 +++++++++++++------------ heatclient/tests/unit/test_shell.py | 35 +++++++++++++++++++++++ heatclient/v1/resources.py | 13 +++++++-- heatclient/v1/shell.py | 6 +++- 4 files changed, 70 insertions(+), 21 deletions(-) diff --git a/heatclient/tests/unit/test_resources.py b/heatclient/tests/unit/test_resources.py index 1444af90..120b1c4b 100644 --- a/heatclient/tests/unit/test_resources.py +++ b/heatclient/tests/unit/test_resources.py @@ -14,6 +14,7 @@ from heatclient.common import utils from heatclient.v1 import resources +from six.moves.urllib import parse from mox3 import mox import testtools @@ -106,25 +107,27 @@ def test_get_with_unicode_resource_name(self): self.m.VerifyAll() def test_list(self): - fields = {'stack_id': 'teststack'} - expect = ('/stacks/teststack/resources') - key = 'resources' - - class FakeResponse(object): - def json(self): - return {key: {}} - - class FakeClient(object): - def get(self, *args, **kwargs): - assert args[0] == expect - return FakeResponse() - - manager = resources.ResourceManager(FakeClient()) - manager.list(**fields) + self._test_list( + fields={'stack_id': 'teststack'}, + expect='/stacks/teststack/resources') def test_list_nested(self): - fields = {'stack_id': 'teststack', 'nested_depth': '99'} - expect = ('/stacks/teststack/resources?nested_depth=99') + self._test_list( + fields={'stack_id': 'teststack', 'nested_depth': '99'}, + expect='/stacks/teststack/resources?%s' % parse.urlencode({ + 'nested_depth': 99, + }, True) + ) + + def test_list_detail(self): + self._test_list( + fields={'stack_id': 'teststack', 'with_detail': 'True'}, + expect='/stacks/teststack/resources?%s' % parse.urlencode({ + 'with_detail': True, + }, True) + ) + + def _test_list(self, fields, expect): key = 'resources' class FakeResponse(object): diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 42cdd89e..d0e33be4 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -3271,6 +3271,41 @@ def test_resource_list_nested(self): for field in required: self.assertRegexpMatches(resource_list_text, field) + def test_resource_list_detail(self): + self.register_keystone_auth_fixture() + resp_dict = {"resources": [{ + "resource_name": "foobar", + "links": [{ + "href": "http://heat.example.com:8004/foo/12/resources/foobar", + "rel": "self" + }, { + "href": "http://heat.example.com:8004/foo/12", + "rel": "stack" + }], + }]} + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(resp_dict)) + stack_id = 'teststack/1' + http.SessionClient.request('/stacks/%s/resources?%s' % ( + stack_id, + parse.urlencode({'with_detail': True}, True) + ), 'GET').AndReturn(resp) + + self.m.ReplayAll() + + shell_cmd = 'resource-list {0} --with-detail'.format(stack_id) + resource_list_text = self.shell(shell_cmd) + + required = [ + 'resource_name', 'foobar', + 'stack_name', 'foo', + ] + for field in required: + self.assertRegexpMatches(resource_list_text, field) + def test_resource_show_with_attrs(self): self.register_keystone_auth_fixture() resp_dict = {"resource": diff --git a/heatclient/v1/resources.py b/heatclient/v1/resources.py index 74987b11..8949d270 100644 --- a/heatclient/v1/resources.py +++ b/heatclient/v1/resources.py @@ -15,6 +15,7 @@ from heatclient.common import utils from oslo_utils import encodeutils +import six from six.moves.urllib import parse from heatclient.openstack.common.apiclient import base @@ -48,13 +49,19 @@ def stack_name(self): class ResourceManager(stacks.StackChildManager): resource_class = Resource - def list(self, stack_id, nested_depth=0): + def list(self, stack_id, **kwargs): """Get a list of resources. :rtype: list of :class:`Resource` """ + params = {} + + for key, value in six.iteritems(kwargs): + if value: + params[key] = value url = '/stacks/%s/resources' % stack_id - if nested_depth: - url += '?nested_depth=%s' % nested_depth + if params: + url += '?%s' % parse.urlencode(params, True) + return self._list(url, "resources") def get(self, stack_id, resource_name, with_attr=None): diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 535b5dd4..1f8b22ed 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -736,11 +736,15 @@ def do_template_validate(hc, args): help=_('Name or ID of stack to show the resources for.')) @utils.arg('-n', '--nested-depth', metavar='', help=_('Depth of nested stacks from which to display resources.')) +@utils.arg('--with-detail', default=False, action="store_true", + help=_('Enable detail information presented for each resource' + 'in resources list.')) def do_resource_list(hc, args): '''Show list of resources belonging to a stack.''' fields = { 'stack_id': args.id, 'nested_depth': args.nested_depth, + 'with_detail': args.with_detail, } try: resources = hc.resources.list(**fields) @@ -754,7 +758,7 @@ def do_resource_list(hc, args): else: fields.insert(0, 'resource_name') - if args.nested_depth: + if args.nested_depth or args.with_detail: fields.append('stack_name') utils.print_list(resources, fields, sortby_index=4) From a3e854902ea5b4f7c11b0b42bba58dd5be52d3b6 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 29 Jul 2015 03:50:29 +0000 Subject: [PATCH 015/553] Updated from global requirements Change-Id: Idbeae4619d14047f5ef38e956c9b522a17358339 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 4941e1d7..b998f9ea 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ requests-mock>=0.6.0 # Apache-2.0 mock>=1.2 mox3>=0.7.0 oslosphinx>=2.5.0 # Apache-2.0 -oslotest>=1.7.0 # Apache-2.0 +oslotest>=1.9.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 tempest-lib>=0.6.1 testrepository>=0.0.18 From 755aaf1ef2d3740beec78b524464078e87fda672 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 4 Aug 2015 00:48:49 +0000 Subject: [PATCH 016/553] Updated from global requirements Change-Id: I812207d78302526102e7090de97d1b724f1d247d --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index b998f9ea..49e38ee0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ requests-mock>=0.6.0 # Apache-2.0 mock>=1.2 mox3>=0.7.0 oslosphinx>=2.5.0 # Apache-2.0 -oslotest>=1.9.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 tempest-lib>=0.6.1 testrepository>=0.0.18 From 142d0996ec9ef086b90fee7356c092d8665c679d Mon Sep 17 00:00:00 2001 From: Sirushti Murugesan Date: Wed, 5 Aug 2015 13:50:45 +0530 Subject: [PATCH 017/553] Update python3 classifiers Add support for 3.4 and drop support for 3.3 Version 3.3 isn't available in all the distros nor do we gate on that python version. So drop it. Change-Id: Icf4c07e87568a8bb6f41b50a87fe95c791adef1d --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 3b001fea..bbae0a8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,7 +17,7 @@ classifier = Programming Language :: Python :: 2.7 Programming Language :: Python :: 2.6 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 [files] packages = From 948dcb57d2116173d114be2da2db8a9c1eb1a89f Mon Sep 17 00:00:00 2001 From: Sergey Kraynev Date: Fri, 7 Aug 2015 10:14:13 -0400 Subject: [PATCH 018/553] Use Heat resource to validate resource_type_show Patch I01aaadf86bf86f2a01bfd3fd28541522ef5a99cf added new functionality, which raise error, if resource related endpoint is not registred in keystone. Due to heatclient functional tests job starts only heat and keystone service, we should use Heat resources for testing. The good candidate for it is OS::Heat::RandomString. Change-Id: I274a6db5c886ce8e19968e6e2ae8b16e93315cb7 Closes-Bug: #1482661 --- heatclient/tests/functional/test_readonly_heat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heatclient/tests/functional/test_readonly_heat.py b/heatclient/tests/functional/test_readonly_heat.py index e49c1e9a..84fe049f 100644 --- a/heatclient/tests/functional/test_readonly_heat.py +++ b/heatclient/tests/functional/test_readonly_heat.py @@ -56,7 +56,7 @@ def test_heat_resource_type_list(self): self.assertTableStruct(rsrc_types, ['resource_type']) def test_heat_resource_type_show(self): - rsrc_schema = self.heat('resource-type-show OS::Nova::Server') + rsrc_schema = self.heat('resource-type-show OS::Heat::RandomString') # resource-type-show returns a json resource schema self.assertIsInstance(json.loads(rsrc_schema), dict) From bdbc26adf1f02cc0ca9bb66cb29a97010c71ec17 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 10 Aug 2015 01:10:06 +0000 Subject: [PATCH 019/553] Updated from global requirements Change-Id: I9d4bdd614837b00fa76cf9af606816dc9b02e345 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ad129789..cb85dc07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. Babel>=1.3 -pbr<2.0,>=1.3 +pbr<2.0,>=1.4 argparse iso8601>=0.1.9 PrettyTable<0.8,>=0.7 From 04b3880cb45fb683df32cca248a47f6578464d50 Mon Sep 17 00:00:00 2001 From: Rakesh H S Date: Thu, 4 Jun 2015 11:43:03 +0530 Subject: [PATCH 020/553] Poll functionality for stack create action Poll functionality is required for long running stacks. When stack-create is passed with --poll argument, it will first print stack-show output and then continously print the events in log format until stack completes its action with success/failure. This patch only implements poll for stack create action. DocImpact A new option --poll is added to stack-create. Partial-Bug: #1420541 Change-Id: Ib7d35b66521f0ccca8544fd18fb70e04eaf98e5a --- heatclient/exc.py | 4 + heatclient/tests/unit/fakes.py | 77 +++++++ heatclient/tests/unit/test_shell.py | 279 ++++++++++++++++--------- heatclient/tests/unit/v1/test_hooks.py | 1 + heatclient/v1/shell.py | 74 +++++-- 5 files changed, 322 insertions(+), 113 deletions(-) diff --git a/heatclient/exc.py b/heatclient/exc.py index 7168d79f..4f8a0623 100644 --- a/heatclient/exc.py +++ b/heatclient/exc.py @@ -190,3 +190,7 @@ class NoTokenLookupException(Exception): class EndpointNotFound(Exception): """DEPRECATED.""" pass + + +class StackFailure(Exception): + pass diff --git a/heatclient/tests/unit/fakes.py b/heatclient/tests/unit/fakes.py index 4a65a995..2469fad8 100644 --- a/heatclient/tests/unit/fakes.py +++ b/heatclient/tests/unit/fakes.py @@ -93,6 +93,83 @@ def mock_script_heat_list(show_nested=False): return resp, resp_dict +def mock_script_event_list( + stack_name="teststack", resource_name=None, + rsrc_eventid1="7fecaeed-d237-4559-93a5-92d5d9111205", + rsrc_eventid2="e953547a-18f8-40a7-8e63-4ec4f509648b", + action="CREATE", final_state="COMPLETE", fakehttp=True): + + resp_dict = {"events": [ + {"event_time": "2013-12-05T14:14:31Z", + "id": rsrc_eventid1, + "links": [{"href": "http://heat.example.com:8004/foo", + "rel": "self"}, + {"href": "http://heat.example.com:8004/foo2", + "rel": "resource"}, + {"href": "http://heat.example.com:8004/foo3", + "rel": "stack"}], + "logical_resource_id": "myDeployment", + "physical_resource_id": None, + "resource_name": resource_name if resource_name else "testresource", + "resource_status": "%s_IN_PROGRESS" % action, + "resource_status_reason": "state changed"}, + {"event_time": "2013-12-05T14:14:32Z", + "id": rsrc_eventid2, + "links": [{"href": "http://heat.example.com:8004/foo", + "rel": "self"}, + {"href": "http://heat.example.com:8004/foo2", + "rel": "resource"}, + {"href": "http://heat.example.com:8004/foo3", + "rel": "stack"}], + "logical_resource_id": "myDeployment", + "physical_resource_id": "bce15ec4-8919-4a02-8a90-680960fb3731", + "resource_name": resource_name if resource_name else "testresource", + "resource_status": "%s_%s" % (action, final_state), + "resource_status_reason": "state changed"}]} + + if resource_name is None: + # if resource_name is not specified, + # then request is made for stack events. Hence include the stack event + stack_event1 = "0159dccd-65e1-46e8-a094-697d20b009e5" + stack_event2 = "8f591a36-7190-4adb-80da-00191fe22388" + resp_dict["events"].insert( + 0, {"event_time": "2013-12-05T14:14:30Z", + "id": stack_event1, + "links": [{"href": "http://heat.example.com:8004/foo", + "rel": "self"}, + {"href": "http://heat.example.com:8004/foo2", + "rel": "resource"}, + {"href": "http://heat.example.com:8004/foo3", + "rel": "stack"}], + "logical_resource_id": "aResource", + "physical_resource_id": None, + "resource_name": stack_name, + "resource_status": "%s_IN_PROGRESS" % action, + "resource_status_reason": "state changed"}) + resp_dict["events"].append( + {"event_time": "2013-12-05T14:14:33Z", + "id": stack_event2, + "links": [{"href": "http://heat.example.com:8004/foo", + "rel": "self"}, + {"href": "http://heat.example.com:8004/foo2", + "rel": "resource"}, + {"href": "http://heat.example.com:8004/foo3", + "rel": "stack"}], + "logical_resource_id": "aResource", + "physical_resource_id": None, + "resource_name": stack_name, + "resource_status": "%s_%s" % (action, final_state), + "resource_status_reason": "state changed"}) + + resp = FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(resp_dict)) if fakehttp else None + + return resp, resp_dict + + def script_heat_normal_error(client=http.HTTPClient): resp_dict = { "explanation": "The resource could not be found.", diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index d0e33be4..d24a5de0 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -388,33 +388,10 @@ def test_event_list(self): status_code=302, headers=h) - resp_dict = {"events": [ - {"event_time": "2014-12-05T14:14:30Z", - "id": eventid1, - "links": [{"href": "http://heat.example.com:8004/foo", - "rel": "self"}, - {"href": "http://heat.example.com:8004/foo2", - "rel": "resource"}, - {"href": "http://heat.example.com:8004/foo3", - "rel": "stack"}], - "logical_resource_id": "myDeployment", - "physical_resource_id": None, - "resource_name": "myDeployment", - "resource_status": "CREATE_IN_PROGRESS", - "resource_status_reason": "state changed"}, - {"event_time": "2014-12-05T14:14:30Z", - "id": eventid2, - "links": [{"href": "http://heat.example.com:8004/foo", - "rel": "self"}, - {"href": "http://heat.example.com:8004/foo2", - "rel": "resource"}, - {"href": "http://heat.example.com:8004/foo3", - "rel": "stack"}], - "logical_resource_id": "myDeployment", - "physical_resource_id": uuid.uuid4().hex, - "resource_name": "myDeployment", - "resource_status": "CREATE_COMPLETE", - "resource_status_reason": "state changed"}]} + resp, resp_dict = fakes.mock_script_event_list( + resource_name="myDeployment", rsrc_eventid1=eventid1, + rsrc_eventid2=eventid2, fakehttp=False + ) self.requests.get('http://heat.example.com/stacks/myStack%2F60f83b5e/' 'resources/myDeployment/events', @@ -434,8 +411,8 @@ def test_event_list(self): eventid2, 'state changed', 'CREATE_IN_PROGRESS', - '2014-12-05T14:14:30Z', - '2014-12-05T14:14:30Z', + '2013-12-05T14:14:31Z', + '2013-12-05T14:14:32Z', ] for r in required: @@ -1145,6 +1122,165 @@ def test_stack_create(self): for r in required: self.assertRegexpMatches(create_text, r) + def test_create_success_with_poll(self): + self.register_keystone_auth_fixture() + + stack_create_resp_dict = {"stack": { + "id": "teststack2/2", + "stack_name": "teststack2", + "stack_status": 'CREATE_IN_PROGRESS', + "creation_time": "2012-10-25T01:58:47Z" + }} + stack_create_resp = fakes.FakeHTTPResponse( + 201, + 'Created', + {'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'}, + jsonutils.dumps(stack_create_resp_dict)) + if self.client == http.SessionClient: + headers = {} + self.client.request( + '/stacks', 'POST', data=mox.IgnoreArg(), + headers=headers).AndReturn(stack_create_resp) + else: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + self.client.json_request( + 'POST', '/stacks', data=mox.IgnoreArg(), + headers=headers + ).AndReturn((stack_create_resp, None)) + fakes.script_heat_list(client=self.client) + + stack_show_resp_dict = {"stack": { + "id": "1", + "stack_name": "teststack", + "stack_status": 'CREATE_COMPLETE', + "creation_time": "2012-10-25T01:58:47Z" + }} + stack_show_resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_show_resp_dict)) + + event_list_resp, event_list_resp_dict = fakes.mock_script_event_list( + stack_name="teststack2") + stack_id = 'teststack2' + + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2', 'GET').MultipleTimes().AndReturn( + stack_show_resp) + self.client.request( + '/stacks/%s/events?sort_dir=asc' % stack_id, 'GET' + ).MultipleTimes().AndReturn(event_list_resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack2').MultipleTimes().AndReturn( + (stack_show_resp, stack_show_resp_dict)) + http.HTTPClient.json_request( + 'GET', '/stacks/%s/events?sort_dir=asc' % stack_id + ).MultipleTimes().AndReturn((event_list_resp, + event_list_resp_dict)) + self.m.ReplayAll() + + template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') + create_text = self.shell( + 'stack-create teststack2 ' + '--poll 4 ' + '--template-file=%s ' + '--parameters="InstanceType=m1.large;DBUsername=wp;' + 'DBPassword=verybadpassword;KeyName=heat_key;' + 'LinuxDistribution=F17"' % template_file) + + required = [ + 'id', + 'stack_name', + 'stack_status', + '2', + 'teststack2', + 'IN_PROGRESS', + '14:14:30', '2013-12-05', '0159dccd-65e1-46e8-a094-697d20b009e5', + 'CREATE_IN_PROGRESS', 'state changed', + '14:14:31', '7fecaeed-d237-4559-93a5-92d5d9111205', + 'testresource', + '14:14:32', 'e953547a-18f8-40a7-8e63-4ec4f509648b', + 'CREATE_COMPLETE', + '14:14:33', '8f591a36-7190-4adb-80da-00191fe22388' + ] + + for r in required: + self.assertRegexpMatches(create_text, r) + + def test_create_failed_with_poll(self): + self.register_keystone_auth_fixture() + stack_create_resp_dict = {"stack": { + "id": "teststack2/2", + "stack_name": "teststack2", + "stack_status": 'CREATE_IN_PROGRESS', + "creation_time": "2012-10-25T01:58:47Z" + }} + stack_create_resp = fakes.FakeHTTPResponse( + 201, + 'Created', + {'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'}, + jsonutils.dumps(stack_create_resp_dict)) + if self.client == http.SessionClient: + headers = {} + self.client.request( + '/stacks', 'POST', data=mox.IgnoreArg(), + headers=headers).AndReturn(stack_create_resp) + else: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + self.client.json_request( + 'POST', '/stacks', data=mox.IgnoreArg(), + headers=headers + ).AndReturn((stack_create_resp, None)) + fakes.script_heat_list(client=self.client) + + stack_show_resp_dict = {"stack": { + "id": "1", + "stack_name": "teststack", + "stack_status": 'CREATE_COMPLETE', + "creation_time": "2012-10-25T01:58:47Z" + }} + stack_show_resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_show_resp_dict)) + + event_list_resp, event_list_resp_dict = fakes.mock_script_event_list( + stack_name="teststack2", action="CREATE", final_state="FAILED") + stack_id = 'teststack2' + + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2', 'GET').MultipleTimes().AndReturn( + stack_show_resp) + self.client.request( + '/stacks/%s/events?sort_dir=asc' % stack_id, 'GET' + ).MultipleTimes().AndReturn(event_list_resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack2').MultipleTimes().AndReturn( + (stack_show_resp, stack_show_resp_dict)) + http.HTTPClient.json_request( + 'GET', '/stacks/%s/events?sort_dir=asc' % stack_id + ).MultipleTimes().AndReturn((event_list_resp, + event_list_resp_dict)) + + self.m.ReplayAll() + + template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') + + e = self.assertRaises(exc.StackFailure, self.shell, + 'stack-create teststack2 --poll ' + '--template-file=%s --parameters="InstanceType=' + 'm1.large;DBUsername=wp;DBPassword=password;' + 'KeyName=heat_key;LinuxDistribution=F17' % + template_file) + self.assertEqual("\n Stack teststack2 CREATE_FAILED \n", + str(e)) + def test_stack_create_param_file(self): self.register_keystone_auth_fixture() resp = fakes.FakeHTTPResponse( @@ -2598,39 +2734,11 @@ def setUp(self): def test_event_list(self): self.register_keystone_auth_fixture() - resp_dict = {"events": [ - {"event_time": "2013-12-05T14:14:30Z", - "id": self.event_id_one, - "links": [{"href": "http://heat.example.com:8004/foo", - "rel": "self"}, - {"href": "http://heat.example.com:8004/foo2", - "rel": "resource"}, - {"href": "http://heat.example.com:8004/foo3", - "rel": "stack"}], - "logical_resource_id": "aResource", - "physical_resource_id": None, - "resource_name": "aResource", - "resource_status": "CREATE_IN_PROGRESS", - "resource_status_reason": "state changed"}, - {"event_time": "2013-12-05T14:14:30Z", - "id": self.event_id_two, - "links": [{"href": "http://heat.example.com:8004/foo", - "rel": "self"}, - {"href": "http://heat.example.com:8004/foo2", - "rel": "resource"}, - {"href": "http://heat.example.com:8004/foo3", - "rel": "stack"}], - "logical_resource_id": "aResource", - "physical_resource_id": - "bce15ec4-8919-4a02-8a90-680960fb3731", - "resource_name": "aResource", - "resource_status": "CREATE_COMPLETE", - "resource_status_reason": "state changed"}]} - resp = fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(resp_dict)) + resp, resp_dict = fakes.mock_script_event_list( + resource_name="aResource", + rsrc_eventid1=self.event_id_one, + rsrc_eventid2=self.event_id_two + ) stack_id = 'teststack/1' resource_name = 'testresource/1' http.SessionClient.request( @@ -2656,47 +2764,20 @@ def test_event_list(self): 'state changed', 'CREATE_IN_PROGRESS', 'CREATE_COMPLETE', - '2013-12-05T14:14:30Z', - '2013-12-05T14:14:30Z', + '2013-12-05T14:14:31Z', + '2013-12-05T14:14:32Z', ] for r in required: self.assertRegexpMatches(event_list_text, r) def test_stack_event_list_log(self): self.register_keystone_auth_fixture() - resp_dict = {"events": [ - {"event_time": "2013-12-05T14:14:30Z", - "id": self.event_id_one, - "links": [{"href": "http://heat.example.com:8004/foo", - "rel": "self"}, - {"href": "http://heat.example.com:8004/foo2", - "rel": "resource"}, - {"href": "http://heat.example.com:8004/foo3", - "rel": "stack"}], - "logical_resource_id": "aResource", - "physical_resource_id": None, - "resource_name": "aResource", - "resource_status": "CREATE_IN_PROGRESS", - "resource_status_reason": "state changed"}, - {"event_time": "2013-12-05T14:14:30Z", - "id": self.event_id_two, - "links": [{"href": "http://heat.example.com:8004/foo", - "rel": "self"}, - {"href": "http://heat.example.com:8004/foo2", - "rel": "resource"}, - {"href": "http://heat.example.com:8004/foo3", - "rel": "stack"}], - "logical_resource_id": "aResource", - "physical_resource_id": - "bce15ec4-8919-4a02-8a90-680960fb3731", - "resource_name": "aResource", - "resource_status": "CREATE_COMPLETE", - "resource_status_reason": "state changed"}]} - resp = fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(resp_dict)) + resp, resp_dict = fakes.mock_script_event_list( + resource_name="aResource", + rsrc_eventid1=self.event_id_one, + rsrc_eventid2=self.event_id_two + ) + stack_id = 'teststack/1' if self.client == http.SessionClient: self.client.request( @@ -2713,9 +2794,9 @@ def test_stack_event_list_log(self): event_list_text = self.shell('event-list {0} --format log'.format( stack_id)) - expected = '14:14:30 2013-12-05 %s [aResource]: ' \ + expected = '14:14:31 2013-12-05 %s [aResource]: ' \ 'CREATE_IN_PROGRESS state changed\n' \ - '14:14:30 2013-12-05 %s [aResource]: CREATE_COMPLETE ' \ + '14:14:32 2013-12-05 %s [aResource]: CREATE_COMPLETE ' \ 'state changed\n' % (self.event_id_one, self.event_id_two) self.assertEqual(expected, event_list_text) diff --git a/heatclient/tests/unit/v1/test_hooks.py b/heatclient/tests/unit/v1/test_hooks.py index 45abb662..2b0724c6 100644 --- a/heatclient/tests/unit/v1/test_hooks.py +++ b/heatclient/tests/unit/v1/test_hooks.py @@ -41,6 +41,7 @@ def setUp(self): type(self.args).rollback = mock.PropertyMock(return_value=None) type(self.args).pre_create = mock.PropertyMock(return_value=False) type(self.args).pre_update = mock.PropertyMock(return_value=False) + type(self.args).poll = mock.PropertyMock(return_value=None) def test_create_hooks_in_args(self): type(self.args).pre_create = mock.PropertyMock( diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 2d99174c..2698e45e 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -20,6 +20,7 @@ from oslo_utils import strutils import six from six.moves.urllib import request +import time import yaml from heatclient.common import deployment_utils @@ -89,6 +90,10 @@ def _do(*args, **kwargs): 'This can be specified multiple times. Parameter value ' 'would be the content of the file'), action='append') +@utils.arg('--poll', metavar='SECONDS', type=int, nargs='?', const=5, + help=_('Poll and report events until stack completes. ' + 'Optional poll interval in seconds can be provided as ' + 'argument, default 5.')) @utils.arg('name', metavar='', help=_('Name of the stack to create.')) @utils.arg('--tags', metavar='', @@ -134,6 +139,8 @@ def do_stack_create(hc, args): hc.stacks.create(**fields) do_stack_list(hc) + if args.poll is not None: + _poll_for_events(hc, args.name, 'CREATE', poll_period=args.poll) def hooks_to_env(env, arg_hooks, hook): @@ -380,20 +387,7 @@ def do_action_check(hc, args): def do_stack_show(hc, args): '''Describe the stack.''' fields = {'stack_id': args.id} - try: - stack = hc.stacks.get(**fields) - except exc.HTTPNotFound: - raise exc.CommandError(_('Stack not found: %s') % args.id) - else: - formatters = { - 'description': utils.text_wrap_formatter, - 'template_description': utils.text_wrap_formatter, - 'stack_status_reason': utils.text_wrap_formatter, - 'parameters': utils.json_formatter, - 'outputs': utils.json_formatter, - 'links': utils.link_formatter - } - utils.print_dict(stack.to_dict(), formatters=formatters) + _do_stack_show(hc, fields) @utils.arg('-f', '--template-file', metavar='', @@ -1442,3 +1436,55 @@ def do_template_function_list(hc, args): _('Template version not found: %s') % args.template_version) else: utils.print_list(functions, ['functions', 'description']) + + +def _do_stack_show(hc, fields): + try: + stack = hc.stacks.get(**fields) + except exc.HTTPNotFound: + raise exc.CommandError(_('Stack not found: %s') % + fields.get('stack_id')) + else: + formatters = { + 'description': utils.text_wrap_formatter, + 'template_description': utils.text_wrap_formatter, + 'stack_status_reason': utils.text_wrap_formatter, + 'parameters': utils.json_formatter, + 'outputs': utils.json_formatter, + 'links': utils.link_formatter + } + utils.print_dict(stack.to_dict(), formatters=formatters) + + +def _poll_for_events(hc, stack_name, action, poll_period): + """When an action is performed on a stack, continuously poll for its + events and display to user as logs. + """ + fields = {'stack_id': stack_name} + _do_stack_show(hc, fields) + marker = None + while True: + events = event_utils.get_events(hc, stack_id=stack_name, + event_args={'sort_dir': 'asc', + 'marker': marker}) + + if len(events) >= 1: + # set marker to last event that was received. + marker = getattr(events[-1], 'id', None) + events_log = utils.event_log_formatter(events) + print(events_log) + for event in events: + # check if stack event was also received + if getattr(event, 'resource_name', '') == stack_name: + stack_status = getattr(event, 'resource_status', '') + msg = _("\n Stack %(name)s %(status)s \n") % dict( + name=stack_name, status=stack_status) + if stack_status == '%s_COMPLETE' % action: + _do_stack_show(hc, fields) + print(msg) + return + elif stack_status == '%s_FAILED' % action: + _do_stack_show(hc, fields) + raise exc.StackFailure(msg) + + time.sleep(poll_period) From 513b2e5ef152f5c2dda8a15497718d7bd7bff2a2 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 13 Aug 2015 20:21:35 +0000 Subject: [PATCH 021/553] Updated from global requirements Change-Id: I37d6860b29c4e472a5a91f48b4fc1cac2097552a --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cb85dc07..f6062377 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ iso8601>=0.1.9 PrettyTable<0.8,>=0.7 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=1.9.0 # Apache-2.0 +oslo.utils>=2.0.0 # Apache-2.0 python-keystoneclient>=1.6.0 python-swiftclient>=2.2.0 PyYAML>=3.1.0 From 177c9c2cbe5f8d883dac49016b06d63a2045e468 Mon Sep 17 00:00:00 2001 From: Ethan Lynn Date: Wed, 19 Aug 2015 16:08:58 +0800 Subject: [PATCH 022/553] Do not show snapshot data when doing snapshot-list Do not show snapshot data when doing snapshot-list Closes-Bug: #1486427 Change-Id: I9db134b426454c9c34038789ce5cf2d98396661d --- heatclient/tests/unit/test_shell.py | 3 --- heatclient/v1/shell.py | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index d24a5de0..d4542397 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -2402,7 +2402,6 @@ def test_snapshot_list(self): "name": "snap1", "status": "COMPLETE", "status_reason": "", - "data": {}, "creation_time": "2014-12-05T01:25:52Z" }]} @@ -2439,11 +2438,9 @@ def test_snapshot_list(self): 'name', 'status', 'status_reason', - 'data', 'creation_time', '2', 'COMPLETE', - '{}', '2014-12-05T01:25:52Z', ] for r in required: diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 2698e45e..d4e6fed0 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -1396,15 +1396,12 @@ def do_snapshot_list(hc, args): except exc.HTTPNotFound: raise exc.CommandError(_('Stack not found: %s') % args.id) else: - fields = ['id', 'name', 'status', 'status_reason', 'data', - 'creation_time'] + fields = ['id', 'name', 'status', 'status_reason', 'creation_time'] formatters = { 'id': lambda x: x['id'], 'name': lambda x: x['name'], 'status': lambda x: x['status'], 'status_reason': lambda x: x['status_reason'], - 'data': lambda x: jsonutils.dumps(x['data'], indent=2, - ensure_ascii=False), 'creation_time': lambda x: x['creation_time'], } utils.print_list(snapshots["snapshots"], fields, formatters=formatters) From 90be5230c9cf29bf81eb3603304e0665b76b0816 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Mon, 24 Aug 2015 12:05:25 +0800 Subject: [PATCH 023/553] Support tags parameter for stack-preview Support 'tags' parameter for stack preview. Change-Id: I4159dab7a545517bda95b8f23224aeb10d5b8bac --- heatclient/tests/unit/test_shell.py | 14 +++++++++++--- heatclient/v1/shell.py | 5 +++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index d24a5de0..10393959 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -1016,7 +1016,8 @@ def test_template_show_hot(self): for r in required: self.assertRegexpMatches(show_text, r) - def _test_stack_preview(self, timeout=None, enable_rollback=False): + def _test_stack_preview(self, timeout=None, enable_rollback=False, + tags=None): self.register_keystone_auth_fixture() resp_dict = {"stack": { "id": "1", @@ -1025,7 +1026,8 @@ def _test_stack_preview(self, timeout=None, enable_rollback=False): "resources": {'1': {'name': 'r1'}}, "creation_time": "2012-10-25T01:58:47Z", "timeout_mins": timeout, - "disable_rollback": not(enable_rollback) + "disable_rollback": not(enable_rollback), + "tags": tags }} resp = fakes.FakeHTTPResponse( 200, @@ -1060,6 +1062,8 @@ def _test_stack_preview(self, timeout=None, enable_rollback=False): cmd += '-r ' if timeout: cmd += '--timeout=%d ' % timeout + if tags: + cmd += '--tags=%s ' % tags preview_text = self.shell(cmd) required = [ @@ -1069,7 +1073,8 @@ def _test_stack_preview(self, timeout=None, enable_rollback=False): '1', 'resources', 'timeout_mins', - 'disable_rollback' + 'disable_rollback', + 'tags' ] for r in required: @@ -1081,6 +1086,9 @@ def test_stack_preview(self): def test_stack_preview_timeout(self): self._test_stack_preview(300, True) + def test_stack_preview_tags(self): + self._test_stack_preview(tags='tag1,tag2') + def test_stack_create(self): self.register_keystone_auth_fixture() resp = fakes.FakeHTTPResponse( diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 2698e45e..31d1082f 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -257,6 +257,8 @@ def do_stack_adopt(hc, args): action='append') @utils.arg('name', metavar='', help=_('Name of the stack to preview.')) +@utils.arg('--tags', metavar='', + help=_('A list of tags to associate with the stack.')) def do_stack_preview(hc, args): '''Preview the stack.''' tpl_files, template = template_utils.get_template_contents( @@ -280,6 +282,9 @@ def do_stack_preview(hc, args): 'environment': env } + if args.tags: + fields['tags'] = args.tags + stack = hc.stacks.preview(**fields) formatters = { 'description': utils.text_wrap_formatter, From 91e20bc0721c641e4cd8c2f18d29c2f286e47e6e Mon Sep 17 00:00:00 2001 From: zengyingzhe Date: Tue, 25 Aug 2015 11:37:46 +0800 Subject: [PATCH 024/553] Improve the format hint of -Pf option Change the format hint of -Pf option for stack-create/stack-preview /stack-update commands, from to . Closes-Bug: #1486855 Change-Id: Ib6117325d1b582710bd6b9cddfdbc5e9971b3707 --- heatclient/v1/shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 2698e45e..7ed12265 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -85,7 +85,7 @@ def _do(*args, **kwargs): 'This can be specified multiple times, or once with parameters ' 'separated by a semicolon.'), action='append') -@utils.arg('-Pf', '--parameter-file', metavar='', +@utils.arg('-Pf', '--parameter-file', metavar='', help=_('Parameter values from file used to create the stack. ' 'This can be specified multiple times. Parameter value ' 'would be the content of the file'), @@ -250,7 +250,7 @@ def do_stack_adopt(hc, args): 'This can be specified multiple times, or once with parameters ' 'separated by semicolon.'), action='append') -@utils.arg('-Pf', '--parameter-file', metavar='', +@utils.arg('-Pf', '--parameter-file', metavar='', help=_('Parameter values from file used to create the stack. ' 'This can be specified multiple times. Parameter value ' 'would be the content of the file'), @@ -428,7 +428,7 @@ def do_stack_show(hc, args): 'This can be specified multiple times, or once with parameters ' 'separated by a semicolon.'), action='append') -@utils.arg('-Pf', '--parameter-file', metavar='', +@utils.arg('-Pf', '--parameter-file', metavar='', help=_('Parameter values from file used to create the stack. ' 'This can be specified multiple times. Parameter value ' 'would be the content of the file'), From 1495ebbe541050434cfd96e47a13b7860355500b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 26 Aug 2015 14:10:21 +0000 Subject: [PATCH 025/553] Updated from global requirements Change-Id: I911655d728b503056263b07d28777333db11135f --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6062377..de9c3532 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. Babel>=1.3 -pbr<2.0,>=1.4 +pbr<2.0,>=1.6 argparse iso8601>=0.1.9 PrettyTable<0.8,>=0.7 From db84e35fa23d6d3bbd96421edf9a53cd7cd8b7b5 Mon Sep 17 00:00:00 2001 From: ricolin Date: Thu, 27 Aug 2015 16:03:12 +0800 Subject: [PATCH 026/553] Replace deprecated function Function *read_url_content* already deprecated, and should be replaced by function *read_url_content* in utils. Change-Id: I34729251da06bbda054bc7b543a6de588a58c728 --- heatclient/common/template_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index a5a018b0..ca0885df 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -119,7 +119,7 @@ def get_file_contents(from_data, files, base_url=None, str_url = parse.urljoin(base_url, value) if str_url not in files: - file_content = read_url_content(str_url) + file_content = utils.read_url_content(str_url) if is_template(file_content): template = get_template_contents( template_url=str_url, files=files)[1] From d15aacb5ca6987ed6f78c4d760ae57a4fd41b91a Mon Sep 17 00:00:00 2001 From: zengyingzhe Date: Fri, 28 Aug 2015 17:09:56 +0800 Subject: [PATCH 027/553] Fix a nit in resource-list help Change-Id: Ib4a24af335278c7478098539c76367536468e527 Closes-Bug: #1489348 --- heatclient/v1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 2698e45e..e43ad74b 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -731,7 +731,7 @@ def do_template_validate(hc, args): @utils.arg('-n', '--nested-depth', metavar='', help=_('Depth of nested stacks from which to display resources.')) @utils.arg('--with-detail', default=False, action="store_true", - help=_('Enable detail information presented for each resource' + help=_('Enable detail information presented for each resource ' 'in resources list.')) def do_resource_list(hc, args): '''Show list of resources belonging to a stack.''' From 47ecd150c310153c4a00667e9f989e40295c1671 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 2 Sep 2015 16:52:15 +0000 Subject: [PATCH 028/553] Add py34 env to default tox run Also, change order of envs in default tox run to avoid some errors when running tox for the first time on fresh clone: - run pypy first to avoid "No module _bsddb" error - run py34 before py2x to avoid LP bug #1489059 Change-Id: I777c55077edb5d4f09518acf35366af97de3943f Closes-Bug: #1492123 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6110c789..312a41aa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,pypy,pep8 +envlist = pypy,py34,py27,py26,pep8 minversion = 1.6 skipsdist = True From 260f6fdb45563f1a7c2c9944fa3a3a946e8fd58a Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Wed, 8 Oct 2014 13:21:29 -0400 Subject: [PATCH 029/553] Add CLI option for update dry-run When performing a stack-update using the --dry-run option will change the output behavior. Instead of printing a list of stacks, the --dry-run option will print the name, type, and action to be taken on resources in the updated stack. Docs change here https://review.openstack.org/132870/ Engine change here https://review.openstack.org/#/c/122473/ Implements: partial-blueprint update-dry-run Co-Authored-By: Jason Dunsmore Change-Id: I3dd54084ce7bd5140b28e81b6eb2349123494de9 --- heatclient/common/utils.py | 28 +++++++++ heatclient/tests/unit/test_shell.py | 95 +++++++++++++++++++++++++++++ heatclient/v1/shell.py | 24 ++++++++ heatclient/v1/stacks.py | 10 +++ 4 files changed, 157 insertions(+) diff --git a/heatclient/common/utils.py b/heatclient/common/utils.py index 87ce0db6..5a8b09cf 100644 --- a/heatclient/common/utils.py +++ b/heatclient/common/utils.py @@ -20,8 +20,10 @@ import uuid from oslo_serialization import jsonutils +from oslo_utils import encodeutils from oslo_utils import importutils import prettytable +import six from six.moves.urllib import error from six.moves.urllib import parse from six.moves.urllib import request @@ -117,6 +119,32 @@ def event_log_formatter(events): return "\n".join(event_log) +def print_update_list(lst, fields, formatters=None): + """Print the stack-update --dry-run output as a table. + + This function is necessary to print the stack-update --dry-run + output, which contains additional information about the update. + """ + formatters = formatters or {} + pt = prettytable.PrettyTable(fields, caching=False, print_empty=False) + pt.align = 'l' + + for change in lst: + row = [] + for field in fields: + if field in formatters: + row.append(formatters[field](change.get(field, None))) + else: + row.append(change.get(field, None)) + + pt.add_row(row) + + if six.PY3: + print(encodeutils.safe_encode(pt.get_string()).decode()) + else: + print(encodeutils.safe_encode(pt.get_string())) + + def find_resource(manager, name_or_id): """Helper for the _find_* methods.""" # first try to get entity as integer id diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index d24a5de0..21d2839c 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -2248,6 +2248,101 @@ def test_stack_update_with_tags(self): for r in required: self.assertRegexpMatches(update_text, r) + def test_stack_update_dry_run(self): + self.register_keystone_auth_fixture() + + resp_dict = {"stack": { + "id": "2", + "stack_name": "teststack2", + "stack_status": 'CREATE_COMPLETE', + "creation_time": "2012-10-25T01:58:47Z" + }} + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2', 'GET').AndReturn(resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack2/2').AndReturn((resp, resp_dict)) + + template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') + template_data = open(template_file).read() + + replaced_res = {"resource_name": "my_res", + "resource_identity": {"stack_name": "teststack2", + "stack_id": "2", + "tenant": "1234", + "path": "/resources/my_res"}, + "description": "", + "stack_identity": {"stack_name": "teststack2", + "stack_id": "2", + "tenant": "1234", + "path": ""}, + "stack_name": "teststack2", + "creation_time": "2015-08-19T19:43:34.025507", + "resource_status": "COMPLETE", + "updated_time": "2015-08-19T19:43:34.025507", + "resource_type": "OS::Heat::RandomString", + "required_by": [], + "resource_status_reason": "", + "physical_resource_id": "", + "attributes": {"value": None}, + "resource_action": "INIT", + "metadata": {}} + resp_dict = {"resource_changes": {"deleted": [], + "unchanged": [], + "added": [], + "replaced": [replaced_res], + "updated": []}} + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(resp_dict)) + expected_data = { + 'files': {}, + 'environment': {}, + 'template': jsonutils.loads(template_data), + 'parameters': {'"KeyPairName': 'updated_key"'}, + 'disable_rollback': False, + 'existing': True} + + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2/preview', 'PUT', + data=expected_data, headers={}).AndReturn(resp) + else: + http.HTTPClient.json_request( + 'PUT', '/stacks/teststack2/2/preview', + data=expected_data, + headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + ).AndReturn((resp, None)) + + self.m.ReplayAll() + + update_preview_text = self.shell( + 'stack-update teststack2/2 ' + '--template-file=%s ' + '--enable-rollback ' + '--existing ' + '--parameters="KeyPairName=updated_key" ' + '--dry-run ' % template_file) + + required = [ + 'stack_name', + 'id', + 'teststack2', + '2', + 'state', + 'replaced' + ] + for r in required: + self.assertRegexpMatches(update_preview_text, r) + def test_stack_delete(self): self.register_keystone_auth_fixture() resp = fakes.FakeHTTPResponse( diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index e43ad74b..032f21d8 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -423,6 +423,9 @@ def do_stack_show(hc, args): 'Values %(false)s set rollback to disabled. ' 'Default is to use the value of existing stack to be updated.') % {'true': strutils.TRUE_STRINGS, 'false': strutils.FALSE_STRINGS}) +@utils.arg('-y', '--dry-run', default=False, action="store_true", + help='Do not actually perform the stack update, but show what ' + 'would be changed') @utils.arg('-P', '--parameters', metavar='', help=_('Parameter values used to create the stack. ' 'This can be specified multiple times, or once with parameters ' @@ -496,6 +499,27 @@ def do_stack_update(hc, args): if args.enable_rollback: fields['disable_rollback'] = False + if args.dry_run is True: + resource_changes = hc.stacks.preview_update(**fields) + + formatters = { + 'resource_identity': utils.json_formatter + } + + fields = ['state', 'resource_name', 'resource_type', + 'resource_identity'] + + for k in resource_changes.get("resource_changes", {}).keys(): + for i in range(len(resource_changes["resource_changes"][k])): + resource_changes["resource_changes"][k][i]['state'] = k + + utils.print_update_list( + sum(resource_changes["resource_changes"].values(), []), + fields, + formatters=formatters + ) + return + hc.stacks.update(**fields) do_stack_list(hc) diff --git a/heatclient/v1/stacks.py b/heatclient/v1/stacks.py index 35984523..a728fdf4 100644 --- a/heatclient/v1/stacks.py +++ b/heatclient/v1/stacks.py @@ -147,6 +147,16 @@ def update(self, stack_id, **kwargs): self.client.put('/stacks/%s' % stack_id, data=kwargs, headers=headers) + def preview_update(self, stack_id, **kwargs): + """Preview a stack update.""" + s = self.get(stack_id) + headers = self.client.credentials_headers() + resp = self.client.put('/stacks/%s/%s/preview' % + (s.stack_name, s.id), + data=kwargs, headers=headers) + body = utils.get_response_body(resp) + return body + def delete(self, stack_id): """Delete a stack.""" self._delete("/stacks/%s" % stack_id) From 4804dce0b4e73c4a6dfc9e46542f209e8c71f845 Mon Sep 17 00:00:00 2001 From: Takashi NATSUME Date: Tue, 8 Sep 2015 16:09:19 +0900 Subject: [PATCH 030/553] Modify test templates and the path to subunit2html This patch contains the following two fixes because the gate job fails if they are not applied at the same time. 1) Use OS::Heat::TestResource in test templates Use OS::Heat::TestResource instead of OS::Nova::Server in heat_minimal.yaml and heat_minimal_hot.yaml 2) Update path to subunit2html in post_test_hook Per: http://lists.openstack.org/pipermail/openstack-dev/2015-August/072982.html The location of subunit2html changed on the images in the gate so update the path used in the post_test_hook. Long-term we should just use what's in devstack-gate. Change-Id: I67acc3145719c3de1199e36c6667d975652d262f Closes-Bug: #1492115 Closes-Bug: #1491646 --- .../tests/functional/hooks/post_test_hook.sh | 2 +- .../functional/templates/heat_minimal.yaml | 22 ++++++++---------- .../templates/heat_minimal_hot.yaml | 23 ++++++++----------- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/heatclient/tests/functional/hooks/post_test_hook.sh b/heatclient/tests/functional/hooks/post_test_hook.sh index cc466708..505a7afa 100755 --- a/heatclient/tests/functional/hooks/post_test_hook.sh +++ b/heatclient/tests/functional/hooks/post_test_hook.sh @@ -18,7 +18,7 @@ function generate_testr_results { if [ -f .testrepository/0 ]; then sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit - sudo .tox/functional/bin/python /usr/local/jenkins/slave_scripts/subunit2html.py $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html + sudo /usr/os-testr-env/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html sudo gzip -9 $BASE/logs/testrepository.subunit sudo gzip -9 $BASE/logs/testr_results.html sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz diff --git a/heatclient/tests/functional/templates/heat_minimal.yaml b/heatclient/tests/functional/templates/heat_minimal.yaml index d85e22ca..79bc4cb6 100644 --- a/heatclient/tests/functional/templates/heat_minimal.yaml +++ b/heatclient/tests/functional/templates/heat_minimal.yaml @@ -1,18 +1,16 @@ HeatTemplateFormatVersion: '2012-12-12' Description: Minimal template to test validation Parameters: - InstanceImage: - Description: Glance image name + ClientName: + Description: Client to poll Type: String - InstanceType: - Description: Nova instance type - Type: String - Default: m1.small - AllowedValues: [m1.tiny, m1.small, m1.medium, m1.large, m1.nano, m1.xlarge, m1.micro, m1.heat] - ConstraintDescription: must be a valid nova instance type. + WaitSecs: + Description: Seconds to wait after an action (-1 is infinite) + Type: Number + Default: 0 Resources: - InstanceResource: - Type: OS::Nova::Server + TestResource: + Type: OS::Heat::TestResource Properties: - flavor: {Ref: InstanceType} - image: {Ref: InstanceImage} + client_name: {Ref: ClientName} + wait_secs: {Ref: WaitSecs} diff --git a/heatclient/tests/functional/templates/heat_minimal_hot.yaml b/heatclient/tests/functional/templates/heat_minimal_hot.yaml index c4eb8b4d..f17ef14b 100644 --- a/heatclient/tests/functional/templates/heat_minimal_hot.yaml +++ b/heatclient/tests/functional/templates/heat_minimal_hot.yaml @@ -1,19 +1,16 @@ heat_template_version: 2015-04-30 description: A minimal HOT test template parameters: - instance_image: - description: Glance image name + test_client_name: + description: Client to poll type: string - instance_type: - description: Nova instance type - type: string - default: m1.small - constraints: - - allowed_values: [m1.tiny, m1.small, m1.medium, m1.large, m1.nano, m1.xlarge, m1.micro, m1.heat] - description: must be a valid nova instance type. + test_wait_secs: + description: Seconds to wait after an action (-1 is infinite) + type: number + default: 0 resources: - instance: - type: OS::Nova::Server + test_resource: + type: OS::Heat::TestResource properties: - image: { get_param: instance_image } - flavor: { get_param: instance_type } + client_name: { get_param: test_client_name } + wait_secs: { get_param: test_wait_secs } From 5447278fc6558b0c26f834305a228d6aec273d40 Mon Sep 17 00:00:00 2001 From: Kanagaraj Manickam Date: Fri, 28 Aug 2015 16:32:10 +0530 Subject: [PATCH 031/553] Enable filters for heat resource-type-list Adds filtering support for resource-type-list based on resource type's name, version and support_status Depends-On: I7500d5c56ce252bf8c0a9ae7a188a8cb25e643ce implements blueprint heat-resource-type-search DocImpact Change-Id: I0029b90dcdce69fc27a6f511db795faad237dc94 --- heatclient/tests/unit/test_resource_types.py | 22 ++++++++++++++++++++ heatclient/v1/resource_types.py | 19 +++++++++++++---- heatclient/v1/shell.py | 9 +++++++- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/heatclient/tests/unit/test_resource_types.py b/heatclient/tests/unit/test_resource_types.py index d3153038..7bbb591d 100644 --- a/heatclient/tests/unit/test_resource_types.py +++ b/heatclient/tests/unit/test_resource_types.py @@ -70,6 +70,28 @@ def get(self, *args, **kwargs): manager = resource_types.ResourceTypeManager(FakeClient()) manager.list() + def test_list_types_with_filters(self): + filters = {'name': 'OS::Keystone::*', + 'version': '5.0.0', + 'support_status': 'SUPPORTED'} + + manager = resource_types.ResourceTypeManager(None) + with mock.patch.object(manager, '_list') as mock_list: + mock_list.return_value = None + manager.list(filters=filters) + self.assertEqual(1, mock_list.call_count) + url, param = mock_list.call_args[0] + self.assertEqual("resource_types", param) + base_url, query_params = utils.parse_query_url(url) + self.assertEqual('/%s' % manager.KEY, base_url) + # parameters in query_params is in list format, so filter params + # are made to be inline with it + filters_params = {} + for item in filters: + filters_params[item] = [filters[item]] + + self.assertEqual(filters_params, query_params) + @mock.patch.object(utils, 'get_response_body') def test_get(self, mock_utils): key = 'resource_types' diff --git a/heatclient/v1/resource_types.py b/heatclient/v1/resource_types.py index 34397b93..952cc3c5 100644 --- a/heatclient/v1/resource_types.py +++ b/heatclient/v1/resource_types.py @@ -31,26 +31,37 @@ def _add_details(self, info): class ResourceTypeManager(base.BaseManager): resource_class = ResourceType + KEY = 'resource_types' - def list(self): + def list(self, **kwargs): """Get a list of resource types. :rtype: list of :class:`ResourceType` """ - return self._list('/resource_types', 'resource_types') + + url = '/%s' % self.KEY + params = {} + if 'filters' in kwargs: + filters = kwargs.pop('filters') + params.update(filters) + url += '?%s' % parse.urlencode(params, True) + + return self._list(url, self.KEY) def get(self, resource_type): """Get the details for a specific resource_type. :param resource_type: name of the resource type to get the details for """ - url_str = '/resource_types/%s' % ( + url_str = '/%s/%s' % ( + self.KEY, parse.quote(encodeutils.safe_encode(resource_type), '')) resp = self.client.get(url_str) body = utils.get_response_body(resp) return body def generate_template(self, resource_type, template_type='cfn'): - url_str = '/resource_types/%s/template' % ( + url_str = '/%s/%s/template' % ( + self.KEY, parse.quote(encodeutils.safe_encode(resource_type), '')) if template_type: url_str += '?%s' % parse.urlencode( diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index f9189f2b..1094bde1 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -641,9 +641,16 @@ def do_output_show(hc, args): print(value) +@utils.arg('-f', '--filters', metavar='', + help=_('Filter parameters to apply on returned resource types. ' + 'This can be specified multiple times, or once with parameters ' + 'separated by a semicolon. It can be any of name, version and ' + 'support_status'), + action='append') def do_resource_type_list(hc, args): '''List the available resource types.''' - types = hc.resource_types.list() + types = hc.resource_types.list( + filters=utils.format_parameters(args.filters)) utils.print_list(types, ['resource_type'], sortby_index=0) From d6fee3b96f6b9b5721737674063fdb98ac6a4310 Mon Sep 17 00:00:00 2001 From: tengqm Date: Thu, 20 Aug 2015 04:17:36 -0400 Subject: [PATCH 032/553] Fix stack sorting support This patch adds sorting support to stack lists. Change-Id: Ie622d41cc8b5f18bb5aa23dca95c1703112bf7f1 Closes-Bug: #1339403 --- heatclient/tests/unit/test_shell.py | 28 +++++++++++++++++--- heatclient/v1/shell.py | 41 +++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index d24a5de0..d7481ae3 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -212,10 +212,20 @@ class ShellParamValidationTest(TestCase): scenarios = [ ('stack-create', dict( command='stack-create ts -P "ab"', + with_tmpl=True, err='Malformed parameter')), ('stack-update', dict( command='stack-update ts -P "a-b"', + with_tmpl=True, err='Malformed parameter')), + ('stack-list-with-sort-dir', dict( + command='stack-list --sort-dir up', + with_tmpl=False, + err='Sorting direction must be one of')), + ('stack-list-with-sort-key', dict( + command='stack-list --sort-keys owner', + with_tmpl=False, + err='Sorting key \'owner\' not one of')), ] def setUp(self): @@ -233,8 +243,12 @@ def test_bad_parameters(self): 'OS_AUTH_URL': BASE_URL, } self.set_fake_env(fake_env) - template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') - cmd = '%s --template-file=%s ' % (self.command, template_file) + cmd = self.command + + if self.with_tmpl: + template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') + cmd = '%s --template-file=%s ' % (self.command, template_file) + self.shell_error(cmd, self.err) @@ -4413,7 +4427,10 @@ def test_stack_list_with_args(self): ' --not-tags-any=tag7,tag8' ' --global-tenant' ' --show-deleted' - ' --show-hidden') + ' --show-hidden' + ' --sort-keys=stack_name;creation_time' + ' --sort-keys=updated_time' + ' --sort-dir=asc') required = [ 'stack_owner', @@ -4444,7 +4461,10 @@ def test_stack_list_with_args(self): 'not_tags_any': ['tag7,tag8'], 'global_tenant': ['True'], 'show_deleted': ['True'], - 'show_hidden': ['True']} + 'show_hidden': ['True'], + 'sort_keys': ['stack_name', 'creation_time', + 'updated_time'], + 'sort_dir': ['asc']} self.assertEqual(expected_query_dict, query_params) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 2698e45e..bdb84dc6 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -540,6 +540,16 @@ def do_stack_cancel_update(hc, args): help=_('Limit the number of stacks returned.')) @utils.arg('-m', '--marker', metavar='', help=_('Only return stacks that appear after the given stack ID.')) +@utils.arg('-k', '--sort-keys', metavar='', + help=_('List of keys for sorting the returned stacks. ' + 'This can be specified multiple times or once with keys ' + 'separated by semicolons. Valid sorting keys include ' + '"stack_name", "stack_status", "creation_time" and ' + '"updated_time".'), + action='append') +@utils.arg('-d', '--sort-dir', metavar='[asc|desc]', + help=_('Sorting direction (either "asc" or "desc") for the sorting ' + 'keys.')) @utils.arg('-g', '--global-tenant', action='store_true', default=False, help=_('Display stacks from all tenants. Operation only authorized ' 'for users who match the policy in heat\'s policy.json.')) @@ -549,7 +559,11 @@ def do_stack_cancel_update(hc, args): def do_stack_list(hc, args=None): '''List the user's stacks.''' kwargs = {} - fields = ['id', 'stack_name', 'stack_status', 'creation_time'] + fields = ['id', 'stack_name', 'stack_status', 'creation_time', + 'updated_time'] + sort_keys = ['stack_name', 'stack_status', 'creation_time', + 'updated_time'] + sortby_index = 3 if args: kwargs = {'limit': args.limit, 'marker': args.marker, @@ -565,13 +579,36 @@ def do_stack_list(hc, args=None): fields.append('parent') kwargs['show_nested'] = True + if args.sort_keys: + # flatten key list first + keys = [] + for k in args.sort_keys: + if ';' in k: + keys.extend(k.split(';')) + else: + keys.append(k) + # validate key list + for key in keys: + if key not in sort_keys: + err = _("Sorting key '%(key)s' not one of the supported " + "keys: %(keys)s") % {'key': key, "keys": sort_keys} + raise exc.CommandError(err) + kwargs['sort_keys'] = keys + sortby_index = None + + if args.sort_dir: + if args.sort_dir not in ('asc', 'desc'): + raise exc.CommandError(_("Sorting direction must be one of " + "'asc' and 'desc'")) + kwargs['sort_dir'] = args.sort_dir + if args.global_tenant or args.show_owner: fields.insert(2, 'stack_owner') if args.global_tenant: fields.insert(2, 'project') stacks = hc.stacks.list(**kwargs) - utils.print_list(stacks, fields, sortby_index=3) + utils.print_list(stacks, fields, sortby_index=sortby_index) @utils.arg('id', metavar='', From 8eb188457d9cf7056eea401c4ade6154eab24ede Mon Sep 17 00:00:00 2001 From: Rabi Mishra Date: Mon, 14 Sep 2015 11:26:13 +0530 Subject: [PATCH 033/553] Fix deployment create error when using existing config We use an empty dict as config when deployment is created without a config. This fixes 'build_derived_config_params' to accept both SoftwareConfig object and empty dict. Change-Id: Ib9b49e72c117aedc15966c11dafa43eca9e57dac Closes-Bug: #1494672 --- heatclient/common/deployment_utils.py | 3 ++ .../tests/unit/test_deployment_utils.py | 37 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/heatclient/common/deployment_utils.py b/heatclient/common/deployment_utils.py index 02a3f79b..e6d68f89 100644 --- a/heatclient/common/deployment_utils.py +++ b/heatclient/common/deployment_utils.py @@ -19,11 +19,14 @@ from heatclient import exc from heatclient.openstack.common._i18n import _ +from heatclient.v1.software_configs import SoftwareConfig def build_derived_config_params(action, source, name, input_values, server_id, signal_transport, signal_id=None): + if isinstance(source, SoftwareConfig): + source = source.to_dict() input_values = input_values or {} inputs = copy.deepcopy(source.get('inputs')) or [] diff --git a/heatclient/tests/unit/test_deployment_utils.py b/heatclient/tests/unit/test_deployment_utils.py index 69a2899a..5754b170 100644 --- a/heatclient/tests/unit/test_deployment_utils.py +++ b/heatclient/tests/unit/test_deployment_utils.py @@ -19,6 +19,7 @@ from heatclient.common import deployment_utils from heatclient import exc +from heatclient.v1 import software_configs from testtools import matchers @@ -27,13 +28,13 @@ def mock_sc(group=None, config=None, options=None, inputs=None, outputs=None): - return { + return software_configs.SoftwareConfig(None, { 'group': group, 'config': config, 'options': options or {}, 'inputs': inputs or [], 'outputs': outputs or [], - } + }, True) class DerivedConfigTest(testtools.TestCase): @@ -70,6 +71,38 @@ class DerivedConfigTest(testtools.TestCase): 'name': 's1', 'options': {}, 'outputs': []})), + ('defaults_empty', dict( + action='UPDATE', + source={}, + name='s1', + input_values=None, + server_id='1234', + signal_transport='NO_SIGNAL', + signal_id=None, + result={ + 'config': '', + 'group': 'Heat::Ungrouped', + 'inputs': [{ + 'description': 'ID of the server being deployed to', + 'name': 'deploy_server_id', + 'type': 'String', + 'value': '1234' + }, { + 'description': 'Name of the current action ' + 'being deployed', + 'name': 'deploy_action', + 'type': 'String', + 'value': 'UPDATE' + }, { + 'description': 'How the server should signal to ' + 'heat with the deployment output values.', + 'name': 'deploy_signal_transport', + 'type': 'String', + 'value': 'NO_SIGNAL'}], + 'name': 's1', + 'options': {}, + 'outputs': []})), + ('config_values', dict( action='UPDATE', source=mock_sc( From 3e1aa3ad9b5f9c5ab1953f572160bc293ef86f51 Mon Sep 17 00:00:00 2001 From: Jay Dobies Date: Thu, 10 Sep 2015 14:51:08 -0400 Subject: [PATCH 034/553] Ability to specify show_nested for template validation This is the client-side change to support Idb51c983fc07e49d371b8ccb3cea6f7e92f99903 Change-Id: I83744bd552bcb5cee183336bec97ab616bd82a91 Implements-blueprint: nested-validation Closes-Bug: #1495695 --- heatclient/tests/unit/test_shell.py | 3 +- heatclient/tests/unit/test_stacks.py | 42 ++++++++++++++++++++++++++++ heatclient/v1/shell.py | 7 ++++- heatclient/v1/stacks.py | 6 +++- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 67a5f201..0d50d831 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -3824,7 +3824,8 @@ def test_config_create(self): six.StringIO('the config script')) http.SessionClient.request( - '/validate', 'POST', data=validate_template).AndReturn(http_resp) + '/validate', 'POST', + data=validate_template).AndReturn(http_resp) http.SessionClient.request( '/software_configs', 'POST', data=create_dict).AndReturn(http_resp) diff --git a/heatclient/tests/unit/test_stacks.py b/heatclient/tests/unit/test_stacks.py index 12ebdc0c..9bb89e5b 100644 --- a/heatclient/tests/unit/test_stacks.py +++ b/heatclient/tests/unit/test_stacks.py @@ -314,3 +314,45 @@ def test_stack_list_pagination(self): results[0].stack_name) self.assertEqual('stack_%s' % (self.offset + last_result), results[-1].stack_name) + + +class StackManagerValidateTest(testtools.TestCase): + + def setUp(self): + super(StackManagerValidateTest, self).setUp() + + self.mock_response = mock.MagicMock() + self.mock_response.json.return_value = {'result': 'fake_response'} + self.mock_response.headers = {'content-type': 'application/json'} + + self.mock_client = mock.MagicMock() + self.mock_client.post.return_value = self.mock_response + + self.manager = stacks.StackManager(self.mock_client) + + def test_validate_show_nested(self): + # Test + result = self.manager.validate(**{'show_nested': True}) + + # Verify + self.assertEqual(self.mock_response.json.return_value, result) + self.mock_client.post.assert_called_once_with( + '/validate?show_nested=True', data={}) + + def test_validate_show_nested_false(self): + # Test + result = self.manager.validate(**{'show_nested': False}) + + # Verify + self.assertEqual(self.mock_response.json.return_value, result) + self.mock_client.post.assert_called_once_with( + '/validate', data={}) + + def test_validate_show_nested_default(self): + # Test + result = self.manager.validate() + + # Verify + self.assertEqual(self.mock_response.json.return_value, result) + self.mock_client.post.assert_called_once_with( + '/validate', data={}) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 583b4b4c..b358a881 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -741,6 +741,8 @@ def do_template_show(hc, args): action='append') @utils.arg('-o', '--template-object', metavar='', help=_('URL to retrieve template object (e.g. from swift).')) +@utils.arg('-n', '--show-nested', default=False, action="store_true", + help=_('Resolve parameters from nested templates as well.')) def do_template_validate(hc, args): '''Validate a template with parameters.''' @@ -755,9 +757,12 @@ def do_template_validate(hc, args): fields = { 'template': template, 'files': dict(list(tpl_files.items()) + list(env_files.items())), - 'environment': env + 'environment': env, } + if args.show_nested: + fields['show_nested'] = args.show_nested + validation = hc.stacks.validate(**fields) print(jsonutils.dumps(validation, indent=2, ensure_ascii=False)) diff --git a/heatclient/v1/stacks.py b/heatclient/v1/stacks.py index a728fdf4..7ce25e04 100644 --- a/heatclient/v1/stacks.py +++ b/heatclient/v1/stacks.py @@ -227,7 +227,11 @@ def template(self, stack_id): def validate(self, **kwargs): """Validate a stack template.""" - resp = self.client.post('/validate', data=kwargs) + url = '/validate' + if kwargs.pop('show_nested', False): + url += '?show_nested=True' + + resp = self.client.post(url, data=kwargs) body = utils.get_response_body(resp) return body From 8ee2fc1b3d4df186d0bcc3e16353324a456a1fa0 Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Sat, 25 Jul 2015 12:33:56 +0100 Subject: [PATCH 035/553] stack-update -x tolerate no template Replacement for the approach outlined in https://review.openstack.org/#/c/154692/ This works with the following heat changes to enable full PATCH update functionality, including omitting the template or environment: https://review.openstack.org/#/c/205754/ https://review.openstack.org/#/c/154619/ Closes-Bug: #1224828 Change-Id: I2a82936743badb69f0de5a6ca64f95ae63a35358 --- heatclient/common/template_utils.py | 4 +- heatclient/tests/unit/test_shell.py | 45 ++++++++++++++++++++ heatclient/tests/unit/test_template_utils.py | 6 +++ heatclient/v1/shell.py | 10 +++-- 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index ca0885df..e71a3b19 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -28,7 +28,7 @@ def get_template_contents(template_file=None, template_url=None, template_object=None, object_request=None, - files=None): + files=None, existing=False): # Transform a bare file path to a file:// URL. if template_file: @@ -41,6 +41,8 @@ def get_template_contents(template_file=None, template_url=None, template_url = template_object tpl = object_request and object_request('GET', template_object) + elif existing: + return {}, None else: raise exc.CommandError(_('Need to specify exactly one of ' '%(arg1)s, %(arg2)s or %(arg3)s') % diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 24953035..133b0e97 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -2223,6 +2223,51 @@ def test_stack_update_with_patched_and_default_parameters(self): for r in required: self.assertRegexpMatches(update_text, r) + def test_stack_update_with_existing_template(self): + self.register_keystone_auth_fixture() + resp = fakes.FakeHTTPResponse( + 202, + 'Accepted', + {}, + 'The request is accepted for processing.') + expected_data = { + 'files': {}, + 'environment': {}, + 'template': None, + 'parameters': {}} + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack2/2', 'PATCH', + data=expected_data, + headers=headers + ).AndReturn(resp) + else: + self.client.json_request( + 'PATCH', '/stacks/teststack2/2', + data=expected_data, + headers=headers + ).AndReturn((resp, None)) + fakes.script_heat_list(client=self.client) + + self.m.ReplayAll() + + update_text = self.shell( + 'stack-update teststack2/2 ' + '--existing') + + required = [ + 'stack_name', + 'id', + 'teststack2', + '1' + ] + for r in required: + self.assertRegexpMatches(update_text, r) + def test_stack_update_with_tags(self): self.register_keystone_auth_fixture() template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') diff --git a/heatclient/tests/unit/test_template_utils.py b/heatclient/tests/unit/test_template_utils.py index 9f917b32..7b584cc6 100644 --- a/heatclient/tests/unit/test_template_utils.py +++ b/heatclient/tests/unit/test_template_utils.py @@ -436,6 +436,12 @@ def test_get_template_contents_file_none(self): '--template-url or --template-object'), str(ex)) + def test_get_template_contents_file_none_existing(self): + files, tmpl_parsed = template_utils.get_template_contents( + existing=True) + self.assertEqual(None, tmpl_parsed) + self.assertEqual({}, files) + def test_get_template_contents_parse_error(self): with tempfile.NamedTemporaryFile() as tmpl_file: diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 386a0455..59c36073 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -442,11 +442,14 @@ def do_stack_show(hc, args): 'would be the content of the file'), action='append') @utils.arg('-x', '--existing', default=False, action="store_true", - help=_('Re-use the set of parameters of the current stack. ' + help=_('Re-use the template, parameters and environment of the ' + 'current stack. If the template argument is omitted then the ' + 'existing template is used. If no %(env_arg)s is specified then ' + 'the existing environment is used. ' 'Parameters specified in %(arg)s will patch over the existing ' 'values in the current stack. Parameters omitted will keep ' 'the existing values.') - % {'arg': '--parameters'}) + % {'arg': '--parameters', 'env_arg': '--environment-file'}) @utils.arg('-c', '--clear-parameter', metavar='', help=_('Remove the parameters from the set of parameters of ' 'current stack for the %(cmd)s. The default value in the ' @@ -464,7 +467,8 @@ def do_stack_update(hc, args): args.template_file, args.template_url, args.template_object, - _authenticated_fetcher(hc)) + _authenticated_fetcher(hc), + existing=args.existing) env_files, env = template_utils.process_multiple_environments_and_files( env_paths=args.environment_file) From 36864381c53db2b0be8b3eebec37566967293d3d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 18 Sep 2015 16:42:10 +0000 Subject: [PATCH 036/553] Updated from global requirements Change-Id: Ibec012cef14cb3fa0ac48dede0c0ea3b72a33f8e --- requirements.txt | 2 +- setup.py | 2 +- test-requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index de9c3532..e5fad4d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. Babel>=1.3 -pbr<2.0,>=1.6 +pbr>=1.6 argparse iso8601>=0.1.9 PrettyTable<0.8,>=0.7 diff --git a/setup.py b/setup.py index d8080d05..782bb21f 100644 --- a/setup.py +++ b/setup.py @@ -25,5 +25,5 @@ pass setuptools.setup( - setup_requires=['pbr>=1.3'], + setup_requires=['pbr>=1.8'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index 49e38ee0..49c83b60 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ mox3>=0.7.0 oslosphinx>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -tempest-lib>=0.6.1 +tempest-lib>=0.8.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 From 35dd0f21df68bab229b04e6c4cceff3ee477b482 Mon Sep 17 00:00:00 2001 From: zengyingzhe Date: Mon, 28 Sep 2015 18:40:25 +0800 Subject: [PATCH 037/553] Fix the bug when showing event in log format The event time displayed in log format is truncated, because the event log formater of heatclient resolves the event_time string incorrectly. Exchange the date and time fields splitted from event_time attribute, and don't truncate the last character of time_date[1]. Closes-Bug: #1498283 Change-Id: If94093e2b0531a78c26ba9baeceafc19b61df5f8 --- heatclient/common/utils.py | 4 ++-- heatclient/tests/unit/fakes.py | 8 ++++---- heatclient/tests/unit/test_shell.py | 12 ++++++------ heatclient/tests/unit/test_utils.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/heatclient/common/utils.py b/heatclient/common/utils.py index 5a8b09cf..27fc02ed 100644 --- a/heatclient/common/utils.py +++ b/heatclient/common/utils.py @@ -102,8 +102,8 @@ def event_log_formatter(events): event_time = getattr(event, 'event_time', '') time_date = event_time.split('T') try: - event_time = time_date[0] - event_date = time_date[1][:-1] + event_date = time_date[0] + event_time = time_date[1] except IndexError: event_time = event_date = '' diff --git a/heatclient/tests/unit/fakes.py b/heatclient/tests/unit/fakes.py index 2469fad8..74ecf304 100644 --- a/heatclient/tests/unit/fakes.py +++ b/heatclient/tests/unit/fakes.py @@ -100,7 +100,7 @@ def mock_script_event_list( action="CREATE", final_state="COMPLETE", fakehttp=True): resp_dict = {"events": [ - {"event_time": "2013-12-05T14:14:31Z", + {"event_time": "2013-12-05T14:14:31", "id": rsrc_eventid1, "links": [{"href": "http://heat.example.com:8004/foo", "rel": "self"}, @@ -113,7 +113,7 @@ def mock_script_event_list( "resource_name": resource_name if resource_name else "testresource", "resource_status": "%s_IN_PROGRESS" % action, "resource_status_reason": "state changed"}, - {"event_time": "2013-12-05T14:14:32Z", + {"event_time": "2013-12-05T14:14:32", "id": rsrc_eventid2, "links": [{"href": "http://heat.example.com:8004/foo", "rel": "self"}, @@ -133,7 +133,7 @@ def mock_script_event_list( stack_event1 = "0159dccd-65e1-46e8-a094-697d20b009e5" stack_event2 = "8f591a36-7190-4adb-80da-00191fe22388" resp_dict["events"].insert( - 0, {"event_time": "2013-12-05T14:14:30Z", + 0, {"event_time": "2013-12-05T14:14:30", "id": stack_event1, "links": [{"href": "http://heat.example.com:8004/foo", "rel": "self"}, @@ -147,7 +147,7 @@ def mock_script_event_list( "resource_status": "%s_IN_PROGRESS" % action, "resource_status_reason": "state changed"}) resp_dict["events"].append( - {"event_time": "2013-12-05T14:14:33Z", + {"event_time": "2013-12-05T14:14:33", "id": stack_event2, "links": [{"href": "http://heat.example.com:8004/foo", "rel": "self"}, diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 0e4bf033..6a1f01f2 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -425,8 +425,8 @@ def test_event_list(self): eventid2, 'state changed', 'CREATE_IN_PROGRESS', - '2013-12-05T14:14:31Z', - '2013-12-05T14:14:32Z', + '2013-12-05T14:14:31', + '2013-12-05T14:14:32', ] for r in required: @@ -2923,8 +2923,8 @@ def test_event_list(self): 'state changed', 'CREATE_IN_PROGRESS', 'CREATE_COMPLETE', - '2013-12-05T14:14:31Z', - '2013-12-05T14:14:32Z', + '2013-12-05T14:14:31', + '2013-12-05T14:14:32', ] for r in required: self.assertRegexpMatches(event_list_text, r) @@ -2953,9 +2953,9 @@ def test_stack_event_list_log(self): event_list_text = self.shell('event-list {0} --format log'.format( stack_id)) - expected = '14:14:31 2013-12-05 %s [aResource]: ' \ + expected = '2013-12-05 14:14:31 %s [aResource]: ' \ 'CREATE_IN_PROGRESS state changed\n' \ - '14:14:32 2013-12-05 %s [aResource]: CREATE_COMPLETE ' \ + '2013-12-05 14:14:32 %s [aResource]: CREATE_COMPLETE ' \ 'state changed\n' % (self.event_id_one, self.event_id_two) self.assertEqual(expected, event_list_text) diff --git a/heatclient/tests/unit/test_utils.py b/heatclient/tests/unit/test_utils.py index 10946eb6..f1950d1d 100644 --- a/heatclient/tests/unit/test_utils.py +++ b/heatclient/tests/unit/test_utils.py @@ -161,6 +161,35 @@ def test_newline_list_formatter(self): self.assertEqual('one\ntwo', utils.newline_list_formatter(['one', 'two'])) + def test_event_log_formatter(self): + event1 = {'event_time': '2015-09-28T12:12:12', + 'id': '123456789', + 'resource_name': 'res_name', + 'resource_status': 'CREATE_IN_PROGRESS', + 'resource_status_reason': 'CREATE started'} + event2 = {'event_time': '2015-09-28T12:12:22', + 'id': '123456789', + 'resource_name': 'res_name', + 'resource_status': 'CREATE_COMPLETE', + 'resource_status_reason': 'CREATE completed'} + events_list = [hc_res.Resource(manager=None, info=event1), + hc_res.Resource(manager=None, info=event2)] + + expected = ('2015-09-28 12:12:12 123456789 [res_name]: ' + 'CREATE_IN_PROGRESS CREATE started\n' + '2015-09-28 12:12:22 123456789 [res_name]: ' + 'CREATE_COMPLETE CREATE completed') + self.assertEqual(expected, utils.event_log_formatter(events_list)) + self.assertEqual('', utils.event_log_formatter([])) + + notime_event = {'id': '123', + 'resource_name': 'resname', + 'resource_status': 'CREATE_COMPLETE', + 'resource_status_reason': 'state changed'} + notime_event_list = [hc_res.Resource(manager=None, info=notime_event)] + self.assertEqual(' 123 [resname]: CREATE_COMPLETE state changed', + utils.event_log_formatter(notime_event_list)) + class ShellTestParameterFiles(testtools.TestCase): From 7998ec89affef39f814b97050657e8aef5c7e43e Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Fri, 2 Oct 2015 10:33:03 +0100 Subject: [PATCH 038/553] Add PATCH support for stack-update --dry-run Previously we just passed the "existing" kwarg directly to the PUT, which was then ignored by the API, so PATCH update preview commands wouldn't work. Now heat supports PATCH preview_update_stack, we can revise this to use the PATCH method when appropriate. Requires this heat patch for --existing --dry-run to work when used together: https://review.openstack.org/#/c/230054 Change-Id: Ib07c0a83edfbef7a08b4af344850d0a791b7704d Closes-Bug: #1501207 --- heatclient/tests/unit/test_shell.py | 39 ++++++++++++++++++++++++----- heatclient/v1/stacks.py | 11 +++++--- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 0e4bf033..b7e3c03d 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -2315,7 +2315,7 @@ def test_stack_update_with_tags(self): for r in required: self.assertRegexpMatches(update_text, r) - def test_stack_update_dry_run(self): + def _setup_stubs_update_dry_run(self, template_file, existing=False): self.register_keystone_auth_fixture() resp_dict = {"stack": { @@ -2336,7 +2336,6 @@ def test_stack_update_dry_run(self): self.client.json_request( 'GET', '/stacks/teststack2/2').AndReturn((resp, resp_dict)) - template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') template_data = open(template_file).read() replaced_res = {"resource_name": "my_res", @@ -2375,22 +2374,50 @@ def test_stack_update_dry_run(self): 'environment': {}, 'template': jsonutils.loads(template_data), 'parameters': {'"KeyPairName': 'updated_key"'}, - 'disable_rollback': False, - 'existing': True} + 'disable_rollback': False} + + if existing is True: + method = 'PATCH' + else: + method = 'PUT' if self.client == http.SessionClient: self.client.request( - '/stacks/teststack2/2/preview', 'PUT', + '/stacks/teststack2/2/preview', method, data=expected_data, headers={}).AndReturn(resp) else: http.HTTPClient.json_request( - 'PUT', '/stacks/teststack2/2/preview', + method, '/stacks/teststack2/2/preview', data=expected_data, headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} ).AndReturn((resp, None)) self.m.ReplayAll() + def test_stack_update_dry_run(self): + template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') + self._setup_stubs_update_dry_run(template_file) + update_preview_text = self.shell( + 'stack-update teststack2/2 ' + '--template-file=%s ' + '--enable-rollback ' + '--parameters="KeyPairName=updated_key" ' + '--dry-run ' % template_file) + + required = [ + 'stack_name', + 'id', + 'teststack2', + '2', + 'state', + 'replaced' + ] + for r in required: + self.assertRegexpMatches(update_preview_text, r) + + def test_stack_update_dry_run_patch(self): + template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') + self._setup_stubs_update_dry_run(template_file, existing=True) update_preview_text = self.shell( 'stack-update teststack2/2 ' '--template-file=%s ' diff --git a/heatclient/v1/stacks.py b/heatclient/v1/stacks.py index 7ce25e04..28aa83ef 100644 --- a/heatclient/v1/stacks.py +++ b/heatclient/v1/stacks.py @@ -151,9 +151,14 @@ def preview_update(self, stack_id, **kwargs): """Preview a stack update.""" s = self.get(stack_id) headers = self.client.credentials_headers() - resp = self.client.put('/stacks/%s/%s/preview' % - (s.stack_name, s.id), - data=kwargs, headers=headers) + if kwargs.pop('existing', None): + resp = self.client.patch('/stacks/%s/%s/preview' % + (s.stack_name, s.id), + data=kwargs, headers=headers) + else: + resp = self.client.put('/stacks/%s/%s/preview' % + (s.stack_name, s.id), + data=kwargs, headers=headers) body = utils.get_response_body(resp) return body From 1338db81c455428ae4f04248afaf9bef87e75d3f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 2 Oct 2015 17:20:23 +0000 Subject: [PATCH 039/553] Updated from global requirements Change-Id: I532d2ec552c6b11fb72ae90beef3ab691bfa435c --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 49c83b60..5059030d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ mox3>=0.7.0 oslosphinx>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -tempest-lib>=0.8.0 +tempest-lib>=0.9.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 From 6c86efc1a39792cb08e18ff6bd5ca3af0c1739ed Mon Sep 17 00:00:00 2001 From: Ronald Bradford Date: Mon, 5 Oct 2015 13:32:44 -0400 Subject: [PATCH 040/553] Add standard code coverage configuration file Add the .coveragerc configuration consistent with other projects, specifically to define including branch = True and omit including tests in the coverage reports Change-Id: Icb1989e4828ac380acc9a395b241fdf592c7d022 --- .coveragerc | 8 ++++++++ .gitignore | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..4248611c --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[run] +branch = True +source = heatclient +omit = heatclient/tests/*,heatclient/openstack/* + +[report] +ignore_errors = True +precision = 2 diff --git a/.gitignore b/.gitignore index ea9789f3..dee193ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.coverage +.coverage* .venv *,cover cover From a021736d4d19679cdc70b5d401a09bc75cfc5020 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 9 Oct 2015 05:04:19 +0000 Subject: [PATCH 041/553] Updated from global requirements Change-Id: I48ce840e3b3266376ecf60d21f9d56a30d4dacb7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e5fad4d0..2798a50c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,5 @@ oslo.utils>=2.0.0 # Apache-2.0 python-keystoneclient>=1.6.0 python-swiftclient>=2.2.0 PyYAML>=3.1.0 -requests>=2.5.2 +requests!=2.8.0,>=2.5.2 six>=1.9.0 From 54a1715e348720ab31b5d2e38035bf429471cdbe Mon Sep 17 00:00:00 2001 From: Peter Razumovsky Date: Thu, 8 Oct 2015 17:22:59 +0300 Subject: [PATCH 042/553] Add option for detailed template error Add option detailed_template_error for informative error during template and environment parsing. yaml.SafeLoader returns error with template snippet where error has been occurred instead of CSafeLoader. But CSafeLoader is faster. So, if user wants to get more informative error, use slower but more detailed SafeLoader for yaml loading. Change-Id: Ied0a573a00eb5f564dea0c636da1301de5de9ea7 Closes-bug: #1496361 --- heatclient/common/environment_format.py | 14 ++++++++++---- heatclient/common/template_format.py | 14 ++++++++++---- heatclient/tests/unit/test_environment_format.py | 12 ++++++++++++ heatclient/tests/unit/test_template_format.py | 12 ++++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/heatclient/common/environment_format.py b/heatclient/common/environment_format.py index 0b876b6b..5c29aad0 100644 --- a/heatclient/common/environment_format.py +++ b/heatclient/common/environment_format.py @@ -21,15 +21,21 @@ def parse(env_str): - '''Takes a string and returns a dict containing the parsed structure. + """Takes a string and returns a dict containing the parsed structure. This includes determination of whether the string is using the YAML format. - ''' + """ try: env = yaml.load(env_str, Loader=template_format.yaml_loader) - except yaml.YAMLError as yea: - raise ValueError(yea) + except yaml.YAMLError: + # NOTE(prazumovsky): we need to return more informative error for + # user, so use SafeLoader, which return error message with template + # snippet where error has been occurred. + try: + env = yaml.load(env_str, Loader=yaml.SafeLoader) + except yaml.YAMLError as yea: + raise ValueError(yea) else: if env is None: env = {} diff --git a/heatclient/common/template_format.py b/heatclient/common/template_format.py index 2f4a91ac..1636ebb7 100644 --- a/heatclient/common/template_format.py +++ b/heatclient/common/template_format.py @@ -40,18 +40,24 @@ def _construct_yaml_str(self, node): def parse(tmpl_str): - '''Takes a string and returns a dict containing the parsed structure. + """Takes a string and returns a dict containing the parsed structure. This includes determination of whether the string is using the JSON or YAML format. - ''' + """ if tmpl_str.startswith('{'): tpl = json.loads(tmpl_str) else: try: tpl = yaml.load(tmpl_str, Loader=yaml_loader) - except yaml.YAMLError as yea: - raise ValueError(yea) + except yaml.YAMLError: + # NOTE(prazumovsky): we need to return more informative error for + # user, so use SafeLoader, which return error message with template + # snippet where error has been occurred. + try: + tpl = yaml.load(tmpl_str, Loader=yaml.SafeLoader) + except yaml.YAMLError as yea: + raise ValueError(yea) else: if tpl is None: tpl = {} diff --git a/heatclient/tests/unit/test_environment_format.py b/heatclient/tests/unit/test_environment_format.py index 702619dd..faea505d 100644 --- a/heatclient/tests/unit/test_environment_format.py +++ b/heatclient/tests/unit/test_environment_format.py @@ -13,6 +13,7 @@ from heatclient.common import environment_format import mock +import six import testscenarios import testtools import yaml @@ -78,3 +79,14 @@ def test_parse_to_value_exception(self): self.assertRaises(ValueError, environment_format.parse, text) + + +class DetailedYAMLParseExceptions(testtools.TestCase): + + def test_parse_to_value_exception(self): + yaml = """not important +but very: + - incorrect +""" + ex = self.assertRaises(ValueError, environment_format.parse, yaml) + self.assertIn('but very:\n ^', six.text_type(ex)) diff --git a/heatclient/tests/unit/test_template_format.py b/heatclient/tests/unit/test_template_format.py index ca8d1fc2..ffc1ec4f 100644 --- a/heatclient/tests/unit/test_template_format.py +++ b/heatclient/tests/unit/test_template_format.py @@ -11,6 +11,7 @@ # under the License. import mock +import six import testscenarios import testtools import yaml @@ -48,3 +49,14 @@ def test_parse_no_version_format(self): Outputs: {} ''' self.assertRaises(ValueError, template_format.parse, yaml2) + + +class DetailedYAMLParseExceptions(testtools.TestCase): + + def test_parse_to_value_exception(self): + yaml = """not important +but very: + - incorrect +""" + ex = self.assertRaises(ValueError, template_format.parse, yaml) + self.assertIn('but very:\n ^', six.text_type(ex)) From c878172ad5f11c59793491a417f4d9de061f9c58 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 13 Oct 2015 11:04:31 +0000 Subject: [PATCH 043/553] Updated from global requirements Change-Id: I010a57e279836d54b6ceb9471669265ebc627e23 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5059030d..275ad728 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ mox3>=0.7.0 oslosphinx>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -tempest-lib>=0.9.0 +tempest-lib>=0.10.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 From e716cd99dc9c46d64c2111c5e941d1cd4f17e1fd Mon Sep 17 00:00:00 2001 From: ricolin Date: Thu, 15 Oct 2015 17:06:57 +0800 Subject: [PATCH 044/553] improve readme content add more information in README Change-Id: I7a47ad86ef997e68a35d93eea166d17029dca48e --- README.rst | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 1c7c561c..4a953f47 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,15 @@ python-heatclient ================= +.. image:: https://img.shields.io/pypi/v/python-heatclient.svg + :target: https://pypi.python.org/pypi/python-heatclient/ + :alt: Latest Version + +.. image:: https://img.shields.io/pypi/dm/python-heatclient.svg + :target: https://pypi.python.org/pypi/python-heatclient/ + :alt: Downloads + + OpenStack Orchestration API Client Library This is a client library for Heat built on the Heat orchestration API. It @@ -9,6 +18,22 @@ provides a Python API (the ``heatclient`` module) and a command-line tool (``heat``). * Free software: Apache license -* Documentation: http://docs.openstack.org/developer/python-heatclient -* Source: http://git.openstack.org/cgit/openstack/python-heatclient -* Bugs: http://bugs.launchpad.net/python-heatclient +* `PyPi`_ - package installation +* `Online Documentation`_ +* `Launchpad project`_ - release management +* `Blueprints`_ - feature specifications +* `Bugs`_ - issue tracking +* `Source`_ +* `Specs`_ +* `Template`_ +* `How to Contribute`_ + +.. _PyPi: https://pypi.python.org/pypi/python-heatclient +.. _Online Documentation: http://docs.openstack.org/developer/python-heatclient +.. _Launchpad project: https://launchpad.net/python-heatclient +.. _Blueprints: https://blueprints.launchpad.net/python-heatclient +.. _Bugs: https://bugs.launchpad.net/python-heatclient +.. _Source: https://git.openstack.org/cgit/openstack/python-heatclient +.. _How to Contribute: http://docs.openstack.org/infra/manual/developers.html +.. _Specs: http://specs.openstack.org/openstack/heat-specs/ +.. _Template: https://git.openstack.org/cgit/openstack/heat-templates/ From 1c89d67d4dab5828d0ca3f1637b719567adb03aa Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Thu, 15 Oct 2015 18:18:35 +0000 Subject: [PATCH 045/553] Fix error in stack-list metadata In the --sort-keys option of the stack-list command, the metadata lists keys separated by commas instead of semicolons. Change-Id: I166d4dc48b97786a29c5d9ef6a5fb9411e80d6b1 --- heatclient/v1/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 312c8b08..a0d3a6c6 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -573,7 +573,7 @@ def do_stack_cancel_update(hc, args): help=_('Limit the number of stacks returned.')) @utils.arg('-m', '--marker', metavar='', help=_('Only return stacks that appear after the given stack ID.')) -@utils.arg('-k', '--sort-keys', metavar='', +@utils.arg('-k', '--sort-keys', metavar='', help=_('List of keys for sorting the returned stacks. ' 'This can be specified multiple times or once with keys ' 'separated by semicolons. Valid sorting keys include ' From dc6ec87f593dddb086d692d24a099067d580d030 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 16 Oct 2015 01:00:36 +0000 Subject: [PATCH 046/553] Updated from global requirements Change-Id: Idea8884ed0da6b17206725952440e8c907ac2f4f --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2798a50c..e329db74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,8 +9,8 @@ iso8601>=0.1.9 PrettyTable<0.8,>=0.7 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=2.0.0 # Apache-2.0 -python-keystoneclient>=1.6.0 +oslo.utils>=2.4.0 # Apache-2.0 +python-keystoneclient!=1.8.0,>=1.6.0 python-swiftclient>=2.2.0 PyYAML>=3.1.0 requests!=2.8.0,>=2.5.2 From 87b559d940e9ed45ffa56b2dc4980ec7bca21a1a Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Tue, 28 Jul 2015 13:27:00 +1200 Subject: [PATCH 047/553] OpenStackClient plugin for stack show This change was derived from Id188cdc0e97b480875a7626443f68e69863f0647 which added stack support to python-openstackclient, but adapting it to be an openstackclient plugin which lives in the python-heatclient tree. This change only implements the "openstack stack show". Subsequent commands will remain in WIP changes at the end of this series until they are ready to merge. The stack show formatting has the following behaviour: - specifying key order for important values, but adding all other stack keys in default order (future proofing for new values) - exclude template_description, its a dupe of 'description' - complex values like parameters, outputs and links get a special formatter depending on the output format ('table' formats them as yaml, 'shell', 'value', 'html' formats them as json) Co-Authored-By: Ryan S. Brown Co-Authored-By: Rico Lin Change-Id: I3096b94146a94d184c29b8c7c9f6c032eed5281d --- heatclient/common/utils.py | 4 + heatclient/osc/__init__.py | 0 heatclient/osc/plugin.py | 59 +++++ heatclient/osc/v1/__init__.py | 0 heatclient/osc/v1/stack.py | 84 ++++++++ heatclient/tests/unit/osc/__init__.py | 0 heatclient/tests/unit/osc/fakes.py | 237 +++++++++++++++++++++ heatclient/tests/unit/osc/utils.py | 93 ++++++++ heatclient/tests/unit/osc/v1/__init__.py | 0 heatclient/tests/unit/osc/v1/fakes.py | 45 ++++ heatclient/tests/unit/osc/v1/test_stack.py | 83 ++++++++ heatclient/tests/unit/test_utils.py | 6 + requirements.txt | 2 + setup.cfg | 7 + 14 files changed, 620 insertions(+) create mode 100644 heatclient/osc/__init__.py create mode 100644 heatclient/osc/plugin.py create mode 100644 heatclient/osc/v1/__init__.py create mode 100644 heatclient/osc/v1/stack.py create mode 100644 heatclient/tests/unit/osc/__init__.py create mode 100644 heatclient/tests/unit/osc/fakes.py create mode 100644 heatclient/tests/unit/osc/utils.py create mode 100644 heatclient/tests/unit/osc/v1/__init__.py create mode 100644 heatclient/tests/unit/osc/v1/fakes.py create mode 100644 heatclient/tests/unit/osc/v1/test_stack.py diff --git a/heatclient/common/utils.py b/heatclient/common/utils.py index 27fc02ed..5a6efc8c 100644 --- a/heatclient/common/utils.py +++ b/heatclient/common/utils.py @@ -71,6 +71,10 @@ def json_formatter(js): separators=(', ', ': ')) +def yaml_formatter(js): + return yaml.safe_dump(js, default_flow_style=False) + + def text_wrap_formatter(d): return '\n'.join(textwrap.wrap(d or '', 55)) diff --git a/heatclient/osc/__init__.py b/heatclient/osc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/heatclient/osc/plugin.py b/heatclient/osc/plugin.py new file mode 100644 index 00000000..dd980efc --- /dev/null +++ b/heatclient/osc/plugin.py @@ -0,0 +1,59 @@ +# 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 openstackclient.common import utils + +LOG = logging.getLogger(__name__) + +DEFAULT_ORCHESTRATION_API_VERSION = '1' +API_VERSION_OPTION = 'os_orchestration_api_version' +API_NAME = 'orchestration' +API_VERSIONS = { + '1': 'heatclient.v1.client.Client', +} + + +def make_client(instance): + """Returns an orchestration service client""" + heat_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS) + LOG.debug('Instantiating orchestration client: %s', heat_client) + + client = heat_client( + endpoint=instance.get_endpoint_for_service_type('orchestration'), + session=instance.session, + auth_url=instance._auth_url, + username=instance._username, + password=instance._password, + region_name=instance._region_name, + ) + + return client + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-orchestration-api-version', + metavar='', + default=utils.env( + 'OS_ORCHESTRATION_API_VERSION', + default=DEFAULT_ORCHESTRATION_API_VERSION), + help='Orchestration API version, default=' + + DEFAULT_ORCHESTRATION_API_VERSION + + ' (Env: OS_ORCHESTRATION_API_VERSION)') + return parser diff --git a/heatclient/osc/v1/__init__.py b/heatclient/osc/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py new file mode 100644 index 00000000..c6546277 --- /dev/null +++ b/heatclient/osc/v1/stack.py @@ -0,0 +1,84 @@ +# 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. +# + +"""Orchestration v1 Stack action implementations""" + +import logging + +from cliff import show +from openstackclient.common import exceptions as exc +from openstackclient.common import utils + +from heatclient.common import utils as heat_utils +from heatclient import exc as heat_exc + + +class ShowStack(show.ShowOne): + """Show stack details""" + + log = logging.getLogger(__name__ + ".ShowStack") + + def get_parser(self, prog_name): + parser = super(ShowStack, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help='Stack to display (name or ID)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + return _show_stack(heat_client, stack_id=parsed_args.stack, + format=parsed_args.formatter) + + +def _show_stack(heat_client, stack_id, format): + try: + data = heat_client.stacks.get(stack_id=stack_id) + except heat_exc.HTTPNotFound: + raise exc.CommandError('Stack not found: %s' % stack_id) + else: + + columns = [ + 'id', + 'stack_name', + 'description', + 'creation_time', + 'updated_time', + 'stack_status', + 'stack_status_reason', + 'parameters', + 'outputs', + 'links', + ] + exclude_columns = ('template_description',) + for key in data.to_dict(): + # add remaining columns without an explicit order + if key not in columns and key not in exclude_columns: + columns.append(key) + formatters = {} + complex_formatter = None + if format in 'table': + complex_formatter = heat_utils.yaml_formatter + elif format in ('shell', 'value', 'html'): + complex_formatter = heat_utils.json_formatter + if complex_formatter: + formatters['parameters'] = complex_formatter + formatters['outputs'] = complex_formatter + formatters['links'] = complex_formatter + + return columns, utils.get_item_properties(data, columns, + formatters=formatters) diff --git a/heatclient/tests/unit/osc/__init__.py b/heatclient/tests/unit/osc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/heatclient/tests/unit/osc/fakes.py b/heatclient/tests/unit/osc/fakes.py new file mode 100644 index 00000000..73865906 --- /dev/null +++ b/heatclient/tests/unit/osc/fakes.py @@ -0,0 +1,237 @@ +# Copyright 2013 Nebula Inc. +# +# 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 sys + +import requests +import six + + +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" +USERNAME = "itchy" +PASSWORD = "scratchy" +TEST_RESPONSE_DICT = { + "access": { + "metadata": { + "is_admin": 0, + "roles": [ + "1234", + ] + }, + "serviceCatalog": [ + { + "endpoints": [ + { + "adminURL": AUTH_URL + "/v2.0", + "id": "1234", + "internalURL": AUTH_URL + "/v2.0", + "publicURL": AUTH_URL + "/v2.0", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + } + ], + "token": { + "expires": "2035-01-01T00:00:01Z", + "id": AUTH_TOKEN, + "issued_at": "2013-01-01T00:00:01.692048", + "tenant": { + "description": None, + "enabled": True, + "id": "1234", + "name": "testtenant" + } + }, + "user": { + "id": "5678", + "name": USERNAME, + "roles": [ + { + "name": "testrole" + }, + ], + "roles_links": [], + "username": USERNAME + } + } +} +TEST_RESPONSE_DICT_V3 = { + "token": { + "audit_ids": [ + "a" + ], + "catalog": [ + ], + "expires_at": "2034-09-29T18:27:15.978064Z", + "extras": {}, + "issued_at": "2014-09-29T17:27:15.978097Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "bbb", + "name": "project" + }, + "roles": [ + ], + "user": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "aaa", + "name": USERNAME + } + } +} +TEST_VERSIONS = { + "versions": { + "values": [ + { + "id": "v3.0", + "links": [ + { + "href": AUTH_URL, + "rel": "self" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v3+json" + }, + { + "base": "application/xml", + "type": "application/vnd.openstack.identity-v3+xml" + } + ], + "status": "stable", + "updated": "2013-03-06T00:00:00Z" + }, + { + "id": "v2.0", + "links": [ + { + "href": AUTH_URL, + "rel": "self" + }, + { + "href": "http://docs.openstack.org/", + "rel": "describedby", + "type": "text/html" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v2.0+json" + }, + { + "base": "application/xml", + "type": "application/vnd.openstack.identity-v2.0+xml" + } + ], + "status": "stable", + "updated": "2014-04-17T00:00:00Z" + } + ] + } +} + + +class FakeStdout: + def __init__(self): + self.content = [] + + def write(self, text): + self.content.append(text) + + def make_string(self): + result = '' + for line in self.content: + result = result + line + return result + + +class FakeApp(object): + def __init__(self, _stdout): + self.stdout = _stdout + self.client_manager = None + self.stdin = sys.stdin + self.stdout = _stdout or sys.stdout + self.stderr = sys.stderr + + +class FakeClient(object): + def __init__(self, **kwargs): + self.endpoint = kwargs['endpoint'] + self.token = kwargs['token'] + + +class FakeClientManager(object): + def __init__(self): + self.compute = None + self.identity = None + self.image = None + self.object_store = None + self.volume = None + self.network = None + self.session = None + self.auth_ref = None + + +class FakeModule(object): + def __init__(self, name, version): + self.name = name + self.__version__ = version + + +class FakeResource(object): + def __init__(self, manager, info, loaded=False): + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + + def _add_details(self, info): + for (k, v) in six.iteritems(info): + setattr(self, k, v) + + def __repr__(self): + reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and + k != 'manager') + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + +class FakeResponse(requests.Response): + def __init__(self, headers={}, status_code=200, data=None, encoding=None): + super(FakeResponse, self).__init__() + + self.status_code = status_code + + self.headers.update(headers) + self._content = json.dumps(data) + if not isinstance(self._content, six.binary_type): + self._content = self._content.encode() diff --git a/heatclient/tests/unit/osc/utils.py b/heatclient/tests/unit/osc/utils.py new file mode 100644 index 00000000..be0ca815 --- /dev/null +++ b/heatclient/tests/unit/osc/utils.py @@ -0,0 +1,93 @@ +# Copyright 2012-2013 OpenStack Foundation +# Copyright 2013 Nebula Inc. +# +# 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 fixtures +import sys +import testtools + +from heatclient.tests.unit.osc import fakes + + +class TestCase(testtools.TestCase): + def setUp(self): + testtools.TestCase.setUp(self) + + if (os.environ.get("OS_STDOUT_CAPTURE") == "True" or + os.environ.get("OS_STDOUT_CAPTURE") == "1"): + stdout = self.useFixture(fixtures.StringStream("stdout")).stream + self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout)) + + if (os.environ.get("OS_STDERR_CAPTURE") == "True" or + os.environ.get("OS_STDERR_CAPTURE") == "1"): + stderr = self.useFixture(fixtures.StringStream("stderr")).stream + self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr)) + + def assertNotCalled(self, m, msg=None): + """Assert a function was not called""" + + if m.called: + if not msg: + msg = 'method %s should not have been called' % m + self.fail(msg) + + # 2.6 doesn't have the assert dict equals so make sure that it exists + if tuple(sys.version_info)[0:2] < (2, 7): + + def assertIsInstance(self, obj, cls, msg=None): + """self.assertTrue(isinstance(obj, cls)), with a nicer message""" + + if not isinstance(obj, cls): + standardMsg = '%s is not an instance of %r' % (obj, cls) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertDictEqual(self, d1, d2, msg=None): + # Simple version taken from 2.7 + self.assertIsInstance(d1, dict, + 'First argument is not a dictionary') + self.assertIsInstance(d2, dict, + 'Second argument is not a dictionary') + if d1 != d2: + if msg: + self.fail(msg) + else: + standardMsg = '%r != %r' % (d1, d2) + self.fail(standardMsg) + + +class TestCommand(TestCase): + """Test cliff command classes""" + + def setUp(self): + super(TestCommand, self).setUp() + # Build up a fake app + self.fake_stdout = fakes.FakeStdout() + self.app = fakes.FakeApp(self.fake_stdout) + self.app.client_manager = fakes.FakeClientManager() + + def check_parser(self, cmd, args, verify_args): + cmd_parser = cmd.get_parser('check_parser') + try: + parsed_args = cmd_parser.parse_args(args) + except SystemExit: + raise Exception("Argument parse failed") + for av in verify_args: + attr, value = av + if attr: + self.assertIn(attr, parsed_args) + self.assertEqual(getattr(parsed_args, attr), value) + return parsed_args diff --git a/heatclient/tests/unit/osc/v1/__init__.py b/heatclient/tests/unit/osc/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/heatclient/tests/unit/osc/v1/fakes.py b/heatclient/tests/unit/osc/v1/fakes.py new file mode 100644 index 00000000..1888c135 --- /dev/null +++ b/heatclient/tests/unit/osc/v1/fakes.py @@ -0,0 +1,45 @@ +# Copyright 2014 OpenStack Foundation +# +# 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 mock + +from heatclient.tests.unit.osc import fakes +from heatclient.tests.unit.osc import utils + + +class FakeOrchestrationv1Client(object): + def __init__(self, **kwargs): + self.http_client = mock.Mock() + self.http_client.auth_token = kwargs['token'] + self.http_client.management_url = kwargs['endpoint'] + self.stacks = mock.Mock() + self.stacks.list = mock.Mock(return_value=[]) + self.resources = fakes.FakeResource(None, {}) + self.resource_types = fakes.FakeResource(None, {}) + self.events = fakes.FakeResource(None, {}) + self.actions = fakes.FakeResource(None, {}) + self.build_info = fakes.FakeResource(None, {}) + self.software_deployments = fakes.FakeResource(None, {}) + self.software_configs = fakes.FakeResource(None, {}) + + +class TestOrchestrationv1(utils.TestCommand): + def setUp(self): + super(TestOrchestrationv1, self).setUp() + + self.app.client_manager.orchestration = FakeOrchestrationv1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py new file mode 100644 index 00000000..ca6b1e0b --- /dev/null +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -0,0 +1,83 @@ +# 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 mock +import testscenarios + +from heatclient.osc.v1 import stack +from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes +from heatclient.v1 import stacks + +load_tests = testscenarios.load_tests_apply_scenarios + + +class TestStack(orchestration_fakes.TestOrchestrationv1): + def setUp(self): + super(TestStack, self).setUp() + self.mock_client = self.app.client_manager.orchestration + self.stack_client = self.app.client_manager.orchestration.stacks + + +class TestStackShow(TestStack): + + scenarios = [ + ('table', dict( + format='table')), + ('shell', dict( + format='shell')), + ('value', dict( + format='value')), + ] + + get_response = {"stack": { + "disable_rollback": True, + "description": "This is a\ndescription\n", + "parent": None, + "tags": None, + "stack_name": "a", + "stack_user_project_id": "02ad9bd403d44ff9ba128cf9ce77f989", + "stack_status_reason": "Stack UPDATE completed successfully", + "creation_time": "2015-08-04T04:46:10", + "links": [{ + "href": "http://192.0.2.1:8004/v1/5dcd28/stacks/a/4af43781", + "rel": "self" + }], + "capabilities": [], + "notification_topics": [], + "updated_time": "2015-08-05T21:33:28", + "timeout_mins": None, + "stack_status": "UPDATE_COMPLETE", + "stack_owner": None, + "parameters": { + "OS::project_id": "e0e5e140c5854c259a852621b65dcd28", + "OS::stack_id": "4af43781", + "OS::stack_name": "a" + }, + "id": "4af43781", + "outputs": [], + "template_description": "This is a\ndescription\n"} + } + + def setUp(self): + super(TestStackShow, self).setUp() + self.cmd = stack.ShowStack(self.app, None) + + def test_stack_show(self): + arglist = ['--format', self.format, 'my_stack'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.stack_client.get = mock.Mock( + return_value=stacks.Stack(None, self.get_response)) + self.cmd.take_action(parsed_args) + self.stack_client.get.assert_called_with(**{ + 'stack_id': 'my_stack', + }) diff --git a/heatclient/tests/unit/test_utils.py b/heatclient/tests/unit/test_utils.py index f1950d1d..762ddfcf 100644 --- a/heatclient/tests/unit/test_utils.py +++ b/heatclient/tests/unit/test_utils.py @@ -144,6 +144,12 @@ def test_json_formatter(self): self.assertEqual(u'{\n "Uni": "test\u2665"\n}', utils.json_formatter({"Uni": u"test\u2665"})) + def test_yaml_formatter(self): + self.assertEqual('null\n...\n', utils.yaml_formatter(None)) + self.assertEqual('{}\n', utils.yaml_formatter({})) + self.assertEqual('foo: bar\n', + utils.yaml_formatter({"foo": "bar"})) + def test_text_wrap_formatter(self): self.assertEqual('', utils.text_wrap_formatter(None)) self.assertEqual('', utils.text_wrap_formatter('')) diff --git a/requirements.txt b/requirements.txt index e329db74..4816cff2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ Babel>=1.3 pbr>=1.6 +cliff>=1.14.0 # Apache-2.0 argparse iso8601>=0.1.9 PrettyTable<0.8,>=0.7 @@ -12,6 +13,7 @@ oslo.serialization>=1.4.0 # Apache-2.0 oslo.utils>=2.4.0 # Apache-2.0 python-keystoneclient!=1.8.0,>=1.6.0 python-swiftclient>=2.2.0 +python-openstackclient>=1.5.0 PyYAML>=3.1.0 requests!=2.8.0,>=2.5.2 six>=1.9.0 diff --git a/setup.cfg b/setup.cfg index bbae0a8e..00b33a5c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,13 @@ packages = console_scripts = heat = heatclient.shell:main +openstack.cli.extension = + orchestration = heatclient.osc.plugin + +openstack.orchestration.v1 = + stack_show = heatclient.osc.v1.stack:ShowStack + + [global] setup-hooks = pbr.hooks.setup_hook From efc82629a588240d689067b702b4c12d97af4414 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 19 Oct 2015 23:32:34 +0000 Subject: [PATCH 048/553] Updated from global requirements Change-Id: Ia134464b2e318eb2eb2a53e1f82f555ee679f7c8 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4816cff2..a86403e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,13 +4,13 @@ Babel>=1.3 pbr>=1.6 -cliff>=1.14.0 # Apache-2.0 +cliff>=1.14.0 # Apache-2.0 argparse iso8601>=0.1.9 PrettyTable<0.8,>=0.7 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 -oslo.utils>=2.4.0 # Apache-2.0 +oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0 python-keystoneclient!=1.8.0,>=1.6.0 python-swiftclient>=2.2.0 python-openstackclient>=1.5.0 From 6cb85feaf88a016edb51eae47895250b5e02abe5 Mon Sep 17 00:00:00 2001 From: Jay Dobies Date: Thu, 22 Oct 2015 15:37:18 -0400 Subject: [PATCH 049/553] Ignore .eggs Runing tox is causing a directory named .eggs to be created, so adding it to the git ignore list. Change-Id: I0a3b1981828365121cdffd53fae932ba0ea0db46 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dee193ab..42bff162 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ run_tests.err.log doc/source/api doc/build *.egg +*.eggs heatclient/versioninfo *.egg-info *.log From d53b542f29cfc9250a8a671fd5f195802c34e6bf Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 23 Oct 2015 17:58:25 +0000 Subject: [PATCH 050/553] Updated from global requirements Change-Id: I326a3d98a657dcbd29319241179404e8c4e6e7a0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a86403e5..215aff13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ argparse iso8601>=0.1.9 PrettyTable<0.8,>=0.7 oslo.i18n>=1.5.0 # Apache-2.0 -oslo.serialization>=1.4.0 # Apache-2.0 +oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0 python-keystoneclient!=1.8.0,>=1.6.0 python-swiftclient>=2.2.0 From 4c53ed854c439e01ebf98b2ff54da3ba36a916b3 Mon Sep 17 00:00:00 2001 From: lvdongbing Date: Wed, 28 Oct 2015 21:37:45 -0400 Subject: [PATCH 051/553] Support to list software configs As API has supported to list software configs, client should support too. Closes-Bug: #1464248 Change-Id: I977b960f0e2f0e754a603c01122eb41f0ea9dbf5 --- .../tests/unit/test_software_configs.py | 11 +++++++++ heatclient/v1/shell.py | 16 +++++++++++++ heatclient/v1/software_configs.py | 23 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/heatclient/tests/unit/test_software_configs.py b/heatclient/tests/unit/test_software_configs.py index 936ed50b..5adcad61 100644 --- a/heatclient/tests/unit/test_software_configs.py +++ b/heatclient/tests/unit/test_software_configs.py @@ -47,6 +47,17 @@ def setUp(self): super(SoftwareConfigManagerTest, self).setUp() self.manager = software_configs.SoftwareConfigManager(mock.MagicMock()) + def test_list(self): + config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' + self.manager.client.json_request.return_value = ( + {}, + {'software_configs': []}) + result = self.manager.list(limit=1, marker=config_id) + self.assertEqual([], result) + call_args = self.manager.client.get.call_args + self.assertEqual( + ('/software_configs?limit=1&marker=%s' % config_id,), *call_args) + @mock.patch.object(utils, 'get_response_body') def test_get(self, mock_body): config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index a0d3a6c6..0c6e87b5 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -1216,6 +1216,22 @@ def do_config_create(hc, args): print(jsonutils.dumps(sc.to_dict(), indent=2)) +@utils.arg('-l', '--limit', metavar='', + help=_('Limit the number of configs returned.')) +@utils.arg('-m', '--marker', metavar='', + help=_('Return configs that appear after the given config ID.')) +def do_config_list(hc, args): + '''List software configs.''' + kwargs = {} + if args.limit: + kwargs['limit'] = args.limit + if args.marker: + kwargs['marker'] = args.marker + scs = hc.software_configs.list(**kwargs) + fields = ['id', 'name', 'group', 'creation_time'] + utils.print_list(scs, fields, sortby_index=None) + + @utils.arg('id', metavar='', help=_('ID of the config.')) @utils.arg('-c', '--config-only', default=False, action="store_true", diff --git a/heatclient/v1/software_configs.py b/heatclient/v1/software_configs.py index dd3264db..bc948283 100644 --- a/heatclient/v1/software_configs.py +++ b/heatclient/v1/software_configs.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import six +from six.moves.urllib import parse + from heatclient.common import utils from heatclient.openstack.common.apiclient import base @@ -28,6 +31,26 @@ def data(self, **kwargs): class SoftwareConfigManager(base.BaseManager): resource_class = SoftwareConfig + def list(self, **kwargs): + """Get a list of software configs. + :rtype: list of :class:`SoftwareConfig` + """ + qparams = {} + + for opt, val in six.iteritems(kwargs): + if val: + qparams[opt] = val + + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + if qparams: + new_qparams = sorted(qparams.items(), key=lambda x: x[0]) + query_string = "?%s" % parse.urlencode(new_qparams) + else: + query_string = "" + url = '/software_configs%s' % query_string + return self._list(url, "software_configs") + def get(self, config_id): """Get the details for a specific software config. From 67b2db0db434802c026e8d1aaf8963a3083f65a5 Mon Sep 17 00:00:00 2001 From: zengyingzhe Date: Mon, 2 Nov 2015 09:54:14 +0800 Subject: [PATCH 052/553] Fix environment files parsing bug for stack-adopt Should use process_multiple_environments_and_files to parse the environment file arguments instead. Change-Id: I2f00df78462f1468ff12f7f9f8ca8a5d4c2d181b Closes-Bug: #1510818 --- heatclient/tests/unit/test_shell.py | 31 ++++++++++++++++++++++ heatclient/tests/unit/var/environment.json | 3 +++ heatclient/v1/shell.py | 4 +-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 heatclient/tests/unit/var/environment.json diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 508602e1..ad0a0b6b 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -1834,6 +1834,37 @@ def test_stack_adopt(self): for r in required: self.assertRegexpMatches(adopt_text, r) + def test_stack_adopt_with_environment(self): + self.register_keystone_auth_fixture() + resp = fakes.FakeHTTPResponse( + 201, + 'Created', + {'location': 'http://no.where/v1/tenant_id/stacks/teststack/1'}, + None) + if self.client is http.HTTPClient: + headers = {'X-Auth-Key': 'password', 'X-Auth-User': 'username'} + else: + headers = {} + if self.client == http.SessionClient: + self.client.request( + '/stacks', 'POST', data=mox.IgnoreArg(), + headers=headers).AndReturn(resp) + else: + self.client.json_request( + 'POST', '/stacks', data=mox.IgnoreArg(), + headers=headers + ).AndReturn((resp, None)) + + fakes.script_heat_list(client=self.client) + self.m.ReplayAll() + + adopt_data_file = os.path.join(TEST_VAR_DIR, 'adopt_stack_data.json') + environment_file = os.path.join(TEST_VAR_DIR, 'environment.json') + self.shell( + 'stack-adopt teststack ' + '--adopt-file=%s ' + '--environment-file=%s' % (adopt_data_file, environment_file)) + def test_stack_adopt_without_data(self): self.register_keystone_auth_fixture() failed_msg = 'Need to specify --adopt-file' diff --git a/heatclient/tests/unit/var/environment.json b/heatclient/tests/unit/var/environment.json new file mode 100644 index 00000000..c56e3b85 --- /dev/null +++ b/heatclient/tests/unit/var/environment.json @@ -0,0 +1,3 @@ +{ + "parameters": {} +} \ No newline at end of file diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 0c6e87b5..9c76f89e 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -193,8 +193,8 @@ def hooks_to_env(env, arg_hooks, hook): help=_('Name of the stack to adopt.')) def do_stack_adopt(hc, args): '''Adopt a stack.''' - env_files, env = template_utils.process_environment_and_files( - env_path=args.environment_file) + env_files, env = template_utils.process_multiple_environments_and_files( + env_paths=args.environment_file) if not args.adopt_file: raise exc.CommandError(_('Need to specify %(arg)s') % From 7a249cf7e973f3dc2c677ccf76134653e1393982 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Thu, 15 Oct 2015 18:16:05 +0000 Subject: [PATCH 053/553] OpenStackClient plugin for stack list This change implements the "openstack stack list" command. Blueprint: heat-support-python-openstackclient Change-Id: Ib3001dfaae0a4242b44fb3bb85713229c49ad8e8 --- heatclient/osc/v1/stack.py | 146 +++++++++++++++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 139 ++++++++++++++++++++ setup.cfg | 1 + 3 files changed, 286 insertions(+) diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index c6546277..d9381b69 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -15,12 +15,15 @@ import logging +from cliff import lister from cliff import show from openstackclient.common import exceptions as exc +from openstackclient.common import parseractions from openstackclient.common import utils from heatclient.common import utils as heat_utils from heatclient import exc as heat_exc +from heatclient.openstack.common._i18n import _ class ShowStack(show.ShowOne): @@ -82,3 +85,146 @@ def _show_stack(heat_client, stack_id, format): return columns, utils.get_item_properties(data, columns, formatters=formatters) + + +class ListStack(lister.Lister): + """List stacks.""" + + log = logging.getLogger(__name__ + '.ListStack') + + def get_parser(self, prog_name): + parser = super(ListStack, self).get_parser(prog_name) + parser.add_argument( + '--deleted', + action='store_true', + help=_('Include soft-deleted stacks in the stack listing') + ) + parser.add_argument( + '--nested', + action='store_true', + help=_('Include nested stacks in the stack listing') + ) + parser.add_argument( + '--hidden', + action='store_true', + help=_('Include hidden stacks in the stack listing') + ) + parser.add_argument( + '--property', + dest='properties', + metavar='', + help=_('Filter properties to apply on returned stacks (repeat to ' + 'filter on multiple properties)'), + action=parseractions.KeyValueAction + ) + parser.add_argument( + '--tags', + metavar='', + help=_('List of tags to filter by. Can be combined with ' + '--tag-mode to specify how to filter tags') + ) + parser.add_argument( + '--tag-mode', + metavar='', + help=_('Method of filtering tags. Must be one of "any", "not", ' + 'or "not-any". If not specified, multiple tags will be ' + 'combined with the boolean AND expression') + ) + parser.add_argument( + '--limit', + metavar='', + help=_('The number of stacks returned') + ) + parser.add_argument( + '--marker', + metavar='', + help=_('Only return stacks that appear after the given ID') + ) + parser.add_argument( + '--sort', + metavar='[:]', + help=_('Sort output by selected keys and directions (asc or desc) ' + '(default: asc). Specify multiple times to sort on ' + 'multiple properties') + ) + parser.add_argument( + '--all-projects', + action='store_true', + help=_('Include all projects (admin only)') + ) + parser.add_argument( + '--short', + action='store_true', + help=_('List fewer fields in output') + ) + parser.add_argument( + '--long', + action='store_true', + help=_('List additional fields in output, this is implied by ' + '--all-projects') + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + client = self.app.client_manager.orchestration + return _list(client, args=parsed_args) + + +def _list(client, args=None): + kwargs = {} + columns = [ + 'ID', + 'Stack Name', + 'Stack Status', + 'Creation Time', + 'Updated Time', + ] + + if args: + kwargs = {'limit': args.limit, + 'marker': args.marker, + 'filters': heat_utils.format_parameters(args.properties), + 'tags': None, + 'tags_any': None, + 'not_tags': None, + 'not_tags_any': None, + 'global_tenant': args.all_projects or args.long, + 'show_deleted': args.deleted, + 'show_hidden': args.hidden} + + if args.tags: + if args.tag_mode: + if args.tag_mode == 'any': + kwargs['tags_any'] = args.tags + elif args.tag_mode == 'not': + kwargs['not_tags'] = args.tags + elif args.tag_mode == 'not-any': + kwargs['not_tags_any'] = args.tags + else: + err = _('tag mode must be one of "any", "not", "not-any"') + raise exc.CommandError(err) + else: + kwargs['tags'] = args.tags + + if args.short: + columns.pop() + columns.pop() + if args.long: + columns.insert(2, 'Stack Owner') + if args.long or args.all_projects: + columns.insert(2, 'Project') + + if args.nested: + columns.append('Parent') + kwargs['show_nested'] = True + + data = client.stacks.list(**kwargs) + data = utils.sort_items(data, args.sort if args else None) + + return ( + columns, + (utils.get_dict_properties(s, columns) for s in data) + ) diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index ca6b1e0b..95514d5c 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -11,9 +11,13 @@ # under the License. # +import copy import mock import testscenarios +from openstackclient.common import exceptions as exc +from openstackclient.common import utils + from heatclient.osc.v1 import stack from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes from heatclient.v1 import stacks @@ -81,3 +85,138 @@ def test_stack_show(self): self.stack_client.get.assert_called_with(**{ 'stack_id': 'my_stack', }) + + +class TestStackList(TestStack): + + defaults = { + 'limit': None, + 'marker': None, + 'filters': {}, + 'tags': None, + 'tags_any': None, + 'not_tags': None, + 'not_tags_any': None, + 'global_tenant': False, + 'show_deleted': False, + 'show_hidden': False, + } + + columns = ['ID', 'Stack Name', 'Stack Status', 'Creation Time', + 'Updated Time'] + + data = { + 'id': '1234', + 'stack_name': 'my_stack', + 'stack_status': 'CREATE_COMPLETE', + 'creation_time': '2015-10-21T07:28:00Z', + 'update_time': '2015-10-21T07:30:00Z' + } + + def setUp(self): + super(TestStackList, self).setUp() + self.cmd = stack.ListStack(self.app, None) + self.stack_client.list = mock.MagicMock(return_value=[self.data]) + utils.get_dict_properties = mock.MagicMock(return_value='') + + def test_stack_list_defaults(self): + arglist = [] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.stack_client.list.assert_called_with(**self.defaults) + self.assertEqual(self.columns, columns) + + def test_stack_list_nested(self): + kwargs = copy.deepcopy(self.defaults) + kwargs['show_nested'] = True + cols = copy.deepcopy(self.columns) + cols.append('Parent') + arglist = ['--nested'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.stack_client.list.assert_called_with(**kwargs) + self.assertEqual(cols, columns) + + def test_stack_list_all_projects(self): + kwargs = copy.deepcopy(self.defaults) + kwargs['global_tenant'] = True + cols = copy.deepcopy(self.columns) + cols.insert(2, 'Project') + arglist = ['--all-projects'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.stack_client.list.assert_called_with(**kwargs) + self.assertEqual(cols, columns) + + def test_stack_list_long(self): + kwargs = copy.deepcopy(self.defaults) + kwargs['global_tenant'] = True + cols = copy.deepcopy(self.columns) + cols.insert(2, 'Stack Owner') + cols.insert(2, 'Project') + arglist = ['--long'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.stack_client.list.assert_called_with(**kwargs) + self.assertEqual(cols, columns) + + def test_stack_list_short(self): + cols = ['ID', 'Stack Name', 'Stack Status'] + arglist = ['--short'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.stack_client.list.assert_called_with(**self.defaults) + self.assertEqual(cols, columns) + + def test_stack_list_sort(self): + arglist = ['--sort', 'stack_name:desc,id'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.stack_client.list.assert_called_with(**self.defaults) + self.assertEqual(self.columns, columns) + + def test_stack_list_sort_invalid_key(self): + arglist = ['--sort', 'bad_key'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + def test_stack_list_tags(self): + kwargs = copy.deepcopy(self.defaults) + kwargs['tags'] = 'tag1,tag2' + arglist = ['--tags', 'tag1,tag2'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.stack_client.list.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) + + def test_stack_list_tags_mode(self): + kwargs = copy.deepcopy(self.defaults) + kwargs['not_tags'] = 'tag1,tag2' + arglist = ['--tags', 'tag1,tag2', '--tag-mode', 'not'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.stack_client.list.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) + + def test_stack_list_tags_bad_mode(self): + arglist = ['--tags', 'tag1,tag2', '--tag-mode', 'bad_mode'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) diff --git a/setup.cfg b/setup.cfg index 00b33a5c..39aa368e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ openstack.cli.extension = openstack.orchestration.v1 = stack_show = heatclient.osc.v1.stack:ShowStack + stack_list = heatclient.osc.v1.stack:ListStack [global] From 11dfd95c469eea456841a9f6c7e04f34d62077c2 Mon Sep 17 00:00:00 2001 From: Bartlomiej Biernacki Date: Thu, 12 Nov 2015 09:43:23 +0100 Subject: [PATCH 054/553] 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 0a42fb8412c35eb4455144e54ead5d13bd623ae0 Mon Sep 17 00:00:00 2001 From: zengyingzhe Date: Fri, 23 Oct 2015 09:38:41 +0800 Subject: [PATCH 055/553] Delete the corresponding config when deleting a deployment Change-Id: I26049a60f8bb5702ede69d46e3a989bd09a2b3af Closes-Bug: #1504384 --- heatclient/tests/unit/test_shell.py | 69 +++++++++++++++++++++++------ heatclient/v1/shell.py | 19 ++++++-- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index ad0a0b6b..9803123f 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -4270,21 +4270,53 @@ def test_deploy_show(self): def test_deploy_delete(self): self.register_keystone_auth_fixture() headers = {'content-type': 'application/json'} + + deploy_resp_dict = {'software_deployment': { + 'config_id': 'dummy_config_id' + }} + + deploy_resp_string = jsonutils.dumps(deploy_resp_dict) + deploy_http_resp = fakes.FakeHTTPResponse(200, 'OK', + headers, deploy_resp_string) + http_resp = fakes.FakeHTTPResponse(204, 'OK', headers, None) response = (http_resp, '') + + def _get_deployment_request_except(id): + self.client.request('/software_deployments/%s' % id, + 'GET').AndRaise(exc.HTTPNotFound()) + + def _delete_deployment_request_except(id): + self.client.request('/software_deployments/%s' % id, + 'GET').AndReturn(deploy_http_resp) + self.client.request('/software_deployments/%s' % id, + 'DELETE').AndRaise(exc.HTTPNotFound()) + + def _delete_config_request_except(id): + self.client.request('/software_deployments/%s' % id, + 'GET').AndReturn(deploy_http_resp) + self.client.request('/software_deployments/%s' % id, + 'DELETE').AndReturn(http_resp) + self.client.request('/software_configs/dummy_config_id', + 'DELETE').AndRaise(exc.HTTPNotFound()) + + def _delete_request_success(id): + self.client.request('/software_deployments/%s' % id, + 'GET').AndReturn(deploy_http_resp) + self.client.request('/software_deployments/%s' % id, + 'DELETE').AndReturn(http_resp) + self.client.request('/software_configs/dummy_config_id', + 'DELETE').AndReturn(http_resp) + if self.client == http.SessionClient: - self.client.request( - '/software_deployments/defg', - 'DELETE').AndReturn(http_resp) - self.client.request( - '/software_deployments/qwer', - 'DELETE').AndReturn(http_resp) - self.client.request( - '/software_deployments/defg', - 'DELETE').AndRaise(exc.HTTPNotFound()) - self.client.request( - '/software_deployments/qwer', - 'DELETE').AndRaise(exc.HTTPNotFound()) + _get_deployment_request_except('defg') + _get_deployment_request_except('qwer') + _delete_deployment_request_except('defg') + _delete_deployment_request_except('qwer') + _delete_config_request_except('defg') + _delete_config_request_except('qwer') + _delete_request_success('defg') + _delete_request_success('qwer') else: self.client.raw_request( 'DELETE', '/software_deployments/defg').AndReturn(response) @@ -4299,9 +4331,20 @@ def test_deploy_delete(self): self.m.ReplayAll() - self.assertEqual('', self.shell('deployment-delete defg qwer')) self.assertRaises(exc.CommandError, self.shell, 'deployment-delete defg qwer') + self.assertRaises(exc.CommandError, self.shell, + 'deployment-delete defg qwer') + + output = self.shell('deployment-delete defg qwer') + self.assertRegexpMatches(output, 'Failed to delete the correlative ' + 'config dummy_config_id of ' + 'deployment defg') + self.assertRegexpMatches(output, 'Failed to delete the correlative ' + 'config dummy_config_id of ' + 'deployment qwer') + + self.assertEqual('', self.shell('deployment-delete defg qwer')) def test_deploy_metadata(self): self.register_keystone_auth_fixture() diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 9c76f89e..1c84b529 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -1363,15 +1363,28 @@ def do_deployment_metadata_show(hc, args): @utils.arg('id', metavar='', nargs='+', help=_('IDs of the deployments to delete.')) def do_deployment_delete(hc, args): - '''Delete a software deployment.''' + '''Delete software deployments.''' failure_count = 0 for deploy_id in args.id: try: + sd = hc.software_deployments.get(deployment_id=deploy_id) hc.software_deployments.delete(deployment_id=deploy_id) - except exc.HTTPNotFound as e: + except Exception as e: + if isinstance(e, exc.HTTPNotFound): + print(_('Deployment with ID %s not found') % deploy_id) failure_count += 1 - print(e) + continue + + # just try best to delete the corresponding config + try: + config_id = getattr(sd, 'config_id') + hc.software_configs.delete(config_id=config_id) + except Exception: + print(_('Failed to delete the correlative config' + ' %(config_id)s of deployment %(deploy_id)s') % + {'config_id': config_id, 'deploy_id': deploy_id}) + if failure_count == len(args.id): raise exc.CommandError(_("Unable to delete any of the specified " "deployments.")) From 66ead66248614fd05b84f50d41a7eb76cee8d815 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Thu, 12 Nov 2015 14:08:50 -0500 Subject: [PATCH 056/553] Last sync from oslo-incubator oslo-incubator will cease to host common code soon. This is hopefully the very last sync from oslo-incubator. Change-Id: Ieb615e1a43521fde14f1961760e8c3d5e1269f62 --- heatclient/openstack/common/cliutils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/heatclient/openstack/common/cliutils.py b/heatclient/openstack/common/cliutils.py index 0b2763c6..5f1170f1 100644 --- a/heatclient/openstack/common/cliutils.py +++ b/heatclient/openstack/common/cliutils.py @@ -140,7 +140,7 @@ def isunauthenticated(func): def print_list(objs, fields, formatters=None, sortby_index=0, mixed_case_fields=None, field_labels=None): - """Print a list or objects as a table, one row per object. + """Print a list of objects as a table, one row per object. :param objs: iterable of :class:`Resource` :param fields: attributes that correspond to columns, in order @@ -186,16 +186,17 @@ def print_list(objs, fields, formatters=None, sortby_index=0, print(encodeutils.safe_encode(pt.get_string(**kwargs))) -def print_dict(dct, dict_property="Property", wrap=0): +def print_dict(dct, dict_property="Property", wrap=0, dict_value='Value'): """Print a `dict` as a table of two columns. :param dct: `dict` to print :param dict_property: name of the first column :param wrap: wrapping for the second column + :param dict_value: header label for the value (second) column """ - pt = prettytable.PrettyTable([dict_property, 'Value']) + pt = prettytable.PrettyTable([dict_property, dict_value]) pt.align = 'l' - for k, v in six.iteritems(dct): + for k, v in sorted(dct.items()): # convert dict to str to check length if isinstance(v, dict): v = six.text_type(v) From 508aa1ab1aee38f5e38ff457b869c44fa6ee52a3 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 16 Nov 2015 16:39:30 +1300 Subject: [PATCH 057/553] Refine event_log_formatter output - Fix timestamp printing so that date is first, and time doesn't have truncated seconds - Remove event UUID from output since it is of limited usefulness and causes clutter Change-Id: I646be059546289b4131956ba050ec1f1f74ab3b9 Closes-Bug: #1516481 --- heatclient/common/utils.py | 14 +++----------- heatclient/tests/unit/test_shell.py | 16 ++++++++-------- heatclient/tests/unit/test_utils.py | 12 ++---------- 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/heatclient/common/utils.py b/heatclient/common/utils.py index 5a6efc8c..524585f0 100644 --- a/heatclient/common/utils.py +++ b/heatclient/common/utils.py @@ -100,20 +100,12 @@ def print_dict(d, formatters=None): def event_log_formatter(events): """Return the events in log format.""" event_log = [] - log_format = _("%(event_date)s %(event_time)s %(event_id)s " - "[%(rsrc_name)s]: %(rsrc_status)s %(rsrc_status_reason)s") + log_format = ("%(event_time)s " + "[%(rsrc_name)s]: %(rsrc_status)s %(rsrc_status_reason)s") for event in events: event_time = getattr(event, 'event_time', '') - time_date = event_time.split('T') - try: - event_date = time_date[0] - event_time = time_date[1] - except IndexError: - event_time = event_date = '' - log = log_format % { - 'event_date': event_date, 'event_time': event_time, - 'event_id': getattr(event, 'id', ''), + 'event_time': event_time.replace('T', ' '), 'rsrc_name': getattr(event, 'resource_name', ''), 'rsrc_status': getattr(event, 'resource_status', ''), 'rsrc_status_reason': getattr(event, 'resource_status_reason', '') diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index ad0a0b6b..8ab25942 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -1220,13 +1220,13 @@ def test_create_success_with_poll(self): '2', 'teststack2', 'IN_PROGRESS', - '14:14:30', '2013-12-05', '0159dccd-65e1-46e8-a094-697d20b009e5', + '14:14:30', '2013-12-05', 'CREATE_IN_PROGRESS', 'state changed', - '14:14:31', '7fecaeed-d237-4559-93a5-92d5d9111205', + '14:14:31', 'testresource', - '14:14:32', 'e953547a-18f8-40a7-8e63-4ec4f509648b', + '14:14:32', 'CREATE_COMPLETE', - '14:14:33', '8f591a36-7190-4adb-80da-00191fe22388' + '14:14:33', ] for r in required: @@ -3011,10 +3011,10 @@ def test_stack_event_list_log(self): event_list_text = self.shell('event-list {0} --format log'.format( stack_id)) - expected = '2013-12-05 14:14:31 %s [aResource]: ' \ - 'CREATE_IN_PROGRESS state changed\n' \ - '2013-12-05 14:14:32 %s [aResource]: CREATE_COMPLETE ' \ - 'state changed\n' % (self.event_id_one, self.event_id_two) + expected = ('2013-12-05 14:14:31 [aResource]: ' + 'CREATE_IN_PROGRESS state changed\n' + '2013-12-05 14:14:32 [aResource]: CREATE_COMPLETE ' + 'state changed\n') self.assertEqual(expected, event_list_text) diff --git a/heatclient/tests/unit/test_utils.py b/heatclient/tests/unit/test_utils.py index 762ddfcf..96605a52 100644 --- a/heatclient/tests/unit/test_utils.py +++ b/heatclient/tests/unit/test_utils.py @@ -181,21 +181,13 @@ def test_event_log_formatter(self): events_list = [hc_res.Resource(manager=None, info=event1), hc_res.Resource(manager=None, info=event2)] - expected = ('2015-09-28 12:12:12 123456789 [res_name]: ' + expected = ('2015-09-28 12:12:12 [res_name]: ' 'CREATE_IN_PROGRESS CREATE started\n' - '2015-09-28 12:12:22 123456789 [res_name]: ' + '2015-09-28 12:12:22 [res_name]: ' 'CREATE_COMPLETE CREATE completed') self.assertEqual(expected, utils.event_log_formatter(events_list)) self.assertEqual('', utils.event_log_formatter([])) - notime_event = {'id': '123', - 'resource_name': 'resname', - 'resource_status': 'CREATE_COMPLETE', - 'resource_status_reason': 'state changed'} - notime_event_list = [hc_res.Resource(manager=None, info=notime_event)] - self.assertEqual(' 123 [resname]: CREATE_COMPLETE state changed', - utils.event_log_formatter(notime_event_list)) - class ShellTestParameterFiles(testtools.TestCase): From d06bcd87d98a337a93cfc7709d6f440a972bb127 Mon Sep 17 00:00:00 2001 From: Peter Razumovsky Date: Tue, 20 Oct 2015 11:26:43 +0300 Subject: [PATCH 058/553] Add output API calls implementations Add output APIs support for output-show and output-list commands. Now output-show and output-list uses output calls instead of requesting stack get. implements bp api-call-output Change-Id: I1bc1cee1c78ecf0c9a18ecc0a871d38e4141e0f7 Depends on: Ia24b24f59e2c592801e4e670ba5510f642ecf45c --- heatclient/tests/unit/test_shell.py | 456 +++++++++++++++++++++------ heatclient/tests/unit/test_stacks.py | 13 + heatclient/v1/shell.py | 78 +++-- heatclient/v1/stacks.py | 21 ++ 4 files changed, 440 insertions(+), 128 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 9803123f..67554469 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -800,69 +800,93 @@ def test_stack_show(self): for r in required: self.assertRegexpMatches(list_text, r) - def _output_fake_response(self): + def _output_fake_response(self, output_key): - resp_dict = {"stack": { + outputs = [ + { + "output_value": "value1", + "output_key": "output1", + "description": "test output 1", + }, + { + "output_value": ["output", "value", "2"], + "output_key": "output2", + "description": "test output 2", + }, + { + "output_value": u"test\u2665", + "output_key": "output_uni", + "description": "test output unicode", + }, + ] + + stack_dict = {"stack": { "id": "1", "stack_name": "teststack", "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z", - "outputs": [ - { - "output_value": "value1", - "output_key": "output1", - "description": "test output 1", - }, - { - "output_value": ["output", "value", "2"], - "output_key": "output2", - "description": "test output 2", - }, - { - "output_value": u"test\u2665", - "output_key": "output_uni", - "description": "test output unicode", - }, - ], "creation_time": "2012-10-25T01:58:47Z" }} + stack_resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict)) + + def find_output(key): + for out in outputs: + if out['output_key'] == key: + return {'output': out} + resp = fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, - jsonutils.dumps(resp_dict)) + jsonutils.dumps(find_output(output_key))) if self.client == http.SessionClient: self.client.request( - '/stacks/teststack/1', 'GET').MultipleTimes().AndReturn( - resp) + '/stacks/teststack/1', + 'GET').AndReturn(stack_resp) + self.client.request( + '/stacks/teststack/1/outputs/%s' % output_key, + 'GET').AndReturn(resp) else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn((stack_resp, + stack_dict)) self.client.json_request( - 'GET', '/stacks/teststack/1').MultipleTimes().AndReturn( - (resp, resp_dict)) + 'GET', + '/stacks/teststack/1/outputs/%s' % output_key).AndReturn( + (resp, find_output(output_key))) self.m.ReplayAll() - def _error_output_fake_response(self): + def _error_output_fake_response(self, output_key): - resp_dict = {"stack": { + resp_dict = { + "output": { + "output_value": "null", + "output_key": "output1", + "description": "test output 1", + "output_error": "The Referenced Attribute (0 PublicIP) " + "is incorrect." + } + } + + stack_dict = {"stack": { "id": "1", "stack_name": "teststack", "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z", - "outputs": [ - { - "output_value": "null", - "output_key": "output1", - "description": "test output 1", - "output_error": "The Referenced Attribute (0 PublicIP) " - "is incorrect." - }, - ], "creation_time": "2012-10-25T01:58:47Z" }} + stack_resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict)) + resp = fakes.FakeHTTPResponse( 200, 'OK', @@ -871,67 +895,22 @@ def _error_output_fake_response(self): if self.client == http.SessionClient: self.client.request( - '/stacks/teststack/1', 'GET').AndReturn(resp) + '/stacks/teststack/1', + 'GET').AndReturn(stack_resp) + self.client.request( + '/stacks/teststack/1/outputs/%s' % output_key, + 'GET').AndReturn(resp) else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn((stack_resp, + stack_dict)) self.client.json_request( - 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) + 'GET', + '/stacks/teststack/1/outputs/%s' % output_key).AndReturn( + (resp, resp_dict)) self.m.ReplayAll() - def test_output_list(self): - self.register_keystone_auth_fixture() - self._output_fake_response() - list_text = self.shell('output-list teststack/1') - for r in ['output1', 'output2', 'output_uni']: - self.assertRegexpMatches(list_text, r) - - def test_output_show(self): - self.register_keystone_auth_fixture() - - self._output_fake_response() - list_text = self.shell('output-show teststack/1 output1') - self.assertEqual('"value1"\n', list_text) - - list_text = self.shell('output-show -F raw teststack/1 output1') - self.assertEqual('value1\n', list_text) - - list_text = self.shell('output-show -F raw teststack/1 output2') - self.assertEqual('[\n "output", \n "value", \n "2"\n]\n', - list_text) - - list_text = self.shell('output-show -F json teststack/1 output2') - self.assertEqual('[\n "output", \n "value", \n "2"\n]\n', - list_text) - - def test_output_show_unicode(self): - self.register_keystone_auth_fixture() - self._output_fake_response() - list_text = self.shell('output-show teststack/1 output_uni') - self.assertEqual(u'"test\u2665"\n', list_text) - - def test_output_show_all(self): - self.register_keystone_auth_fixture() - self._output_fake_response() - list_text = self.shell('output-show teststack/1 --all') - for r in ['output1', 'value1', 'output2', 'test output unicode']: - self.assertRegexpMatches(list_text, r) - - def test_output_show_missing_arg(self): - self.register_keystone_auth_fixture() - error = self.assertRaises( - exc.CommandError, self.shell, 'output-show teststack/1') - self.assertIn('either or --all argument is needed.', - str(error)) - - def test_output_show_error(self): - self.register_keystone_auth_fixture() - self._error_output_fake_response() - error = self.assertRaises( - exc.CommandError, self.shell, - 'output-show teststack/1 output1') - self.assertIn('The Referenced Attribute (0 PublicIP) is incorrect.', - str(error)) - def test_template_show_cfn(self): self.register_keystone_auth_fixture() template_data = open(os.path.join(TEST_VAR_DIR, @@ -2793,6 +2772,297 @@ def test_stack_restore(self): resp = self.shell('stack-restore teststack/1 2') self.assertEqual("", resp) + def test_output_list(self): + self.register_keystone_auth_fixture() + + stack_dict = {"stack": { + "id": "1", + "stack_name": "teststack", + "stack_status": 'CREATE_COMPLETE', + "creation_time": "2012-10-25T01:58:47Z" + }} + + resp_dict = {"outputs": [{ + "output_key": "key", + "description": "description" + }, + { + "output_key": "key1", + "description": "description1" + }]} + + stack_resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict)) + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(resp_dict)) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', + 'GET').AndReturn(stack_resp) + self.client.request( + '/stacks/teststack/1/outputs', + 'GET').AndReturn(resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn((stack_resp, + stack_dict)) + http.HTTPClient.json_request( + 'GET', + '/stacks/teststack/1/outputs').AndReturn((resp, resp_dict)) + + self.m.ReplayAll() + list_text = self.shell('output-list teststack/1') + + required = [ + 'output_key', + 'description', + 'key', + 'description', + 'key1', + 'description1' + ] + for r in required: + self.assertRegexpMatches(list_text, r) + + def test_output_show_all(self): + self.register_keystone_auth_fixture() + + stack_dict = {"stack": { + "id": "1", + "stack_name": "teststack", + "stack_status": 'CREATE_COMPLETE', + "creation_time": "2012-10-25T01:58:47Z" + }} + + resp_dict = {'outputs': [ + { + 'output_key': 'key', + 'description': 'description' + } + ]} + + resp_dict1 = {"output": { + "output_key": "key", + "output_value": "value", + 'description': 'description' + }} + + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(resp_dict)) + + resp1 = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(resp_dict1)) + + if self.client == http.SessionClient: + self.client.request('/stacks/teststack/1', 'GET').AndReturn( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict))) + self.client.request( + '/stacks/teststack/1/outputs', + 'GET').AndReturn(resp) + self.client.request('/stacks/teststack/1', 'GET').AndReturn( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict))) + self.client.request( + '/stacks/teststack/1/outputs/key', + 'GET').AndReturn(resp1) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn(( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict)), stack_dict)) + http.HTTPClient.json_request( + 'GET', + '/stacks/teststack/1/outputs').AndReturn((resp, resp_dict)) + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn(( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict)), stack_dict)) + http.HTTPClient.json_request( + 'GET', + '/stacks/teststack/1/outputs/key').AndReturn((resp1, + resp_dict1)) + + self.m.ReplayAll() + list_text = self.shell('output-show teststack/1 key --all') + required = [ + 'output_key', + 'output_value', + 'description', + 'key', + 'value', + 'description', + ] + for r in required: + self.assertRegexpMatches(list_text, r) + + def test_output_show(self): + self.register_keystone_auth_fixture() + + stack_dict = {"stack": { + "id": "1", + "stack_name": "teststack", + "stack_status": 'CREATE_COMPLETE', + "creation_time": "2012-10-25T01:58:47Z" + }} + + resp_dict = {"output": { + "output_key": "key", + "output_value": "value", + 'description': 'description' + }} + + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(resp_dict)) + if self.client == http.SessionClient: + self.client.request('/stacks/teststack/1', 'GET').AndReturn( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict))) + self.client.request( + '/stacks/teststack/1/outputs/key', + 'GET').AndReturn(resp) + else: + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn(( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict)), stack_dict)) + http.HTTPClient.json_request( + 'GET', + '/stacks/teststack/1/outputs/key').AndReturn((resp, resp_dict)) + + self.m.ReplayAll() + resp = self.shell('output-show teststack/1 key') + required = [ + 'output_key', + 'output_value', + 'description', + 'key', + 'value', + 'description', + ] + for r in required: + self.assertRegexpMatches(resp, r) + + def test_output_show_output1(self): + self.register_keystone_auth_fixture() + + self._output_fake_response('output1') + list_text = self.shell('output-show teststack/1 output1') + required = [ + 'output_key', + 'output_value', + 'description', + 'output1', + 'value1', + 'test output 1', + ] + for r in required: + self.assertRegexpMatches(list_text, r) + + def test_output_show_output1_only_value(self): + self.register_keystone_auth_fixture() + + self._output_fake_response('output1') + list_text = self.shell('output-show -v -F raw teststack/1 output1') + self.assertEqual('value1\n', list_text) + + def test_output_show_output2_raw_only_value(self): + self.register_keystone_auth_fixture() + + self._output_fake_response('output2') + list_text = self.shell('output-show -F raw -v teststack/1 output2') + self.assertEqual('[\n "output", \n "value", \n "2"\n]\n', + list_text) + + def test_output_show_output2_raw(self): + self.register_keystone_auth_fixture() + + self._output_fake_response('output2') + list_text = self.shell('output-show -F raw teststack/1 output2') + required = [ + 'output_key', + 'output_value', + 'description', + 'output2', + "[u'output', u'value', u'2']", + 'test output 2', + ] + for r in required: + self.assertRegexpMatches(list_text, r) + + def test_output_show_output2_json_only_value(self): + self.register_keystone_auth_fixture() + + self._output_fake_response('output2') + list_text = self.shell('output-show -F json -v teststack/1 output2') + self.assertEqual('[\n "output", \n "value", \n "2"\n]\n', + list_text) + + def test_output_show_output2_json(self): + self.register_keystone_auth_fixture() + + self._output_fake_response('output2') + list_text = self.shell('output-show -F json teststack/1 output2') + required = [ + 'output_key', + 'output_value', + 'description', + 'output2', + '[\n "output", \n "value", \n "2"\n ]' + 'test output 2', + ] + for r in required: + self.assertRegexpMatches(list_text, r) + + def test_output_show_unicode_output_only_value(self): + self.register_keystone_auth_fixture() + + self._output_fake_response('output_uni') + list_text = self.shell('output-show -v teststack/1 output_uni') + self.assertEqual(u'"test\u2665"\n', list_text) + + def test_output_show_error(self): + self.register_keystone_auth_fixture() + self._error_output_fake_response('output1') + error = self.assertRaises( + exc.CommandError, self.shell, + 'output-show teststack/1 output1') + self.assertIn('The Referenced Attribute (0 PublicIP) is incorrect.', + six.text_type(error)) + class ShellTestActions(ShellBase): diff --git a/heatclient/tests/unit/test_stacks.py b/heatclient/tests/unit/test_stacks.py index 9bb89e5b..b55c7d06 100644 --- a/heatclient/tests/unit/test_stacks.py +++ b/heatclient/tests/unit/test_stacks.py @@ -137,6 +137,19 @@ def test_snapshot_list(self): stack.snapshot_list() manager.snapshot_list.assert_called_once_with('the_stack/abcd1234') + def test_output_list(self): + manager = mock.MagicMock() + stack = mock_stack(manager, 'the_stack', 'abcd1234') + stack.output_list() + manager.output_list.assert_called_once_with('the_stack/abcd1234') + + def test_output_show(self): + manager = mock.MagicMock() + stack = mock_stack(manager, 'the_stack', 'abcd1234') + stack.output_show('out123') + manager.output_show.assert_called_once_with('the_stack/abcd1234', + 'out123') + class StackManagerNoPaginationTest(testtools.TestCase): diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 1c84b529..f5f4402f 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -647,63 +647,71 @@ def do_stack_list(hc, args=None): @utils.arg('id', metavar='', help=_('Name or ID of stack to query.')) def do_output_list(hc, args): - '''Show available outputs.''' + """Show available outputs.""" try: - stack = hc.stacks.get(stack_id=args.id) + outputs = hc.stacks.output_list(args.id) except exc.HTTPNotFound: raise exc.CommandError(_('Stack not found: %s') % args.id) else: - outputs = stack.to_dict()['outputs'] fields = ['output_key', 'description'] formatters = { 'output_key': lambda x: x['output_key'], 'description': lambda x: x['description'], } - utils.print_list(outputs, fields, formatters=formatters) + utils.print_list(outputs['outputs'], fields, formatters=formatters) @utils.arg('id', metavar='', help=_('Name or ID of stack to query.')) @utils.arg('output', metavar='', nargs='?', default=None, help=_('Name of an output to display.')) -@utils.arg('-a', '--all', default=False, action='store_true', - help=_('Display all stack outputs.')) @utils.arg('-F', '--format', metavar='', - help=_('The output value format, one of: json, raw'), + help=_('The output value format, one of: json, raw.'), default='json') +@utils.arg('-a', '--all', default=False, action='store_true', + help=_('Display all stack outputs.')) +@utils.arg('-v', '--only-value', default=False, action="store_true", + help=_('Returns only output value in specified format.')) def do_output_show(hc, args): - '''Show a specific stack output.''' - if (not args.all and args.output is None or - args.all and args.output is not None): - raise exc.CommandError( - _('Error: either %(output)s or %(all)s argument is needed.') - % {'output': '', 'all': '--all'}) - try: - stack = hc.stacks.get(stack_id=args.id) - except exc.HTTPNotFound: - raise exc.CommandError(_('Stack not found: %s') % args.id) - else: - if args.all: - print(utils.json_formatter(stack.to_dict().get('outputs', []))) + """Show a specific stack output.""" + def show_output_by_key(output_key): + try: + output = hc.stacks.output_show(args.id, output_key) + except exc.HTTPNotFound: + raise exc.CommandError(_('Stack %(id)s or ' + 'output %(key)s not found.') % { + 'id': args.id, + 'key': args.output}) else: - for output in stack.to_dict().get('outputs', []): - if output['output_key'] == args.output: - if 'output_error' in output: - msg = _("Error: %s") % output['output_error'] - raise exc.CommandError(msg) - else: - value = output['output_value'] - break + if 'output_error' in output['output']: + msg = _("Output error: %s") % output['output']['output_error'] + raise exc.CommandError(msg) + if args.only_value: + if (args.format == 'json' + or isinstance(output['output']['output_value'], dict) + or isinstance(output['output']['output_value'], list)): + print( + utils.json_formatter(output['output']['output_value'])) + else: + print(output['output']['output_value']) else: - return + formatters = { + 'output_value': (lambda x: utils.json_formatter(x) + if args.format == 'json' + else x) + } + utils.print_dict(output['output'], formatters=formatters) - if (args.format == 'json' - or isinstance(value, dict) - or isinstance(value, list)): - print(utils.json_formatter(value)) - else: - print(value) + if args.all: + try: + outputs = hc.stacks.output_list(args.id) + except exc.HTTPNotFound: + raise exc.CommandError(_('Stack not found: %s') % args.id) + for output in outputs['outputs']: + show_output_by_key(output['output_key']) + else: + show_output_by_key(args.output) @utils.arg('-f', '--filters', metavar='', diff --git a/heatclient/v1/stacks.py b/heatclient/v1/stacks.py index 28aa83ef..b7102717 100644 --- a/heatclient/v1/stacks.py +++ b/heatclient/v1/stacks.py @@ -54,6 +54,12 @@ def restore(self, snapshot_id): def snapshot_list(self): return self.manager.snapshot_list(self.identifier) + def output_list(self): + return self.manager.output_list(self.identifier) + + def output_show(self, output_key): + return self.manager.output_show(self.identifier, output_key) + def get(self): # set_loaded() first ... so if we have to bail, we know we tried. self._loaded = True @@ -211,6 +217,21 @@ def snapshot_list(self, stack_id): body = utils.get_response_body(resp) return body + def output_list(self, stack_id): + stack = self.get(stack_id) + resp = self.client.get('/stacks/%s/outputs' % stack.identifier) + body = utils.get_response_body(resp) + return body + + def output_show(self, stack_id, output_key): + stack = self.get(stack_id) + resp = self.client.get('/stacks/%(id)s/outputs/%(key)s' % { + 'id': stack.identifier, + 'key': output_key + }) + body = utils.get_response_body(resp) + return body + def get(self, stack_id): """Get the metadata for a specific stack. From 47cbec7ff5dbad0c467e29648c86d1443e0b3c4e Mon Sep 17 00:00:00 2001 From: ricolin Date: Sun, 15 Nov 2015 21:34:22 +0800 Subject: [PATCH 059/553] Enable pep8 H405 tests Enabled H405 multi line docstring summary not separated with an empty line in tox testing. Change-Id: I89d063133154ae343315872b677fa06cc901cf3b --- heatclient/common/environment_format.py | 4 ++-- heatclient/tests/functional/base.py | 2 +- heatclient/tests/functional/test_readonly_heat.py | 1 + heatclient/v1/events.py | 1 + heatclient/v1/resource_types.py | 1 + heatclient/v1/resources.py | 6 +++--- heatclient/v1/services.py | 1 + heatclient/v1/shell.py | 9 ++++----- heatclient/v1/software_configs.py | 1 + heatclient/v1/software_deployments.py | 5 +++-- heatclient/v1/stacks.py | 3 +-- heatclient/v1/template_versions.py | 1 + tox.ini | 2 +- 13 files changed, 21 insertions(+), 16 deletions(-) diff --git a/heatclient/common/environment_format.py b/heatclient/common/environment_format.py index 5c29aad0..c5f3e942 100644 --- a/heatclient/common/environment_format.py +++ b/heatclient/common/environment_format.py @@ -51,8 +51,8 @@ def parse(env_str): def default_for_missing(env): - '''Checks a parsed environment for missing sections. - ''' + """Checks a parsed environment for missing sections.""" + for param in SECTIONS: if param not in env: env[param] = {} diff --git a/heatclient/tests/functional/base.py b/heatclient/tests/functional/base.py index 2f833415..44718cad 100644 --- a/heatclient/tests/functional/base.py +++ b/heatclient/tests/functional/base.py @@ -17,8 +17,8 @@ class ClientTestBase(base.ClientTestBase): """This is a first pass at a simple read only python-heatclient test. - This only exercises client commands that are read only. + This only exercises client commands that are read only. This should test commands: * as a regular user * as a admin user diff --git a/heatclient/tests/functional/test_readonly_heat.py b/heatclient/tests/functional/test_readonly_heat.py index 84fe049f..9ccb477f 100644 --- a/heatclient/tests/functional/test_readonly_heat.py +++ b/heatclient/tests/functional/test_readonly_heat.py @@ -21,6 +21,7 @@ class SimpleReadOnlyHeatClientTest(base.ClientTestBase): """Basic, read-only tests for Heat CLI client. + Basic smoke test for the heat CLI commands which do not require creating or modifying stacks. """ diff --git a/heatclient/v1/events.py b/heatclient/v1/events.py index d468a123..d3489277 100644 --- a/heatclient/v1/events.py +++ b/heatclient/v1/events.py @@ -43,6 +43,7 @@ class EventManager(stacks.StackChildManager): def list(self, stack_id, resource_name=None, **kwargs): """Get a list of events. + :param stack_id: ID of stack the events belong to :param resource_name: Optional name of resources to filter events by :rtype: list of :class:`Event` diff --git a/heatclient/v1/resource_types.py b/heatclient/v1/resource_types.py index 952cc3c5..75ea22af 100644 --- a/heatclient/v1/resource_types.py +++ b/heatclient/v1/resource_types.py @@ -35,6 +35,7 @@ class ResourceTypeManager(base.BaseManager): def list(self, **kwargs): """Get a list of resource types. + :rtype: list of :class:`ResourceType` """ diff --git a/heatclient/v1/resources.py b/heatclient/v1/resources.py index 8949d270..f063c841 100644 --- a/heatclient/v1/resources.py +++ b/heatclient/v1/resources.py @@ -51,6 +51,7 @@ class ResourceManager(stacks.StackChildManager): def list(self, stack_id, **kwargs): """Get a list of resources. + :rtype: list of :class:`Resource` """ params = {} @@ -112,9 +113,8 @@ def signal(self, stack_id, resource_name, data=None): return body def generate_template(self, resource_name): - """DEPRECATED! Use `generate_template` of `ResourceTypeManager` - instead. - """ + """Deprecated in favor of generate_template in ResourceTypeManager.""" + url_str = '/resource_types/%s/template' % ( parse.quote(encodeutils.safe_encode(resource_name), '')) resp = self.client.get(url_str) diff --git a/heatclient/v1/services.py b/heatclient/v1/services.py index 9e77c733..f833585c 100644 --- a/heatclient/v1/services.py +++ b/heatclient/v1/services.py @@ -26,6 +26,7 @@ class ServiceManager(base.BaseManager): def list(self): """Get a list of services. + :rtype: list of :class:`Service` """ url = '/services' diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 1c84b529..e6bc3be8 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -40,8 +40,8 @@ def _authenticated_fetcher(hc): - """A wrapper around the heat client object to fetch a template. - """ + """A wrapper around the heat client object to fetch a template.""" + def _do(*args, **kwargs): if isinstance(hc.http_client, http.SessionClient): method, url = args @@ -1565,9 +1565,8 @@ def _do_stack_show(hc, fields): def _poll_for_events(hc, stack_name, action, poll_period): - """When an action is performed on a stack, continuously poll for its - events and display to user as logs. - """ + """Continuously poll events and logs for performed action on stack.""" + fields = {'stack_id': stack_name} _do_stack_show(hc, fields) marker = None diff --git a/heatclient/v1/software_configs.py b/heatclient/v1/software_configs.py index bc948283..ac7423b5 100644 --- a/heatclient/v1/software_configs.py +++ b/heatclient/v1/software_configs.py @@ -33,6 +33,7 @@ class SoftwareConfigManager(base.BaseManager): def list(self, **kwargs): """Get a list of software configs. + :rtype: list of :class:`SoftwareConfig` """ qparams = {} diff --git a/heatclient/v1/software_deployments.py b/heatclient/v1/software_deployments.py index 494bbce8..21529193 100644 --- a/heatclient/v1/software_deployments.py +++ b/heatclient/v1/software_deployments.py @@ -32,14 +32,15 @@ class SoftwareDeploymentManager(base.BaseManager): def list(self, **kwargs): """Get a list of software deployments. + :rtype: list of :class:`SoftwareDeployment` """ url = '/software_deployments?%s' % parse.urlencode(kwargs) return self._list(url, "software_deployments") def metadata(self, server_id): - """Get a grouped collection of software deployment metadata for a - given server. + """Get a collection of software deployment metadata for given server. + :rtype: list of :class:`SoftwareDeployment` """ url = '/software_deployments/metadata/%s' % parse.quote( diff --git a/heatclient/v1/stacks.py b/heatclient/v1/stacks.py index 28aa83ef..542feaa2 100644 --- a/heatclient/v1/stacks.py +++ b/heatclient/v1/stacks.py @@ -221,8 +221,7 @@ def get(self, stack_id): return Stack(self, body.get('stack')) def template(self, stack_id): - """Get the template content for a specific stack as a parsed JSON - object. + """Get template content for a specific stack as a parsed JSON object. :param stack_id: Stack ID to get the template for """ diff --git a/heatclient/v1/template_versions.py b/heatclient/v1/template_versions.py index 3ba4c630..0bfc0a1d 100644 --- a/heatclient/v1/template_versions.py +++ b/heatclient/v1/template_versions.py @@ -30,6 +30,7 @@ class TemplateVersionManager(base.BaseManager): def list(self): """Get a list of template versions. + :rtype: list of :class:`TemplateVersion` """ return self._list('/template_versions', 'template_versions') diff --git a/tox.ini b/tox.ini index 312a41aa..230ebfe7 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ commands= downloadcache = ~/cache/pip [flake8] -ignore = E123,E126,E128,E241,E265,E713,H202,H405,H238 +ignore = E123,E126,E128,E241,E265,E713,H202,H238 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build max-complexity=20 From 623c70431280aba1841b2b0d2c438e685fc5d5ce Mon Sep 17 00:00:00 2001 From: ricolin Date: Sun, 15 Nov 2015 22:04:03 +0800 Subject: [PATCH 060/553] Enable pep8 H238 test Enable H238 old style class declaration, use new style (inherit from `object`) Change-Id: I4d11e398a008bcb1b22ecc0b6b2df41c1abae6d5 --- heatclient/tests/unit/fakes.py | 8 ++++---- heatclient/tests/unit/osc/fakes.py | 2 +- tox.ini | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/heatclient/tests/unit/fakes.py b/heatclient/tests/unit/fakes.py index 74ecf304..fca1ab99 100644 --- a/heatclient/tests/unit/fakes.py +++ b/heatclient/tests/unit/fakes.py @@ -211,23 +211,23 @@ def fake_headers(): 'User-Agent': 'python-heatclient'} -class FakeServiceCatalog(): +class FakeServiceCatalog(object): def url_for(self, endpoint_type, service_type): return 'http://192.168.1.5:8004/v1/f14b41234' -class FakeKeystone(): +class FakeKeystone(object): service_catalog = FakeServiceCatalog() def __init__(self, auth_token): self.auth_token = auth_token -class FakeRaw(): +class FakeRaw(object): version = 110 -class FakeHTTPResponse(): +class FakeHTTPResponse(object): version = 1.1 diff --git a/heatclient/tests/unit/osc/fakes.py b/heatclient/tests/unit/osc/fakes.py index 73865906..1fcd6e59 100644 --- a/heatclient/tests/unit/osc/fakes.py +++ b/heatclient/tests/unit/osc/fakes.py @@ -160,7 +160,7 @@ } -class FakeStdout: +class FakeStdout(object): def __init__(self): self.content = [] diff --git a/tox.ini b/tox.ini index 230ebfe7..b9ee36f5 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ commands= downloadcache = ~/cache/pip [flake8] -ignore = E123,E126,E128,E241,E265,E713,H202,H238 +ignore = E123,E126,E128,E241,E265,E713,H202 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build max-complexity=20 From 728b40bc0d76a34ec0ea91eb95346e5946db7a21 Mon Sep 17 00:00:00 2001 From: ricolin Date: Sun, 15 Nov 2015 22:58:24 +0800 Subject: [PATCH 061/553] Enable pep8 H202 test Change-Id: Id9331cfd3ac61bd061b29a717eaeb28441d51e0c --- heatclient/tests/unit/test_shell.py | 35 +++++++++++++++++------------ tox.ini | 2 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index bb6f060a..a166002f 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -110,11 +110,11 @@ def assertNotRegexpMatches(self, text, expected_regexp, msg=None): else: raise self.failureException(msg) - def shell_error(self, argstr, error_match): + def shell_error(self, argstr, error_match, exception): orig = sys.stderr sys.stderr = six.StringIO() _shell = heatclient.shell.HeatShell() - e = self.assertRaises(Exception, _shell.main, argstr.split()) + e = self.assertRaises(exception, _shell.main, argstr.split()) self.assertRegexpMatches(e.__str__(), error_match) err = sys.stderr.getvalue() sys.stderr.close() @@ -181,7 +181,7 @@ def test_missing_auth(self): } fake_env[self.remove] = None self.set_fake_env(fake_env) - self.shell_error('stack-list', self.err) + self.shell_error('stack-list', self.err, exception=exc.CommandError) class EnvVarTestToken(TestCase): @@ -204,7 +204,7 @@ def test_missing_auth(self): } fake_env[self.remove] = None self.set_fake_env(fake_env) - self.shell_error('stack-list', self.err) + self.shell_error('stack-list', self.err, exception=exc.CommandError) class ShellParamValidationTest(TestCase): @@ -249,7 +249,7 @@ def test_bad_parameters(self): template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') cmd = '%s --template-file=%s ' % (self.command, template_file) - self.shell_error(cmd, self.err) + self.shell_error(cmd, self.err, exception=exc.CommandError) class ShellValidationTest(TestCase): @@ -269,7 +269,7 @@ def test_failed_auth(self): self.m.ReplayAll() self.set_fake_env(FAKE_ENV_KEYSTONE_V2) - self.shell_error('stack-list', failed_msg) + self.shell_error('stack-list', failed_msg, exception=exc.Unauthorized) def test_stack_create_validation(self): self.register_keystone_auth_fixture() @@ -279,7 +279,8 @@ def test_stack_create_validation(self): '--parameters="InstanceType=m1.large;DBUsername=wp;' 'DBPassword=verybadpassword;KeyName=heat_key;' 'LinuxDistribution=F17"', - 'Need to specify exactly one of') + 'Need to specify exactly one of', + exception=exc.CommandError) def test_stack_create_with_paramfile_validation(self): self.register_keystone_auth_fixture() @@ -290,7 +291,8 @@ def test_stack_create_with_paramfile_validation(self): '--parameters="InstanceType=m1.large;DBUsername=wp;' 'DBPassword=verybadpassword;KeyName=heat_key;' 'LinuxDistribution=F17"', - 'Need to specify exactly one of') + 'Need to specify exactly one of', + exception=exc.CommandError) def test_stack_create_validation_keystone_v3(self): self.register_keystone_auth_fixture() @@ -300,7 +302,9 @@ def test_stack_create_validation_keystone_v3(self): '--parameters="InstanceType=m1.large;DBUsername=wp;' 'DBPassword=verybadpassword;KeyName=heat_key;' 'LinuxDistribution=F17"', - 'Need to specify exactly one of') + 'Need to specify exactly one of', + exception=exc.CommandError + ) class ShellBase(TestCase): @@ -389,7 +393,8 @@ def test_stack_create_parameter_missing_err_msg(self): template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') self.shell_error('stack-create -f %s stack' % template_file, - 'The Parameter \(key_name\) was not provided.') + 'The Parameter \(key_name\) was not provided.', + exception=exc.HTTPBadRequest) def test_event_list(self): eventid1 = uuid.uuid4().hex @@ -1869,7 +1874,8 @@ def test_stack_adopt_without_data(self): self.register_keystone_auth_fixture() failed_msg = 'Need to specify --adopt-file' self.m.ReplayAll() - self.shell_error('stack-adopt teststack ', failed_msg) + self.shell_error('stack-adopt teststack ', failed_msg, + exception=exc.CommandError) def test_stack_update_enable_rollback(self): self.register_keystone_auth_fixture() @@ -1983,7 +1989,8 @@ def test_stack_update_fault_rollback_value(self): self.shell_error('stack-update teststack2/2 ' '--rollback Foo ' '--template-file=%s' % template_file, - "Unrecognized value 'Foo', acceptable values are:" + "Unrecognized value 'Foo', acceptable values are:", + exception=exc.CommandError ) def test_stack_update_rollback_default(self): @@ -4558,13 +4565,13 @@ def test_bad_template_file(self): bad_json_file.write(b"{foo:}") bad_json_file.flush() self.shell_error("stack-create ts -f %s" % bad_json_file.name, - failed_msg) + failed_msg, exception=exc.CommandError) with tempfile.NamedTemporaryFile() as bad_json_file: bad_json_file.write(b'{"foo": None}') bad_json_file.flush() self.shell_error("stack-create ts -f %s" % bad_json_file.name, - failed_msg) + failed_msg, exception=exc.CommandError) def test_commandline_args_passed_to_requests(self): """Check that we have sent the proper arguments to requests.""" diff --git a/tox.ini b/tox.ini index b9ee36f5..3126aece 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ commands= downloadcache = ~/cache/pip [flake8] -ignore = E123,E126,E128,E241,E265,E713,H202 +ignore = E123,E126,E128,E241,E265,E713 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build max-complexity=20 From a83a392585acbf51140c4e4082d0e85f2b0dc4f8 Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Sun, 15 Nov 2015 16:00:29 +0000 Subject: [PATCH 062/553] OpenStack typo change capitalization of OpenStack in file doc/source/man heat.rst TrivialFix Change-Id: I9f40fa7b2ed953ae6f2bb305f3028ec63b7262df --- doc/source/man/heat.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/man/heat.rst b/doc/source/man/heat.rst index 1b291117..9355a80e 100644 --- a/doc/source/man/heat.rst +++ b/doc/source/man/heat.rst @@ -90,7 +90,7 @@ List heat-engines running status :: heat service-list Note: stack-adopt and stack-abandon commands are not available by default. -Please ask your Openstack operator to enable this feature. +Please ask your OpenStack operator to enable this feature. BUGS ==== From cdcd7165531a672dd0fdc1c9efd9f714bb0ec9dc Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Wed, 18 Nov 2015 21:30:51 +0000 Subject: [PATCH 063/553] Fix error when listing stacks Previously the stack list command would error out if any stacks were present. This was due to the action code attempting to index on a stack object, rather than grabbing its attributes. This change switches the action code to use the proper utility for a resource instead of a dict. Change-Id: Ia6b73d5c1ba7f7e3d7bc5493167da81062102b42 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/stack.py | 2 +- heatclient/tests/unit/osc/v1/test_stack.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index d9381b69..6e471f08 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -226,5 +226,5 @@ def _list(client, args=None): return ( columns, - (utils.get_dict_properties(s, columns) for s in data) + (utils.get_item_properties(s, columns) for s in data) ) diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index 95514d5c..2f33ed1a 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -116,7 +116,8 @@ class TestStackList(TestStack): def setUp(self): super(TestStackList, self).setUp() self.cmd = stack.ListStack(self.app, None) - self.stack_client.list = mock.MagicMock(return_value=[self.data]) + self.stack_client.list = mock.MagicMock( + return_value=[stacks.Stack(None, self.data)]) utils.get_dict_properties = mock.MagicMock(return_value='') def test_stack_list_defaults(self): From b2619a9e65ddb543f9cbf8b87bedaafdbc4d899c Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Sun, 22 Nov 2015 15:45:26 +0000 Subject: [PATCH 064/553] Use six.iteritems() instead of iter(XXX.items()) This patch changes the method iter(XXX.items()) to six.iteritems() Change-Id: I178cfae85e0c128633f30236da565be8b2c7acc1 --- heatclient/common/template_utils.py | 4 ++-- heatclient/common/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index e71a3b19..6d58b990 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -112,7 +112,7 @@ def get_file_contents(from_data, files, base_url=None, get_file_contents(value, files, base_url, ignore_if, recurse_if) if isinstance(from_data, dict): - for key, value in iter(from_data.items()): + for key, value in six.iteritems(from_data): if ignore_if and ignore_if(key, value): continue @@ -218,7 +218,7 @@ def ignore_if(key, value): get_file_contents(rr, files, base_url, ignore_if) - for res_name, res_dict in iter(rr.get('resources', {}).items()): + for res_name, res_dict in six.iteritems(rr.get('resources', {})): res_base_url = res_dict.get('base_url', base_url) get_file_contents( res_dict, files, res_base_url, ignore_if) diff --git a/heatclient/common/utils.py b/heatclient/common/utils.py index 524585f0..53a84c03 100644 --- a/heatclient/common/utils.py +++ b/heatclient/common/utils.py @@ -231,7 +231,7 @@ def format_parameter_file(param_files, template_file=None, template_file, template_url)) param_file = {} - for key, value in iter(params.items()): + for key, value in six.iteritems(params): param_file[key] = resolve_param_get_file(value, template_base_url) return param_file From 5a658551bddb909d4ff57e53efab21b6d53cc721 Mon Sep 17 00:00:00 2001 From: ricolin Date: Sun, 15 Nov 2015 23:02:12 +0800 Subject: [PATCH 065/553] Enable pep8 E713 test Enable E713 test for membership should be 'not in' Change-Id: I22c8f849713f613e0ef5f6011c075de5f31b7621 --- heatclient/common/http.py | 8 ++++---- tox.ini | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/heatclient/common/http.py b/heatclient/common/http.py index 3a268966..fad66276 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -158,7 +158,7 @@ def _http_request(self, url, method, **kwargs): kwargs['headers'].setdefault('X-Auth-Url', self.auth_url) if self.region_name: kwargs['headers'].setdefault('X-Region-Name', self.region_name) - if self.include_pass and not 'X-Auth-Key' in kwargs['headers']: + if self.include_pass and 'X-Auth-Key' not in kwargs['headers']: kwargs['headers'].update(self.credentials_headers()) if osprofiler_web: kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) @@ -206,9 +206,9 @@ def _http_request(self, url, method, **kwargs): self.log_http_response(resp) - if not 'X-Auth-Key' in kwargs['headers'] and \ - (resp.status_code == 401 or - (resp.status_code == 500 and "(HTTP 401)" in resp.content)): + if not ('X-Auth-Key' in kwargs['headers']) and ( + resp.status_code == 401 or + (resp.status_code == 500 and "(HTTP 401)" in resp.content)): raise exc.HTTPUnauthorized(_("Authentication failed. Please try" " again with option %(option)s or " "export %(var)s\n%(content)s") % diff --git a/tox.ini b/tox.ini index 3126aece..ee2d780e 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ commands= downloadcache = ~/cache/pip [flake8] -ignore = E123,E126,E128,E241,E265,E713 +ignore = E123,E126,E128,E241,E265 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build max-complexity=20 From 9e7a04110a7c4d7ba85589f986222aa4976f7b39 Mon Sep 17 00:00:00 2001 From: ricolin Date: Tue, 17 Nov 2015 09:59:16 +0800 Subject: [PATCH 066/553] Enable pep8 E241 test Enable E241 multiple spaces after ',' Change-Id: Iced079fe6315df0ac91995e77861b16340cb8727 --- heatclient/tests/unit/test_shell.py | 2 +- heatclient/tests/unit/v1/test_hooks.py | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index a166002f..1e37ffad 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -1225,7 +1225,7 @@ def test_create_success_with_poll(self): '2', 'teststack2', 'IN_PROGRESS', - '14:14:30', '2013-12-05', + '14:14:30', '2013-12-05', 'CREATE_IN_PROGRESS', 'state changed', '14:14:31', 'testresource', diff --git a/heatclient/tests/unit/v1/test_hooks.py b/heatclient/tests/unit/v1/test_hooks.py index 2b0724c6..7fc0de2e 100644 --- a/heatclient/tests/unit/v1/test_hooks.py +++ b/heatclient/tests/unit/v1/test_hooks.py @@ -129,7 +129,7 @@ def test_create_hooks_in_env_and_args(self): } }, } - self.assertEqual(expected_hooks, actual_hooks) + self.assertEqual(expected_hooks, actual_hooks) def test_update_hooks_in_args(self): type(self.args).pre_update = mock.PropertyMock( diff --git a/tox.ini b/tox.ini index ee2d780e..2913f911 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ commands= downloadcache = ~/cache/pip [flake8] -ignore = E123,E126,E128,E241,E265 +ignore = E123,E126,E128,E265 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build max-complexity=20 From 6f1542c2ca9a477d836039ec82ff6d2890eb71cd Mon Sep 17 00:00:00 2001 From: ricolin Date: Sun, 15 Nov 2015 23:07:59 +0800 Subject: [PATCH 067/553] Enable pep8 E265 test Enable E265 block comment should start with '# ' Change-Id: I78813e73f8dd800bc58a1d69eedc2c066ccb8c28 --- doc/source/conf.py | 80 +++++++++++++++++++++++----------------------- heatclient/exc.py | 2 +- tox.ini | 2 +- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index e611dbe6..7b66a52d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -28,7 +28,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) +# sys.path.append(os.path.abspath('.')) exec(open(os.path.join("ext", "gen_ref.py")).read()) # -- General configuration ---------------------------------------------------- @@ -48,7 +48,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' @@ -68,13 +68,13 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -82,24 +82,24 @@ # The reST default role (used for this markup: `text`) # to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] primary_domain = 'py' nitpicky = False @@ -120,23 +120,23 @@ } # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -145,46 +145,46 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1" html_last_updated_fmt = os.popen(git_cmd).read() # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'python-heatclientdoc' @@ -194,13 +194,13 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', + # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', + # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. - #'preamble': '', + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples @@ -212,23 +212,23 @@ # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output ------------------------------------------- @@ -242,7 +242,7 @@ ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ----------------------------------------------- @@ -257,10 +257,10 @@ ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/heatclient/exc.py b/heatclient/exc.py index 4f8a0623..b7649427 100644 --- a/heatclient/exc.py +++ b/heatclient/exc.py @@ -167,7 +167,7 @@ class HTTPServiceUnavailable(ServiceUnavailable): pass -#NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception +# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception # classes _code_map = {} for obj_name in dir(sys.modules[__name__]): diff --git a/tox.ini b/tox.ini index 2913f911..3c476781 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ commands= downloadcache = ~/cache/pip [flake8] -ignore = E123,E126,E128,E265 +ignore = E123,E126,E128 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build max-complexity=20 From ec2530b975200c98ca93131725e62705f57badfd Mon Sep 17 00:00:00 2001 From: ricolin Date: Sun, 15 Nov 2015 23:16:35 +0800 Subject: [PATCH 068/553] Enable pep8 E128 test Enable E128 continuation line under-indented for visual indent Change-Id: If662f729e7aacdd439b38c5e5630d74afefebcc6 --- heatclient/shell.py | 107 ++++++++++++++++----------------- heatclient/tests/unit/fakes.py | 10 +-- heatclient/v1/shell.py | 100 +++++++++++++++--------------- tox.ini | 2 +- 4 files changed, 110 insertions(+), 109 deletions(-) diff --git a/heatclient/shell.py b/heatclient/shell.py index b3a3db7d..16353611 100644 --- a/heatclient/shell.py +++ b/heatclient/shell.py @@ -49,19 +49,19 @@ def _append_global_identity_args(self, parser): # they should be provided by python-keystoneclient. We will need to # refactor this code once this functionality is avaible in # python-keystoneclient. - parser.add_argument('-k', '--insecure', - default=False, - action='store_true', - help=_('Explicitly allow heatclient to perform ' - '\"insecure SSL\" (https) requests. The server\'s ' - 'certificate will not be verified against any ' - 'certificate authorities. This option should ' - 'be used with caution.')) - - parser.add_argument('--os-cert', - help=_('Path of certificate file to use in SSL ' - 'connection. This file can optionally be ' - 'prepended with the private key.')) + parser.add_argument( + '-k', '--insecure', default=False, action='store_true', + help=_('Explicitly allow heatclient to perform ' + '\"insecure SSL\" (https) requests. ' + 'The server\'s certificate will not be verified ' + 'against any certificate authorities. ' + 'This option should be used with caution.')) + + parser.add_argument( + '--os-cert', + help=_('Path of certificate file to use in SSL connection. ' + 'This file can optionally be prepended with ' + 'the private key.')) # for backward compatibility only parser.add_argument('--cert-file', @@ -71,8 +71,9 @@ def _append_global_identity_args(self, parser): parser.add_argument('--os-key', help=_('Path of client key to use in SSL ' - 'connection. This option is not necessary ' - 'if your key is prepended to your cert file.')) + 'connection. This option is not necessary ' + 'if your key is prepended to your cert ' + 'file.')) parser.add_argument('--key-file', dest='os_key', @@ -84,9 +85,9 @@ def _append_global_identity_args(self, parser): dest='os_cacert', default=utils.env('OS_CACERT'), help=_('Path of CA TLS certificate(s) used to ' - 'verify the remote server\'s certificate. ' - 'Without this option glance looks for the ' - 'default system CA certificates.')) + 'verify the remote server\'s certificate. ' + 'Without this option glance looks for the ' + 'default system CA certificates.')) parser.add_argument('--ca-file', dest='os_cacert', @@ -290,7 +291,7 @@ def get_base_parser(self): default=utils.env('OS_NO_CLIENT_AUTH'), action='store_true', help=(_("Do not contact keystone for a token. " - "Defaults to %(value)s.") % + "Defaults to %(value)s.") % {'value': 'env[OS_NO_CLIENT_AUTH]'})) parser.add_argument('--heat-url', @@ -334,17 +335,17 @@ def get_base_parser(self): self._append_global_identity_args(parser) if osprofiler_profiler: - parser.add_argument('--profile', - metavar='HMAC_KEY', - help=_('HMAC key to use for encrypting ' - 'context data for performance profiling of ' - 'operation. This key should be the value of ' - 'HMAC key configured in osprofiler middleware ' - 'in heat, it is specified in the paste ' - 'configuration (/etc/heat/api-paste.ini). ' - 'Without the key, profiling will not be ' - 'triggered even if osprofiler is enabled ' - 'on server side.')) + parser.add_argument( + '--profile', + metavar='HMAC_KEY', + help=_('HMAC key to use for encrypting context data ' + 'for performance profiling of operation. ' + 'This key should be the value of HMAC key ' + 'configured in osprofiler middleware in heat, ' + 'it is specified in the paste configuration ' + '(/etc/heat/api-paste.ini). Without the key, ' + 'profiling will not be triggered ' + 'even if osprofiler is enabled on server side.')) return parser def get_subcommand_parser(self, version): @@ -546,23 +547,23 @@ def main(self, argv): return 0 if not args.os_username and not args.os_auth_token: - raise exc.CommandError(_("You must provide a username via" - " either --os-username or env[OS_USERNAME]" - " or a token via --os-auth-token or" - " env[OS_AUTH_TOKEN]")) + raise exc.CommandError(_("You must provide a username via either " + "--os-username or env[OS_USERNAME] " + "or a token via --os-auth-token or " + "env[OS_AUTH_TOKEN]")) if not args.os_password and not args.os_auth_token: - raise exc.CommandError(_("You must provide a password via" - " either --os-password or env[OS_PASSWORD]" - " or a token via --os-auth-token or" - " env[OS_AUTH_TOKEN]")) + raise exc.CommandError(_("You must provide a password via either " + "--os-password or env[OS_PASSWORD] " + "or a token via --os-auth-token or " + "env[OS_AUTH_TOKEN]")) if args.os_no_client_auth: if not args.heat_url: - raise exc.CommandError(_("If you specify --os-no-client-auth" - " you must also specify a Heat API URL" - " via either --heat-url or" - " env[HEAT_URL]")) + raise exc.CommandError(_("If you specify --os-no-client-auth " + "you must also specify a Heat API " + "URL via either --heat-url or " + "env[HEAT_URL]")) else: # Tenant/project name or ID is needed to make keystoneclient # retrieve a service catalog, it's not required if @@ -570,20 +571,18 @@ def main(self, argv): if not (args.os_tenant_id or args.os_tenant_name or args.os_project_id or args.os_project_name): - raise exc.CommandError(_("You must provide a tenant id via" - " either --os-tenant-id or" - " env[OS_TENANT_ID] or a tenant name" - " via either --os-tenant-name or" - " env[OS_TENANT_NAME] or a project id" - " via either --os-project-id or" - " env[OS_PROJECT_ID] or a project" - " name via either --os-project-name or" - " env[OS_PROJECT_NAME]")) + raise exc.CommandError( + _("You must provide a tenant id via either " + "--os-tenant-id or env[OS_TENANT_ID] or a tenant name " + "via either --os-tenant-name or env[OS_TENANT_NAME] " + "or a project id via either --os-project-id or " + "env[OS_PROJECT_ID] or a project name via " + "either --os-project-name or env[OS_PROJECT_NAME]")) if not args.os_auth_url: - raise exc.CommandError(_("You must provide an auth url via" - " either --os-auth-url or via" - " env[OS_AUTH_URL]")) + raise exc.CommandError(_("You must provide an auth url via " + "either --os-auth-url or via " + "env[OS_AUTH_URL]")) kwargs = { 'insecure': args.insecure, diff --git a/heatclient/tests/unit/fakes.py b/heatclient/tests/unit/fakes.py index fca1ab99..1ffa4b3e 100644 --- a/heatclient/tests/unit/fakes.py +++ b/heatclient/tests/unit/fakes.py @@ -116,11 +116,11 @@ def mock_script_event_list( {"event_time": "2013-12-05T14:14:32", "id": rsrc_eventid2, "links": [{"href": "http://heat.example.com:8004/foo", - "rel": "self"}, - {"href": "http://heat.example.com:8004/foo2", - "rel": "resource"}, - {"href": "http://heat.example.com:8004/foo3", - "rel": "stack"}], + "rel": "self"}, + {"href": "http://heat.example.com:8004/foo2", + "rel": "resource"}, + {"href": "http://heat.example.com:8004/foo3", + "rel": "stack"}], "logical_resource_id": "myDeployment", "physical_resource_id": "bce15ec4-8919-4a02-8a90-680960fb3731", "resource_name": resource_name if resource_name else "testresource", diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 4c806bd6..4bfa4e9b 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -72,8 +72,8 @@ def _do(*args, **kwargs): help=_('URL to retrieve template object (e.g. from swift).')) @utils.arg('-c', '--create-timeout', metavar='', type=int, - help=_('Stack creation timeout in minutes.' - ' DEPRECATED use %(arg)s instead.') + help=_('Stack creation timeout in minutes. ' + 'DEPRECATED use %(arg)s instead.') % {'arg': '--timeout'}) @utils.arg('-t', '--timeout', metavar='', type=int, @@ -82,13 +82,13 @@ def _do(*args, **kwargs): help=_('Enable rollback on create/update failure.')) @utils.arg('-P', '--parameters', metavar='', help=_('Parameter values used to create the stack. ' - 'This can be specified multiple times, or once with parameters ' - 'separated by a semicolon.'), + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), action='append') @utils.arg('-Pf', '--parameter-file', metavar='', help=_('Parameter values from file used to create the stack. ' - 'This can be specified multiple times. Parameter value ' - 'would be the content of the file'), + 'This can be specified multiple times. Parameter value ' + 'would be the content of the file'), action='append') @utils.arg('--poll', metavar='SECONDS', type=int, nargs='?', const=5, help=_('Poll and report events until stack completes. ' @@ -174,8 +174,8 @@ def hooks_to_env(env, arg_hooks, hook): action='append') @utils.arg('-c', '--create-timeout', metavar='', type=int, - help=_('Stack creation timeout in minutes.' - ' DEPRECATED use %(arg)s instead.') + help=_('Stack creation timeout in minutes. ' + 'DEPRECATED use %(arg)s instead.') % {'arg': '--timeout'}) @utils.arg('-t', '--timeout', metavar='', type=int, @@ -185,9 +185,9 @@ def hooks_to_env(env, arg_hooks, hook): @utils.arg('-r', '--enable-rollback', default=False, action="store_true", help=_('Enable rollback on create/update failure.')) @utils.arg('-P', '--parameters', metavar='', - help=_('Parameter values used to create the stack. ' - 'This can be specified multiple times, or once with parameters ' - 'separated by a semicolon.'), + help=_('Parameter values used to create the stack. This can be ' + 'specified multiple times, or once with parameters ' + 'separated by a semicolon.'), action='append') @utils.arg('name', metavar='', help=_('Name of the stack to adopt.')) @@ -239,21 +239,21 @@ def do_stack_adopt(hc, args): @utils.arg('-o', '--template-object', metavar='', help=_('URL to retrieve template object (e.g. from swift)')) @utils.arg('-t', '--timeout', metavar='', type=int, - help=_('Stack creation timeout in minutes. This is only used during' - 'validation in preview.')) + help=_('Stack creation timeout in minutes. This is only used ' + 'during validation in preview.')) @utils.arg('-r', '--enable-rollback', default=False, action="store_true", - help=_('Enable rollback on failure. This option is not used during' - 'preview and exists only for symmetry with %(cmd)s.') + help=_('Enable rollback on failure. This option is not used during ' + 'preview and exists only for symmetry with %(cmd)s.') % {'cmd': 'stack-create'}) @utils.arg('-P', '--parameters', metavar='', help=_('Parameter values used to preview the stack. ' - 'This can be specified multiple times, or once with parameters ' - 'separated by semicolon.'), + 'This can be specified multiple times, or once with ' + 'parameters separated by semicolon.'), action='append') @utils.arg('-Pf', '--parameter-file', metavar='', help=_('Parameter values from file used to create the stack. ' - 'This can be specified multiple times. Parameter value ' - 'would be the content of the file'), + 'This can be specified multiple times. Parameter value ' + 'would be the content of the file'), action='append') @utils.arg('name', metavar='', help=_('Name of the stack to preview.')) @@ -319,8 +319,8 @@ def do_stack_delete(hc, args): @utils.arg('-O', '--output-file', metavar='', help=_('file to output abandon result. ' - 'If the option is specified, the result will be' - ' output into .')) + 'If the option is specified, the result will be ' + 'output into .')) @utils.arg('id', metavar='', help=_('Name or ID of stack to abandon.')) def do_stack_abandon(hc, args): @@ -418,42 +418,44 @@ def do_stack_show(hc, args): help=_('Stack update timeout in minutes.')) @utils.arg('-r', '--enable-rollback', default=False, action="store_true", help=_('DEPRECATED! Use %(arg)s argument instead. ' - 'Enable rollback on stack update failure. ' - 'NOTE: default behavior is now to use the rollback value ' - 'of existing stack.') + 'Enable rollback on stack update failure. ' + 'NOTE: default behavior is now to use the rollback value ' + 'of existing stack.') % {'arg': '--rollback'}) @utils.arg('--rollback', default=None, metavar='', help=_('Set rollback on update failure. ' - 'Values %(true)s set rollback to enabled. ' - 'Values %(false)s set rollback to disabled. ' - 'Default is to use the value of existing stack to be updated.') + 'Values %(true)s set rollback to enabled. ' + 'Values %(false)s set rollback to disabled. ' + 'Default is to use the value of existing stack to be ' + 'updated.') % {'true': strutils.TRUE_STRINGS, 'false': strutils.FALSE_STRINGS}) @utils.arg('-y', '--dry-run', default=False, action="store_true", help='Do not actually perform the stack update, but show what ' 'would be changed') @utils.arg('-P', '--parameters', metavar='', help=_('Parameter values used to create the stack. ' - 'This can be specified multiple times, or once with parameters ' - 'separated by a semicolon.'), + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), action='append') @utils.arg('-Pf', '--parameter-file', metavar='', help=_('Parameter values from file used to create the stack. ' - 'This can be specified multiple times. Parameter value ' - 'would be the content of the file'), + 'This can be specified multiple times. Parameter value ' + 'would be the content of the file'), action='append') @utils.arg('-x', '--existing', default=False, action="store_true", help=_('Re-use the template, parameters and environment of the ' - 'current stack. If the template argument is omitted then the ' - 'existing template is used. If no %(env_arg)s is specified then ' - 'the existing environment is used. ' - 'Parameters specified in %(arg)s will patch over the existing ' - 'values in the current stack. Parameters omitted will keep ' - 'the existing values.') + 'current stack. If the template argument is omitted then ' + 'the existing template is used. If no %(env_arg)s is ' + 'specified then the existing environment is used. ' + 'Parameters specified in %(arg)s will patch over the ' + 'existing values in the current stack. Parameters omitted ' + 'will keep the existing values.') % {'arg': '--parameters', 'env_arg': '--environment-file'}) @utils.arg('-c', '--clear-parameter', metavar='', help=_('Remove the parameters from the set of parameters of ' - 'current stack for the %(cmd)s. The default value in the ' - 'template will be used. This can be specified multiple times.') + 'current stack for the %(cmd)s. The default value in the ' + 'template will be used. This can be specified multiple ' + 'times.') % {'cmd': 'stack-update'}, action='append') @utils.arg('id', metavar='', @@ -554,8 +556,8 @@ def do_stack_cancel_update(hc, args): help=_('Include hidden stacks in the stack listing.')) @utils.arg('-f', '--filters', metavar='', help=_('Filter parameters to apply on returned stacks. ' - 'This can be specified multiple times, or once with parameters ' - 'separated by a semicolon.'), + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), action='append') @utils.arg('-t', '--tags', metavar='', help=_('Show stacks containing these tags, combine multiple tags ' @@ -716,9 +718,9 @@ def show_output_by_key(output_key): @utils.arg('-f', '--filters', metavar='', help=_('Filter parameters to apply on returned resource types. ' - 'This can be specified multiple times, or once with parameters ' - 'separated by a semicolon. It can be any of name, version and ' - 'support_status'), + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon. It can be any of ' + 'name, version and support_status'), action='append') def do_resource_type_list(hc, args): '''List the available resource types.''' @@ -1010,8 +1012,8 @@ def clear_wildcard_hooks(stack_id, stack_patterns): help=_('Name of the resource to filter events by.')) @utils.arg('-f', '--filters', metavar='', help=_('Filter parameters to apply on returned events. ' - 'This can be specified multiple times, or once with parameters ' - 'separated by a semicolon.'), + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), action='append') @utils.arg('-l', '--limit', metavar='', help=_('Limit the number of events returned.')) @@ -1176,7 +1178,7 @@ def do_event_show(hc, args): @utils.arg('-f', '--definition-file', metavar='', help=_('Path to JSON/YAML containing map defining ' - ', , and .')) + ', , and .')) @utils.arg('-c', '--config-file', metavar='', help=_('Path to configuration script/data.')) @utils.arg('-g', '--group', metavar='', default='Heat::Ungrouped', @@ -1389,8 +1391,8 @@ def do_deployment_delete(hc, args): config_id = getattr(sd, 'config_id') hc.software_configs.delete(config_id=config_id) except Exception: - print(_('Failed to delete the correlative config' - ' %(config_id)s of deployment %(deploy_id)s') % + print(_('Failed to delete the correlative config ' + '%(config_id)s of deployment %(deploy_id)s') % {'config_id': config_id, 'deploy_id': deploy_id}) if failure_count == len(args.id): diff --git a/tox.ini b/tox.ini index 3c476781..9f3b198f 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ commands= downloadcache = ~/cache/pip [flake8] -ignore = E123,E126,E128 +ignore = E123,E126 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build max-complexity=20 From 978eeb39949afa64d9bbbee7ae5799d45e9fd3d8 Mon Sep 17 00:00:00 2001 From: ricolin Date: Mon, 16 Nov 2015 00:42:28 +0800 Subject: [PATCH 069/553] Enable pep8 E126 test Enable E126 continuation line over-indented for hanging indent. Change-Id: I12ca2d8d610574dfcd528b0e613ec0328f4fb45e --- heatclient/common/http.py | 3 +-- heatclient/common/template_utils.py | 3 +-- heatclient/exc.py | 3 +-- heatclient/shell.py | 6 ++---- heatclient/v1/shell.py | 6 ++---- tox.ini | 2 +- 6 files changed, 8 insertions(+), 15 deletions(-) diff --git a/heatclient/common/http.py b/heatclient/common/http.py index fad66276..1fbcdcdc 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -215,8 +215,7 @@ def _http_request(self, url, method, **kwargs): { 'option': '--include-password', 'var': 'HEAT_INCLUDE_PASSWORD=1', - 'content': resp.content - }) + 'content': resp.content}) elif 400 <= resp.status_code < 600: raise exc.from_response(resp) elif resp.status_code in (301, 302, 305): diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index 6d58b990..4fa72d86 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -49,8 +49,7 @@ def get_template_contents(template_file=None, template_url=None, { 'arg1': '--template-file', 'arg2': '--template-url', - 'arg3': '--template-object' - }) + 'arg3': '--template-object'}) if not tpl: raise exc.CommandError(_('Could not fetch template from %s') diff --git a/heatclient/exc.py b/heatclient/exc.py index b7649427..ea7d5758 100644 --- a/heatclient/exc.py +++ b/heatclient/exc.py @@ -80,8 +80,7 @@ def __str__(self): { 'name': self.__class__.__name__, 'code': self.code, - 'details': self.details - }) + 'details': self.details}) class BadRequest(HTTPException): diff --git a/heatclient/shell.py b/heatclient/shell.py index 16353611..019df3ec 100644 --- a/heatclient/shell.py +++ b/heatclient/shell.py @@ -137,8 +137,7 @@ def _append_global_identity_args(self, parser): '%(arg)s. Defaults to %(value)s.') % { 'arg': '--os-tenant-id', - 'value': 'env[OS_PROJECT_ID]' - })) + 'value': 'env[OS_PROJECT_ID]'})) parser.add_argument('--os_project_id', help=argparse.SUPPRESS) @@ -150,8 +149,7 @@ def _append_global_identity_args(self, parser): '%(arg)s. Defaults to %(value)s.') % { 'arg': '--os-tenant-name', - 'value': 'env[OS_PROJECT_NAME]' - })) + 'value': 'env[OS_PROJECT_NAME]'})) parser.add_argument('--os_project_name', help=argparse.SUPPRESS) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 4bfa4e9b..307ef7e4 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -113,8 +113,7 @@ def do_stack_create(hc, args): 'please use %(arg2)s instead'), { 'arg1': '-c/--create-timeout', - 'arg2': '-t/--timeout' - }) + 'arg2': '-t/--timeout'}) if args.pre_create: hooks_to_env(env, args.pre_create, 'pre-create') @@ -208,8 +207,7 @@ def do_stack_adopt(hc, args): 'please use %(arg2)s instead'), { 'arg1': '-c/--create-timeout', - 'arg2': '-t/--timeout' - }) + 'arg2': '-t/--timeout'}) fields = { 'stack_name': args.name, diff --git a/tox.ini b/tox.ini index 9f3b198f..54ed0d1b 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ commands= downloadcache = ~/cache/pip [flake8] -ignore = E123,E126 +ignore = E123 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build max-complexity=20 From 3e8d6720c190a10098298aaa2c7c5ed3cdb15536 Mon Sep 17 00:00:00 2001 From: ricolin Date: Mon, 16 Nov 2015 00:59:42 +0800 Subject: [PATCH 070/553] Enable pep8 E123 test Change-Id: Ib22a14c8060f4c78ade64b65988ed989607b04cb --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 54ed0d1b..e096d044 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,6 @@ commands= downloadcache = ~/cache/pip [flake8] -ignore = E123 show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build max-complexity=20 From 35066b234d4990dc324526d88afd558a41632352 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 1 Dec 2015 06:09:46 +0000 Subject: [PATCH 071/553] Updated from global requirements Change-Id: I3cfff7684224f9b47445b0d47c7f631393c17b19 --- requirements.txt | 6 +++--- test-requirements.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 215aff13..2fb3ded7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,16 +4,16 @@ Babel>=1.3 pbr>=1.6 -cliff>=1.14.0 # Apache-2.0 +cliff>=1.15.0 # Apache-2.0 argparse iso8601>=0.1.9 PrettyTable<0.8,>=0.7 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils!=2.6.0,>=2.4.0 # Apache-2.0 +oslo.utils>=2.8.0 # Apache-2.0 python-keystoneclient!=1.8.0,>=1.6.0 python-swiftclient>=2.2.0 python-openstackclient>=1.5.0 PyYAML>=3.1.0 -requests!=2.8.0,>=2.5.2 +requests>=2.8.1 six>=1.9.0 diff --git a/test-requirements.txt b/test-requirements.txt index 275ad728..7e5ed4f7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,10 +7,10 @@ hacking<0.11,>=0.10.0 coverage>=3.6 discover fixtures>=1.3.1 -requests-mock>=0.6.0 # Apache-2.0 +requests-mock>=0.7.0 # Apache-2.0 mock>=1.2 mox3>=0.7.0 -oslosphinx>=2.5.0 # Apache-2.0 +oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 tempest-lib>=0.10.0 From debd086b2a1cb53db7eda34fb967eb6719027ac4 Mon Sep 17 00:00:00 2001 From: shu-mutou Date: Wed, 2 Dec 2015 12:36:56 +0900 Subject: [PATCH 072/553] Delete python bytecode before every test run Because python creates pyc files during tox runs, certain changes in the tree, like deletes of files, or switching branches, can create spurious errors. Change-Id: I9b0e7afeeb934d24c9847573cd7582709e990c9e Closes-Bug: #1368661 --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3c476781..75f96de4 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,9 @@ usedevelop = True install_command = pip install -U {opts} {packages} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt -commands = python setup.py testr --slowest --testr-args='{posargs}' +commands = find . -type f -name "*.pyc" -delete + python setup.py testr --slowest --testr-args='{posargs}' +whitelist_externals = find [testenv:pypy] deps = setuptools<3.2 From 32397cbd66bc7573e227cb49da1039f137c5f545 Mon Sep 17 00:00:00 2001 From: ricolin Date: Wed, 2 Dec 2015 15:17:05 +0800 Subject: [PATCH 073/553] Remove py26 support As of mitaka, the infra team won't have the resources available to reasonably test py26, also the oslo team is dropping py26 support from their libraries. sine we rely on oslo for a lot of our work, and depend on infra for our CI, we should drop py26 support too. Closes-Bug: 1519510 Change-Id: I680043c48c75a50b3ef8eed4f22e97dca029c477 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3c476781..5dbe0ce5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pypy,py34,py27,py26,pep8 +envlist = pypy,py34,py27,pep8 minversion = 1.6 skipsdist = True From dbf34861a89602abbf371721b5ca1d3676394922 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 6 Dec 2015 10:43:40 +0000 Subject: [PATCH 074/553] Updated from global requirements Change-Id: Ie519fec109f90edf639ff0528cc23532c3fbc076 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 7e5ed4f7..4b85cd03 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ mox3>=0.7.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -tempest-lib>=0.10.0 +tempest-lib>=0.11.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 From 9c610d6ef5d720d94b555632cc780603d6e7ac4c Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Wed, 9 Dec 2015 20:58:36 +0000 Subject: [PATCH 075/553] Add JsonFormat display option JsonFormat is a base class from which commands that expect to dump json output can inherit. This class defaults to the cliff json_format formatter, and will not produce any output if no data is provided. Change-Id: I49abb2172afab802fa1b311bac9e1c66c0373803 --- heatclient/common/format_utils.py | 53 +++++++++++++ heatclient/tests/unit/test_format_utils.py | 91 ++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 heatclient/common/format_utils.py create mode 100644 heatclient/tests/unit/test_format_utils.py diff --git a/heatclient/common/format_utils.py b/heatclient/common/format_utils.py new file mode 100644 index 00000000..2b02fc93 --- /dev/null +++ b/heatclient/common/format_utils.py @@ -0,0 +1,53 @@ +# 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. +# +# Copyright 2015 IBM Corp. + +from cliff import show + + +class RawFormat(show.ShowOne): + + def produce_output(self, parsed_args, column_names, data): + if data is None: + return + + self.formatter.emit_one(column_names, data, + self.app.stdout, parsed_args) + + +class JsonFormat(RawFormat): + + @property + def formatter_default(self): + return 'json' + + +class YamlFormat(RawFormat): + + @property + def formatter_default(self): + return 'yaml' + + +class ShellFormat(RawFormat): + + @property + def formatter_default(self): + return 'shell' + + +class ValueFormat(RawFormat): + + @property + def formatter_default(self): + return 'value' diff --git a/heatclient/tests/unit/test_format_utils.py b/heatclient/tests/unit/test_format_utils.py new file mode 100644 index 00000000..8e948ce2 --- /dev/null +++ b/heatclient/tests/unit/test_format_utils.py @@ -0,0 +1,91 @@ +# 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. +# +# Copyright 2015 IBM Corp. + +import json +import yaml + +from heatclient.common import format_utils +from heatclient.tests.unit.osc import utils + + +columns = ['col1', 'col2', 'col3'] +data = ['abcde', ['fg', 'hi', 'jk'], {'lmnop': 'qrstu'}] + + +class ShowJson(format_utils.JsonFormat): + def take_action(self, parsed_args): + return columns, data + + +class ShowYaml(format_utils.YamlFormat): + def take_action(self, parsed_args): + return columns, data + + +class ShowShell(format_utils.ShellFormat): + def take_action(self, parsed_args): + return columns, data + + +class ShowValue(format_utils.ValueFormat): + def take_action(self, parsed_args): + return columns, data + + +class TestFormats(utils.TestCommand): + + def test_json_format(self): + self.cmd = ShowJson(self.app, None) + parsed_args = self.check_parser(self.cmd, [], []) + expected = json.dumps(dict(zip(columns, data)), indent=2) + + self.cmd.run(parsed_args) + + self.assertEqual(expected, self.app.stdout.make_string()) + + def test_yaml_format(self): + self.cmd = ShowYaml(self.app, None) + parsed_args = self.check_parser(self.cmd, [], []) + expected = yaml.safe_dump(dict(zip(columns, data)), + default_flow_style=False) + + self.cmd.run(parsed_args) + + self.assertEqual(expected, self.app.stdout.make_string()) + + def test_shell_format(self): + self.cmd = ShowShell(self.app, None) + parsed_args = self.check_parser(self.cmd, [], []) + expected = '''\ +col1="abcde" +col2="['fg', 'hi', 'jk']" +col3="{'lmnop': 'qrstu'}" +''' + + self.cmd.run(parsed_args) + + self.assertEqual(expected, self.app.stdout.make_string()) + + def test_value_format(self): + self.cmd = ShowValue(self.app, None) + parsed_args = self.check_parser(self.cmd, [], []) + expected = '''\ +abcde +['fg', 'hi', 'jk'] +{'lmnop': 'qrstu'} +''' + + self.cmd.run(parsed_args) + + self.assertEqual(expected, self.app.stdout.make_string()) From 4c1fcb78caef5b87df04ec8da6fa8fdf0afa6810 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 10 Dec 2015 16:45:07 -0500 Subject: [PATCH 076/553] remove py26 support tag in setup.cfg commit 32397cbd66bc7573e227cb49da1039f137c5f545 removed py26 from tox.ini, so it's no longer test, we shouldn't claim support for it in setup.cfg. Change-Id: I7fbf6b9850914523de0ffb018626f310720c9845 --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 39aa368e..be9a2a41 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,6 @@ classifier = Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 - Programming Language :: Python :: 2.6 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 From aff3a997f164f319fc98bb66faed232a0d9c598a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 11 Dec 2015 15:25:17 +0000 Subject: [PATCH 077/553] Updated from global requirements Change-Id: Icc772ca70d6684b28c28fe0b32b2a2483e1e9e3a --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2fb3ded7..45fa9461 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ iso8601>=0.1.9 PrettyTable<0.8,>=0.7 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=2.8.0 # Apache-2.0 +oslo.utils!=3.1.0,>=2.8.0 # Apache-2.0 python-keystoneclient!=1.8.0,>=1.6.0 python-swiftclient>=2.2.0 python-openstackclient>=1.5.0 From 4bfc6ea5deb97ae394fd50d5a9c0cf0143740ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Nov=C3=BD?= Date: Fri, 11 Dec 2015 23:11:56 +0100 Subject: [PATCH 078/553] Deprecated tox -downloadcache option removed Caching is enabled by default from pip version 6.0 More info: https://testrun.org/tox/latest/config.html#confval-downloadcache=path https://pip.pypa.io/en/stable/reference/pip_install/#caching Change-Id: I89546c2c77b1a15659918be65e6ef73157f98b23 --- tox.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/tox.ini b/tox.ini index aacf1807..1fb767d9 100644 --- a/tox.ini +++ b/tox.ini @@ -40,9 +40,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' commands= python setup.py build_sphinx -[tox:jenkins] -downloadcache = ~/cache/pip - [flake8] show-source = True exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build From 62e9dc9a5a98ab016858586b381ff4005592bcf8 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 14 Dec 2015 01:29:42 -0500 Subject: [PATCH 079/553] remove py26 specific code from heatclient test_shell had helper functions when testing with py26, but in the mitaka release, py26 support was dropped from all projects, so remove some of the py26 specific code in heatclient. Change-Id: I511e2db3ddc703a65e457db647a2f8e898d3ac3d --- heatclient/tests/unit/test_shell.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index ac48b106..e3f6db91 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -14,7 +14,6 @@ import fixtures import os from oslotest import mockpatch -import re import requests import six from six.moves.urllib import parse @@ -90,26 +89,6 @@ def set_fake_env(self, fake_env): self.useFixture( fixtures.EnvironmentVariable(key, fake_env.get(key))) - # required for testing with Python 2.6 - def assertRegexpMatches(self, text, expected_regexp, msg=None): - """Fail the test unless the text matches the regular expression.""" - if isinstance(expected_regexp, six.string_types): - expected_regexp = re.compile(expected_regexp) - if not expected_regexp.search(text): - msg = msg or "Regexp didn't match" - msg = '%s: %r not found in %r' % ( - msg, expected_regexp.pattern, text) - raise self.failureException(msg) - - # required for testing with Python 2.6 - def assertNotRegexpMatches(self, text, expected_regexp, msg=None): - try: - self.assertRegexpMatches(text, expected_regexp, msg) - except self.failureException: - pass - else: - raise self.failureException(msg) - def shell_error(self, argstr, error_match, exception): orig = sys.stderr sys.stderr = six.StringIO() From 0f625acfa18e4c53892e7d044e764ace0ed85091 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 15 Dec 2015 18:59:46 +0000 Subject: [PATCH 080/553] Updated from global requirements Change-Id: I8cab2bfcf5acca0445e67bf5cb4593af8c743644 --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 45fa9461..80f910f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,10 +10,10 @@ iso8601>=0.1.9 PrettyTable<0.8,>=0.7 oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils!=3.1.0,>=2.8.0 # Apache-2.0 +oslo.utils>=3.2.0 # Apache-2.0 python-keystoneclient!=1.8.0,>=1.6.0 python-swiftclient>=2.2.0 -python-openstackclient>=1.5.0 +python-openstackclient>=2.0.0 PyYAML>=3.1.0 -requests>=2.8.1 +requests!=2.9.0,>=2.8.1 six>=1.9.0 From aeea4ec943caddac3fc74f4b1d3dce603f0b251f Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Tue, 1 Dec 2015 20:16:34 +0800 Subject: [PATCH 081/553] Fix Resource.__eq__ mismatch semantics of object equal The __eq__ of apiclient.base.Resource will return True, if the two objects have same id, even if they have different other attributes value. The behavior is weird and don't match the semantics of object equal. The objects that have different value should be different objects. Fix this issue and add some test cases in this patch. Change-Id: I5904b82d80d6011c1c3f220c357cc14e8d98da61 Closes-Bug: #1499369 --- heatclient/openstack/common/apiclient/base.py | 18 ++++- .../tests/unit/test_openstack_common.py | 75 +++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 heatclient/tests/unit/test_openstack_common.py diff --git a/heatclient/openstack/common/apiclient/base.py b/heatclient/openstack/common/apiclient/base.py index d3d772ca..13faeb67 100644 --- a/heatclient/openstack/common/apiclient/base.py +++ b/heatclient/openstack/common/apiclient/base.py @@ -39,14 +39,17 @@ import abc import copy +import logging from oslo_utils import strutils import six from six.moves.urllib import parse -from heatclient.openstack.common._i18n import _ +from heatclient.openstack.common._i18n import _, _LW from heatclient.openstack.common.apiclient import exceptions +LOG = logging.getLogger(__name__) + def getid(obj): """Return id if argument is a Resource. @@ -518,10 +521,19 @@ def __eq__(self, other): # two resources of different types are not equal if not isinstance(other, self.__class__): return False - if hasattr(self, 'id') and hasattr(other, 'id'): - return self.id == other.id + LOG.warning(_LW("Two objects are equal when all of the attributes are " + "equal, if you want to identify whether two objects " + "are same one with same id, please use is_same_obj() " + "function.")) return self._info == other._info + def is_same_obj(self, other): + """Identify the two objects are same one with same id.""" + if isinstance(other, self.__class__): + if hasattr(self, 'id') and hasattr(other, 'id'): + return self.id == other.id + return False + def is_loaded(self): return self._loaded diff --git a/heatclient/tests/unit/test_openstack_common.py b/heatclient/tests/unit/test_openstack_common.py new file mode 100644 index 00000000..3375c835 --- /dev/null +++ b/heatclient/tests/unit/test_openstack_common.py @@ -0,0 +1,75 @@ +# Copyright 2015 Huawei. +# All Rights Reserved. +# +# 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 testtools + +from heatclient.openstack.common.apiclient import base +from heatclient.v1 import events +from heatclient.v1 import stacks + + +class BaseTest(testtools.TestCase): + + def test_two_resources_with_same_id_are_not_equal(self): + # Two resources with same ID: never equal if their info is not equal + r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) + r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) + self.assertNotEqual(r1, r2) + + def test_two_resources_with_same_id_and_info_are_equal(self): + # Two resources with same ID: equal if their info is equal + r1 = base.Resource(None, {'id': 1, 'name': 'hello'}) + r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) + self.assertEqual(r1, r2) + + def test_two_resources_with_diff_type_are_not_equal(self): + # Two resoruces of different types: never equal + r1 = base.Resource(None, {'id': 1}) + r2 = events.Event(None, {'id': 1}) + self.assertNotEqual(r1, r2) + + def test_two_resources_with_no_id_are_equal(self): + # Two resources with no ID: equal if their info is equal + r1 = base.Resource(None, {'name': 'joe', 'age': 12}) + r2 = base.Resource(None, {'name': 'joe', 'age': 12}) + self.assertEqual(r1, r2) + + def test_is_same_object(self): + # Two resources with same type and same ID: is same object + r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) + r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) + self.assertTrue(r1.is_same_obj(r2)) + self.assertTrue(r2.is_same_obj(r1)) + + def test_is_diff_object_with_diff_id(self): + # Two resources with same type and different ID: is different object + r1 = base.Resource(None, {'id': 1, 'name': 'hello'}) + r2 = base.Resource(None, {'id': 2, 'name': 'hello'}) + self.assertFalse(r1.is_same_obj(r2)) + self.assertFalse(r2.is_same_obj(r1)) + + def test_is_diff_object_with_diff_type(self): + # Two resources with different types: is different object + r1 = events.Event(None, {'id': 1}) + r2 = stacks.Stack(None, {'id': 1}) + self.assertFalse(r1.is_same_obj(r2)) + self.assertFalse(r2.is_same_obj(r1)) + + def test_is_diff_object_with_no_id(self): + # Two resources with no ID: is different object + r1 = base.Resource(None, {'name': 'joe', 'age': 12}) + r2 = base.Resource(None, {'name': 'joe', 'age': 12}) + self.assertFalse(r1.is_same_obj(r2)) + self.assertFalse(r2.is_same_obj(r1)) From 7d1c02b9748a775479ead5e99589562b02d94745 Mon Sep 17 00:00:00 2001 From: "sonu.kumar" Date: Thu, 17 Dec 2015 15:16:29 +0530 Subject: [PATCH 082/553] Removes MANIFEST.in as it is not needed explicitely by PBR This patch removes `MANIFEST.in` file as pbr generates a sensible manifest from git files and some standard files and it removes the need for an explicit `MANIFEST.in` file. Change-Id: I2ec5996c9fb4ff9fc0c0cb685d5e0575e4b93471 --- MANIFEST.in | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 0d1fc6cf..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,10 +0,0 @@ -include AUTHORS -include babel.cfg -include LICENSE -include README.rst -include ChangeLog -include tox.ini -include .testr.conf -recursive-include doc * -recursive-include tools * -recursive-include python-heatclient *.po *.pot From 73c3af4b8a6e0afd5d2ea13cb6a1962be6e40370 Mon Sep 17 00:00:00 2001 From: Shuquan Huang Date: Thu, 17 Dec 2015 21:56:09 +0800 Subject: [PATCH 083/553] Replace assertEqual(None, *) with assertIsNone in tests Replace assertEqual(None, *) with assertIsNone in tests to have more clear messages in case of failure. Change-Id: I517928a81723d28999de2cfb649fc36f893b2ddb Closes-bug: #1280522 --- heatclient/tests/unit/test_template_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heatclient/tests/unit/test_template_utils.py b/heatclient/tests/unit/test_template_utils.py index 7b584cc6..5e32b896 100644 --- a/heatclient/tests/unit/test_template_utils.py +++ b/heatclient/tests/unit/test_template_utils.py @@ -439,7 +439,7 @@ def test_get_template_contents_file_none(self): def test_get_template_contents_file_none_existing(self): files, tmpl_parsed = template_utils.get_template_contents( existing=True) - self.assertEqual(None, tmpl_parsed) + self.assertIsNone(tmpl_parsed) self.assertEqual({}, files) def test_get_template_contents_parse_error(self): From 67461757ee1ea4bb11e0971a2b38eebc32d7ca03 Mon Sep 17 00:00:00 2001 From: Oleksii Chuprykov Date: Mon, 21 Dec 2015 14:53:12 +0200 Subject: [PATCH 084/553] Use stack lookup for getting stack name/id Use stack lookup without redirecting to stack show when we need to get only stack id. This significantly reduces load on engine for large stacks, because, for example, we doesn't resolve outputs for variety of api calls. Change-Id: Ifcd61f8b039c930a4c2d20d31b5ccb8b11864e5b --- heatclient/tests/unit/test_shell.py | 250 ---------------------------- heatclient/v1/stacks.py | 84 +++++----- 2 files changed, 42 insertions(+), 292 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index ac48b106..26a6e42c 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -825,19 +825,6 @@ def _output_fake_response(self, output_key): }, ] - stack_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - - stack_resp = fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)) - def find_output(key): for out in outputs: if out['output_key'] == key: @@ -850,16 +837,10 @@ def find_output(key): jsonutils.dumps(find_output(output_key))) if self.client == http.SessionClient: - self.client.request( - '/stacks/teststack/1', - 'GET').AndReturn(stack_resp) self.client.request( '/stacks/teststack/1/outputs/%s' % output_key, 'GET').AndReturn(resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((stack_resp, - stack_dict)) self.client.json_request( 'GET', '/stacks/teststack/1/outputs/%s' % output_key).AndReturn( @@ -879,19 +860,6 @@ def _error_output_fake_response(self, output_key): } } - stack_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - - stack_resp = fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)) - resp = fakes.FakeHTTPResponse( 200, 'OK', @@ -899,16 +867,10 @@ def _error_output_fake_response(self, output_key): jsonutils.dumps(resp_dict)) if self.client == http.SessionClient: - self.client.request( - '/stacks/teststack/1', - 'GET').AndReturn(stack_resp) self.client.request( '/stacks/teststack/1/outputs/%s' % output_key, 'GET').AndReturn(resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((stack_resp, - stack_dict)) self.client.json_request( 'GET', '/stacks/teststack/1/outputs/%s' % output_key).AndReturn( @@ -1668,13 +1630,6 @@ def test_stack_create_with_tags(self): def test_stack_abandon(self): self.register_keystone_auth_fixture() - resp_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - abandoned_stack = { "action": "CREATE", "status": "COMPLETE", @@ -1692,25 +1647,16 @@ def test_stack_abandon(self): } } - resp = fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(resp_dict)) abandoned_resp = fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, jsonutils.dumps(abandoned_stack)) if self.client == http.SessionClient: - self.client.request( - '/stacks/teststack/1', 'GET').AndReturn(resp) self.client.request( '/stacks/teststack/1/abandon', 'DELETE').AndReturn(abandoned_resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) http.HTTPClient.raw_request( 'DELETE', '/stacks/teststack/1/abandon').AndReturn( abandoned_resp) @@ -1722,13 +1668,6 @@ def test_stack_abandon(self): def test_stack_abandon_with_outputfile(self): self.register_keystone_auth_fixture() - resp_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - abandoned_stack = { "action": "CREATE", "status": "COMPLETE", @@ -1746,25 +1685,16 @@ def test_stack_abandon_with_outputfile(self): } } - resp = fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(resp_dict)) abandoned_resp = fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, jsonutils.dumps(abandoned_stack)) if self.client == http.SessionClient: - self.client.request( - '/stacks/teststack/1', 'GET').AndReturn(resp) self.client.request( '/stacks/teststack/1/abandon', 'DELETE').AndReturn(abandoned_resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((resp, resp_dict)) http.HTTPClient.raw_request( 'DELETE', '/stacks/teststack/1/abandon').AndReturn( abandoned_resp) @@ -2335,24 +2265,6 @@ def test_stack_update_with_tags(self): def _setup_stubs_update_dry_run(self, template_file, existing=False): self.register_keystone_auth_fixture() - resp_dict = {"stack": { - "id": "2", - "stack_name": "teststack2", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - resp = fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(resp_dict)) - if self.client == http.SessionClient: - self.client.request( - '/stacks/teststack2/2', 'GET').AndReturn(resp) - else: - self.client.json_request( - 'GET', '/stacks/teststack2/2').AndReturn((resp, resp_dict)) - template_data = open(template_file).read() replaced_res = {"resource_name": "my_res", @@ -2548,13 +2460,6 @@ def test_build_info(self): def test_stack_snapshot(self): self.register_keystone_auth_fixture() - stack_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - resp_dict = {"snapshot": { "id": "1", "creation_time": "2012-10-25T01:58:47Z" @@ -2566,24 +2471,11 @@ def test_stack_snapshot(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) if self.client == http.SessionClient: - self.client.request('/stacks/teststack/1', 'GET').AndReturn( - fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict))) self.client.request( '/stacks/teststack/1/snapshots', 'POST', data={}).AndReturn(resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn( - (fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)), stack_dict)) http.HTTPClient.json_request( 'POST', '/stacks/teststack/1/snapshots', @@ -2596,13 +2488,6 @@ def test_stack_snapshot(self): def test_snapshot_list(self): self.register_keystone_auth_fixture() - stack_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - resp_dict = {"snapshots": [{ "id": "2", "name": "snap1", @@ -2611,27 +2496,16 @@ def test_snapshot_list(self): "creation_time": "2014-12-05T01:25:52Z" }]} - stack_resp = fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)) resp = fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) if self.client == http.SessionClient: - self.client.request( - '/stacks/teststack/1', - 'GET').AndReturn(stack_resp) self.client.request( '/stacks/teststack/1/snapshots', 'GET').AndReturn(resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((stack_resp, - stack_dict)) http.HTTPClient.json_request( 'GET', '/stacks/teststack/1/snapshots').AndReturn((resp, resp_dict)) @@ -2655,13 +2529,6 @@ def test_snapshot_list(self): def test_snapshot_show(self): self.register_keystone_auth_fixture() - stack_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - resp_dict = {"snapshot": { "id": "2", "creation_time": "2012-10-25T01:58:47Z" @@ -2673,23 +2540,10 @@ def test_snapshot_show(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) if self.client == http.SessionClient: - self.client.request('/stacks/teststack/1', 'GET').AndReturn( - fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict))) self.client.request( '/stacks/teststack/1/snapshots/2', 'GET').AndReturn(resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn(( - fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)), stack_dict)) http.HTTPClient.json_request( 'GET', '/stacks/teststack/1/snapshots/2').AndReturn((resp, resp_dict)) @@ -2701,37 +2555,21 @@ def test_snapshot_show(self): def test_snapshot_delete(self): self.register_keystone_auth_fixture() - stack_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - resp_dict = {"snapshot": { "id": "2", "creation_time": "2012-10-25T01:58:47Z" }} - resp = fakes.FakeHTTPResponse( - 204, - 'No Content', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)) second_resp = fakes.FakeHTTPResponse( 204, 'No Content', {}, jsonutils.dumps(resp_dict)) if self.client == http.SessionClient: - self.client.request( - '/stacks/teststack/1', 'GET').AndReturn(resp) self.client.request( '/stacks/teststack/1/snapshots/2', 'DELETE').AndReturn(second_resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((resp, stack_dict)) http.HTTPClient.raw_request( 'DELETE', '/stacks/teststack/1/snapshots/2').AndReturn(second_resp) @@ -2743,33 +2581,16 @@ def test_snapshot_delete(self): def test_stack_restore(self): self.register_keystone_auth_fixture() - stack_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - - stack_resp = fakes.FakeHTTPResponse( - 204, - 'No Content', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)) no_resp = fakes.FakeHTTPResponse( 204, 'No Content', {'content-type': 'application/json'}, jsonutils.dumps({})) if self.client == http.SessionClient: - self.client.request( - '/stacks/teststack/1', 'GET').AndReturn(stack_resp) self.client.request( '/stacks/teststack/1/snapshots/2/restore', 'POST').AndReturn(no_resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((stack_resp, - stack_dict)) http.HTTPClient.json_request( 'POST', '/stacks/teststack/1/snapshots/2/restore').AndReturn((no_resp, @@ -2782,13 +2603,6 @@ def test_stack_restore(self): def test_output_list(self): self.register_keystone_auth_fixture() - stack_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - resp_dict = {"outputs": [{ "output_key": "key", "description": "description" @@ -2798,27 +2612,16 @@ def test_output_list(self): "description": "description1" }]} - stack_resp = fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)) resp = fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) if self.client == http.SessionClient: - self.client.request( - '/stacks/teststack/1', - 'GET').AndReturn(stack_resp) self.client.request( '/stacks/teststack/1/outputs', 'GET').AndReturn(resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn((stack_resp, - stack_dict)) http.HTTPClient.json_request( 'GET', '/stacks/teststack/1/outputs').AndReturn((resp, resp_dict)) @@ -2840,13 +2643,6 @@ def test_output_list(self): def test_output_show_all(self): self.register_keystone_auth_fixture() - stack_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - resp_dict = {'outputs': [ { 'output_key': 'key', @@ -2873,42 +2669,16 @@ def test_output_show_all(self): jsonutils.dumps(resp_dict1)) if self.client == http.SessionClient: - self.client.request('/stacks/teststack/1', 'GET').AndReturn( - fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict))) self.client.request( '/stacks/teststack/1/outputs', 'GET').AndReturn(resp) - self.client.request('/stacks/teststack/1', 'GET').AndReturn( - fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict))) self.client.request( '/stacks/teststack/1/outputs/key', 'GET').AndReturn(resp1) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn(( - fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)), stack_dict)) http.HTTPClient.json_request( 'GET', '/stacks/teststack/1/outputs').AndReturn((resp, resp_dict)) - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn(( - fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)), stack_dict)) http.HTTPClient.json_request( 'GET', '/stacks/teststack/1/outputs/key').AndReturn((resp1, @@ -2930,13 +2700,6 @@ def test_output_show_all(self): def test_output_show(self): self.register_keystone_auth_fixture() - stack_dict = {"stack": { - "id": "1", - "stack_name": "teststack", - "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" - }} - resp_dict = {"output": { "output_key": "key", "output_value": "value", @@ -2949,23 +2712,10 @@ def test_output_show(self): {'content-type': 'application/json'}, jsonutils.dumps(resp_dict)) if self.client == http.SessionClient: - self.client.request('/stacks/teststack/1', 'GET').AndReturn( - fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict))) self.client.request( '/stacks/teststack/1/outputs/key', 'GET').AndReturn(resp) else: - http.HTTPClient.json_request( - 'GET', '/stacks/teststack/1').AndReturn(( - fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(stack_dict)), stack_dict)) http.HTTPClient.json_request( 'GET', '/stacks/teststack/1/outputs/key').AndReturn((resp, resp_dict)) diff --git a/heatclient/v1/stacks.py b/heatclient/v1/stacks.py index b9b102b5..bd1cf805 100644 --- a/heatclient/v1/stacks.py +++ b/heatclient/v1/stacks.py @@ -87,7 +87,27 @@ def identifier(self): return '%s/%s' % (self.stack_name, self.id) -class StackManager(base.BaseManager): +class StackChildManager(base.BaseManager): + @property + def api(self): + return self.client + + def _resolve_stack_id(self, stack_id): + # if the id already has a slash in it, + # then it is already {stack_name}/{stack_id} + if stack_id.find('/') > 0: + return stack_id + # We want to capture the redirect, not actually get the stack, + # since all we want is the stacks:lookup response to get the + # fully qualified ID, and not all users are allowed to do the + # redirected stacks:show, so pass redirect=False + resp = self.client.get('/stacks/%s' % stack_id, redirect=False) + location = resp.headers.get('location') + path = self.client.strip_endpoint(location) + return path[len('/stacks/'):] + + +class StackManager(StackChildManager): resource_class = Stack def list(self, **kwargs): @@ -155,15 +175,15 @@ def update(self, stack_id, **kwargs): def preview_update(self, stack_id, **kwargs): """Preview a stack update.""" - s = self.get(stack_id) + stack_identifier = self._resolve_stack_id(stack_id) headers = self.client.credentials_headers() if kwargs.pop('existing', None): - resp = self.client.patch('/stacks/%s/%s/preview' % - (s.stack_name, s.id), + resp = self.client.patch('/stacks/%s/preview' % + stack_identifier, data=kwargs, headers=headers) else: - resp = self.client.put('/stacks/%s/%s/preview' % - (s.stack_name, s.id), + resp = self.client.put('/stacks/%s/preview' % + stack_identifier, data=kwargs, headers=headers) body = utils.get_response_body(resp) return body @@ -174,59 +194,59 @@ def delete(self, stack_id): def abandon(self, stack_id): """Abandon a stack.""" - stack = self.get(stack_id) - resp = self.client.delete('/stacks/%s/abandon' % stack.identifier) + stack_identifier = self._resolve_stack_id(stack_id) + resp = self.client.delete('/stacks/%s/abandon' % stack_identifier) body = utils.get_response_body(resp) return body def snapshot(self, stack_id, name=None): """Snapshot a stack.""" - stack = self.get(stack_id) + stack_identifier = self._resolve_stack_id(stack_id) data = {} if name: data['name'] = name - resp = self.client.post('/stacks/%s/snapshots' % stack.identifier, + resp = self.client.post('/stacks/%s/snapshots' % stack_identifier, data=data) body = utils.get_response_body(resp) return body def snapshot_show(self, stack_id, snapshot_id): - stack = self.get(stack_id) - resp = self.client.get('/stacks/%s/snapshots/%s' % (stack.identifier, + stack_identifier = self._resolve_stack_id(stack_id) + resp = self.client.get('/stacks/%s/snapshots/%s' % (stack_identifier, snapshot_id)) body = utils.get_response_body(resp) return body def snapshot_delete(self, stack_id, snapshot_id): - stack = self.get(stack_id) + stack_identifier = self._resolve_stack_id(stack_id) resp = self.client.delete('/stacks/%s/snapshots/%s' % - (stack.identifier, snapshot_id)) + (stack_identifier, snapshot_id)) body = utils.get_response_body(resp) return body def restore(self, stack_id, snapshot_id): - stack = self.get(stack_id) + stack_identifier = self._resolve_stack_id(stack_id) resp = self.client.post('/stacks/%s/snapshots/%s/restore' % - (stack.identifier, snapshot_id)) + (stack_identifier, snapshot_id)) body = utils.get_response_body(resp) return body def snapshot_list(self, stack_id): - stack = self.get(stack_id) - resp = self.client.get('/stacks/%s/snapshots' % stack.identifier) + stack_identifier = self._resolve_stack_id(stack_id) + resp = self.client.get('/stacks/%s/snapshots' % stack_identifier) body = utils.get_response_body(resp) return body def output_list(self, stack_id): - stack = self.get(stack_id) - resp = self.client.get('/stacks/%s/outputs' % stack.identifier) + stack_identifier = self._resolve_stack_id(stack_id) + resp = self.client.get('/stacks/%s/outputs' % stack_identifier) body = utils.get_response_body(resp) return body def output_show(self, stack_id, output_key): - stack = self.get(stack_id) + stack_identifier = self._resolve_stack_id(stack_id) resp = self.client.get('/stacks/%(id)s/outputs/%(key)s' % { - 'id': stack.identifier, + 'id': stack_identifier, 'key': output_key }) body = utils.get_response_body(resp) @@ -259,23 +279,3 @@ def validate(self, **kwargs): resp = self.client.post(url, data=kwargs) body = utils.get_response_body(resp) return body - - -class StackChildManager(base.BaseManager): - @property - def api(self): - return self.client - - def _resolve_stack_id(self, stack_id): - # if the id already has a slash in it, - # then it is already {stack_name}/{stack_id} - if stack_id.find('/') > 0: - return stack_id - # We want to capture the redirect, not actually get the stack, - # since all we want is the stacks:lookup response to get the - # fully qualified ID, and not all users are allowed to do the - # redirected stacks:show, so pass redirect=False - resp = self.client.get('/stacks/%s' % stack_id, redirect=False) - location = resp.headers.get('location') - path = self.client.strip_endpoint(location) - return path[len('/stacks/'):] From a004819e30856127f19fed138a89a7f3975b27c4 Mon Sep 17 00:00:00 2001 From: Oleksii Chuprykov Date: Tue, 15 Dec 2015 17:34:45 +0200 Subject: [PATCH 085/553] Resolve outputs from stack object There are use cases where user have new heatclient and old version of rpc api ( < 1.19) so heatclient call new api methods that doesn't exist. In this case get stack outputs from stack object. Change-Id: I6c1fdbe13ecb8ae61690e0e7d8bac31405c148c6 Closes-Bug: #1526392 --- heatclient/tests/unit/test_shell.py | 114 ++++++++++++++++++++++++++++ heatclient/v1/shell.py | 92 +++++++++++++--------- 2 files changed, 172 insertions(+), 34 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 2a7c1f4e..908f4b7a 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -2619,6 +2619,64 @@ def test_output_list(self): for r in required: self.assertRegexpMatches(list_text, r) + def test_output_list_api_400_error(self): + self.register_keystone_auth_fixture() + outputs = [{ + "output_key": "key", + "description": "description" + }, + { + "output_key": "key1", + "description": "description1" + }] + stack_dict = {"stack": { + "id": "1", + "stack_name": "teststack", + "stack_status": 'CREATE_COMPLETE', + "creation_time": "2012-10-25T01:58:47Z", + "outputs": outputs + }} + + stack_resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict)) + resp = fakes.FakeHTTPResponse( + 404, + 'Not Found', + {'content-type': 'application/json'}, + jsonutils.dumps({})) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1/outputs', + 'GET').AndRaise(exc.from_response(resp)) + self.client.request( + '/stacks/teststack/1', + 'GET').AndReturn(stack_resp) + else: + http.HTTPClient.json_request( + 'GET', + '/stacks/teststack/1/outputs').AndRaise( + exc.from_response(resp)) + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn((stack_resp, + stack_dict)) + + self.m.ReplayAll() + list_text = self.shell('output-list teststack/1') + + required = [ + 'output_key', + 'description', + 'key', + 'description', + 'key1', + 'description1' + ] + for r in required: + self.assertRegexpMatches(list_text, r) + def test_output_show_all(self): self.register_keystone_auth_fixture() @@ -2712,6 +2770,62 @@ def test_output_show(self): for r in required: self.assertRegexpMatches(resp, r) + def test_output_show_api_400_error(self): + self.register_keystone_auth_fixture() + output = { + "output_key": "key", + "output_value": "value", + 'description': 'description' + } + stack_dict = {"stack": { + "id": "1", + "stack_name": "teststack", + "stack_status": 'CREATE_COMPLETE', + "creation_time": "2012-10-25T01:58:47Z", + 'outputs': [output] + }} + + resp = fakes.FakeHTTPResponse( + 404, + 'Internal Error', + {'content-type': 'application/json'}, + jsonutils.dumps({})) + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1/outputs/key', + 'GET').AndRaise(exc.from_response(resp)) + self.client.request('/stacks/teststack/1', 'GET').AndReturn( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict))) + else: + http.HTTPClient.json_request( + 'GET', + '/stacks/teststack/1/outputs/key').AndRaise( + exc.from_response(resp)) + http.HTTPClient.json_request( + 'GET', '/stacks/teststack/1').AndReturn(( + fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(stack_dict)), stack_dict)) + + self.m.ReplayAll() + resp = self.shell('output-show teststack/1 key') + required = [ + 'output_key', + 'output_value', + 'description', + 'key', + 'value', + 'description', + ] + for r in required: + self.assertRegexpMatches(resp, r) + def test_output_show_output1(self): self.register_keystone_auth_fixture() diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 307ef7e4..1e8a69af 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -651,15 +651,18 @@ def do_output_list(hc, args): try: outputs = hc.stacks.output_list(args.id) except exc.HTTPNotFound: - raise exc.CommandError(_('Stack not found: %s') % args.id) - else: - fields = ['output_key', 'description'] - formatters = { - 'output_key': lambda x: x['output_key'], - 'description': lambda x: x['description'], - } + try: + outputs = hc.stacks.get(args.id).to_dict() + except exc.HTTPNotFound: + raise exc.CommandError(_('Stack not found: %s') % args.id) - utils.print_list(outputs['outputs'], fields, formatters=formatters) + fields = ['output_key', 'description'] + formatters = { + 'output_key': lambda x: x['output_key'], + 'description': lambda x: x['description'], + } + + utils.print_list(outputs['outputs'], fields, formatters=formatters) @utils.arg('id', metavar='', @@ -675,43 +678,64 @@ def do_output_list(hc, args): help=_('Returns only output value in specified format.')) def do_output_show(hc, args): """Show a specific stack output.""" - def show_output_by_key(output_key): + def resolve_output(output_key): try: output = hc.stacks.output_show(args.id, output_key) except exc.HTTPNotFound: - raise exc.CommandError(_('Stack %(id)s or ' - 'output %(key)s not found.') % { - 'id': args.id, - 'key': args.output}) - else: - if 'output_error' in output['output']: - msg = _("Output error: %s") % output['output']['output_error'] - raise exc.CommandError(msg) - if args.only_value: - if (args.format == 'json' - or isinstance(output['output']['output_value'], dict) - or isinstance(output['output']['output_value'], list)): - print( - utils.json_formatter(output['output']['output_value'])) - else: - print(output['output']['output_value']) + try: + output = None + stack = hc.stacks.get(args.id).to_dict() + for o in stack.get('outputs', []): + if o['output_key'] == output_key: + output = {'output': o} + break + if output is None: + raise exc.CommandError(_('Output %(key)s not found.') % { + 'key': args.output}) + except exc.HTTPNotFound: + raise exc.CommandError( + _('Stack %(id)s or output %(key)s not found.') % { + 'id': args.id, + 'key': args.output}) + return output + + def show_output(output): + if 'output_error' in output['output']: + msg = _("Output error: %s") % output['output']['output_error'] + raise exc.CommandError(msg) + if args.only_value: + if (args.format == 'json' + or isinstance(output['output']['output_value'], dict) + or isinstance(output['output']['output_value'], list)): + print( + utils.json_formatter(output['output']['output_value'])) else: - formatters = { - 'output_value': (lambda x: utils.json_formatter(x) - if args.format == 'json' - else x) - } - utils.print_dict(output['output'], formatters=formatters) + print(output['output']['output_value']) + else: + formatters = { + 'output_value': (lambda x: utils.json_formatter(x) + if args.format == 'json' + else x) + } + utils.print_dict(output['output'], formatters=formatters) if args.all: try: outputs = hc.stacks.output_list(args.id) + resolved = False except exc.HTTPNotFound: - raise exc.CommandError(_('Stack not found: %s') % args.id) + try: + outputs = hc.stacks.get(args.id).to_dict() + resolved = True + except exc.HTTPNotFound: + raise exc.CommandError(_('Stack not found: %s') % args.id) for output in outputs['outputs']: - show_output_by_key(output['output_key']) + if resolved: + show_output({'output': output}) + else: + show_output(resolve_output(output['output_key'])) else: - show_output_by_key(args.output) + show_output(resolve_output(args.output)) @utils.arg('-f', '--filters', metavar='', From 7401335f85614650f7248f6eb122da29092c7ef9 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Sun, 27 Dec 2015 01:29:53 -0500 Subject: [PATCH 086/553] use keystoneclient exceptions instead of oslo-incubator code depending on any oslo-incubator code from another project is dangerous. keystoneclient makes its exceptions public and it's not recommended to use any code from keystoneclient.openstack.common.apiclient since it's maintained by oslo-incubator. Change-Id: Id1f5d1ddc5b297c9547c6d16d3ee70496c7852f4 --- heatclient/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heatclient/shell.py b/heatclient/shell.py index 019df3ec..5eeb95a5 100644 --- a/heatclient/shell.py +++ b/heatclient/shell.py @@ -28,7 +28,7 @@ from keystoneclient.auth.identity import v2 as v2_auth from keystoneclient.auth.identity import v3 as v3_auth from keystoneclient import discover -from keystoneclient.openstack.common.apiclient import exceptions as ks_exc +from keystoneclient import exceptions as ks_exc from keystoneclient import session as kssession import heatclient From f349b4d25d6afb2a27747f23177b09a5da9af71b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 1 Jan 2016 20:32:40 +0000 Subject: [PATCH 087/553] Updated from global requirements Change-Id: Idc6805f7878c26fd61b7a3d7bc2065279db5281f --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 4b85cd03..342a75f7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ mox3>=0.7.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -tempest-lib>=0.11.0 +tempest-lib>=0.12.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 From b65817fa43fa12dc7dbade45c7954714e7179860 Mon Sep 17 00:00:00 2001 From: zhangguoqing Date: Mon, 4 Jan 2016 05:25:40 +0000 Subject: [PATCH 088/553] Change LOG.warn to LOG.warning Python 3 deprecated the logger.warn method, see: https://docs.python.org/3/library/logging.html#logging.warning so we prefer to use warning to avoid DeprecationWarning. Change-Id: Id0c5d67a763121c3245316955c4901a0dbea73ad Closes-Bug: #1530742 --- heatclient/common/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heatclient/common/http.py b/heatclient/common/http.py index 1fbcdcdc..86bcc2ce 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -54,7 +54,7 @@ def get_system_ca_file(): if os.path.exists(ca): LOG.debug("Using ca file %s", ca) return ca - LOG.warn(_LW("System ca file could not be found.")) + LOG.warning(_LW("System ca file could not be found.")) class HTTPClient(object): From 63c3bf0fbd43b9ac55f545763342832d4a142b53 Mon Sep 17 00:00:00 2001 From: neetu Date: Tue, 5 Jan 2016 13:19:25 -0600 Subject: [PATCH 089/553] Fix error message on authentication failure Removing the "please try again" part of the error message on authentication failure. Change-Id: I5b7424915d7a62e08b4e6cb1cd89c7b304afa24b closes-Bug: #1526944 --- heatclient/common/http.py | 8 +------- heatclient/tests/unit/test_common_http.py | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/heatclient/common/http.py b/heatclient/common/http.py index 86bcc2ce..d08bdef3 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -209,13 +209,7 @@ def _http_request(self, url, method, **kwargs): if not ('X-Auth-Key' in kwargs['headers']) and ( resp.status_code == 401 or (resp.status_code == 500 and "(HTTP 401)" in resp.content)): - raise exc.HTTPUnauthorized(_("Authentication failed. Please try" - " again with option %(option)s or " - "export %(var)s\n%(content)s") % - { - 'option': '--include-password', - 'var': 'HEAT_INCLUDE_PASSWORD=1', - 'content': resp.content}) + raise exc.HTTPUnauthorized("Authentication failed") elif 400 <= resp.status_code < 600: raise exc.from_response(resp) elif resp.status_code in (301, 302, 305): diff --git a/heatclient/tests/unit/test_common_http.py b/heatclient/tests/unit/test_common_http.py index bb2fa20b..7d146023 100644 --- a/heatclient/tests/unit/test_common_http.py +++ b/heatclient/tests/unit/test_common_http.py @@ -181,7 +181,7 @@ def test_not_include_pass(self): client = http.HTTPClient('http://example.com:8004') e = self.assertRaises(exc.HTTPUnauthorized, client.raw_request, 'GET', '') - self.assertIn('include-password', str(e)) + self.assertIn('Authentication failed', str(e)) def test_region_name(self): # Record a 200 From b06c3daeb78ce15b6a77ebfe75056d3ebed6a023 Mon Sep 17 00:00:00 2001 From: Jay Dobies Date: Wed, 6 Jan 2016 11:41:56 -0500 Subject: [PATCH 090/553] Added -P support to template-validate Change-Id: Ia6945d6b3f86966a6e585866447db245b1efab2d Partial-Bug: #1508857 --- heatclient/tests/unit/test_shell.py | 36 +++++++++++++++++++++++++++++ heatclient/v1/shell.py | 8 ++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 908f4b7a..7a94144c 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -955,6 +955,42 @@ def test_template_show_hot(self): for r in required: self.assertRegexpMatches(show_text, r) + def test_template_validate(self): + self.register_keystone_auth_fixture() + resp_dict = {"heat_template_version": "2013-05-23", + "parameters": {}, + "resources": {}, + "outputs": {}} + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(resp_dict)) + if self.client == http.SessionClient: + self.client.request('/validate', + 'POST', + data=mox.IgnoreArg() + ).AndReturn(resp) + else: + self.client.json_request('POST', + '/validate', + data=mox.IgnoreArg() + ).AndReturn((resp, resp_dict)) + + self.m.ReplayAll() + + template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') + cmd = 'template-validate -f %s -P foo=bar' % template_file + show_text = self.shell(cmd) + required = [ + 'heat_template_version', + 'outputs', + 'parameters', + 'resources' + ] + for r in required: + self.assertRegexpMatches(show_text, r) + def _test_stack_preview(self, timeout=None, enable_rollback=False, tags=None): self.register_keystone_auth_fixture() diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 1e8a69af..7be7621e 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -816,8 +816,13 @@ def do_template_show(hc, args): help=_('URL to retrieve template object (e.g. from swift).')) @utils.arg('-n', '--show-nested', default=False, action="store_true", help=_('Resolve parameters from nested templates as well.')) +@utils.arg('-P', '--parameters', metavar='', + help=_('Parameter values for the template. ' + 'This can be specified multiple times, or once with ' + 'parameters separated by a semicolon.'), + action='append') def do_template_validate(hc, args): - '''Validate a template with parameters.''' + """Validate a template with parameters.""" tpl_files, template = template_utils.get_template_contents( args.template_file, @@ -829,6 +834,7 @@ def do_template_validate(hc, args): env_paths=args.environment_file) fields = { 'template': template, + 'parameters': utils.format_parameters(args.parameters), 'files': dict(list(tpl_files.items()) + list(env_files.items())), 'environment': env, } From 3b242e95750d323f5e2d2a439d37ce16c378820e Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 8 Jan 2016 15:52:36 -0500 Subject: [PATCH 091/553] Update --template-object to support nested stacks This patch updates heatclient so that it supports creating stacks from an object (Swift container) which contain nested stack relative links to nested stack URLs. The motivation here: We would like to be able to use heatclient to create stacks directly from Swift containers in TripleO. The --template-object support already exists and is great... it just needs to support loading the associated nested stacks as well. Change-Id: I53ac83e9f9985224d19993935467c03611aaef6a Closes-bug: #1532326 --- heatclient/common/template_utils.py | 30 ++++++++++++++------ heatclient/tests/unit/test_template_utils.py | 27 ++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index 4fa72d86..2134a761 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -30,6 +30,7 @@ def get_template_contents(template_file=None, template_url=None, template_object=None, object_request=None, files=None, existing=False): + is_object = False # Transform a bare file path to a file:// URL. if template_file: template_url = utils.normalise_file_path_to_url(template_file) @@ -38,6 +39,7 @@ def get_template_contents(template_file=None, template_url=None, tpl = request.urlopen(template_url).read() elif template_object: + is_object = True template_url = template_object tpl = object_request and object_request('GET', template_object) @@ -66,11 +68,13 @@ def get_template_contents(template_file=None, template_url=None, tmpl_base_url = utils.base_url_for_url(template_url) if files is None: files = {} - resolve_template_get_files(template, files, tmpl_base_url) + resolve_template_get_files(template, files, tmpl_base_url, is_object, + object_request) return files, template -def resolve_template_get_files(template, files, template_base_url): +def resolve_template_get_files(template, files, template_base_url, + is_object=False, object_request=None): def ignore_if(key, value): if key != 'get_file' and key != 'type': @@ -86,7 +90,7 @@ def recurse_if(value): return isinstance(value, (dict, list)) get_file_contents(template, files, template_base_url, - ignore_if, recurse_if) + ignore_if, recurse_if, is_object, object_request) def is_template(file_content): @@ -100,7 +104,8 @@ def is_template(file_content): def get_file_contents(from_data, files, base_url=None, - ignore_if=None, recurse_if=None): + ignore_if=None, recurse_if=None, + is_object=False, object_request=None): if recurse_if and recurse_if(from_data): if isinstance(from_data, dict): @@ -108,7 +113,8 @@ def get_file_contents(from_data, files, base_url=None, else: recurse_data = from_data for value in recurse_data: - get_file_contents(value, files, base_url, ignore_if, recurse_if) + get_file_contents(value, files, base_url, ignore_if, recurse_if, + is_object, object_request) if isinstance(from_data, dict): for key, value in six.iteritems(from_data): @@ -120,10 +126,18 @@ def get_file_contents(from_data, files, base_url=None, str_url = parse.urljoin(base_url, value) if str_url not in files: - file_content = utils.read_url_content(str_url) + if is_object and object_request: + file_content = object_request('GET', str_url) + else: + file_content = utils.read_url_content(str_url) if is_template(file_content): - template = get_template_contents( - template_url=str_url, files=files)[1] + if is_object: + template = get_template_contents( + template_object=str_url, files=files, + object_request=object_request)[1] + else: + template = get_template_contents( + template_url=str_url, files=files)[1] file_content = jsonutils.dumps(template) files[str_url] = file_content # replace the data value with the normalised absolute URL diff --git a/heatclient/tests/unit/test_template_utils.py b/heatclient/tests/unit/test_template_utils.py index 5e32b896..29935480 100644 --- a/heatclient/tests/unit/test_template_utils.py +++ b/heatclient/tests/unit/test_template_utils.py @@ -493,6 +493,33 @@ def object_request(method, object_url): self.assertEqual({}, files) self.assertTrue(self.object_requested) + def test_get_nested_stack_template_contents_object(self): + tmpl = ('{"heat_template_version": "2016-04-08",' + '"resources": {' + '"FooBar": {' + '"type": "foo/bar.yaml"}}}') + url = 'http://no.where/path/to/a.yaml' + self.m.ReplayAll() + + self.object_requested = False + + def object_request(method, object_url): + self.object_requested = True + self.assertEqual('GET', method) + self.assertTrue(object_url.startswith("http://no.where/path/to/")) + if object_url == url: + return tmpl + else: + return '{"heat_template_version": "2016-04-08"}' + + files, tmpl_parsed = template_utils.get_template_contents( + template_object=url, + object_request=object_request) + + self.assertEqual(files['http://no.where/path/to/foo/bar.yaml'], + '{"heat_template_version": "2016-04-08"}') + self.assertTrue(self.object_requested) + def check_non_utf8_content(self, filename, content): base_url = 'file:///tmp' url = '%s/%s' % (base_url, filename) From 5e92ba8f441ddc244ce5aa02e1abe560d1de1969 Mon Sep 17 00:00:00 2001 From: LiuNanke Date: Sat, 9 Jan 2016 21:17:34 +0800 Subject: [PATCH 092/553] Replace deprecated library function os.popen() with subprocess os.popen() is deprecated since version 2.6. Resolved with use of subprocess module. Closes-bug: #1529836 Change-Id: I04978ecdce04c8817c4c1c231ba7e65b908f5471 --- doc/source/conf.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 7b66a52d..e483b53c 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -24,6 +24,7 @@ # serve to show the default. import os +import subprocess # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -146,8 +147,10 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' -git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1" -html_last_updated_fmt = os.popen(git_cmd).read() +git_cmd = ["git", "log", "--pretty=format:'%ad, commit %h'", "--date=local", + "-n1"] +html_last_updated_fmt = subprocess.Popen( + git_cmd, stdout=subprocess.PIPE).communicate()[0] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. From 39c3fc41a0d7862df862657d4a565a304aa5f168 Mon Sep 17 00:00:00 2001 From: "ting.wang" Date: Tue, 12 Jan 2016 11:16:39 +0800 Subject: [PATCH 093/553] Add debug testenv in tox Once we add debug testenv, we can use "tox -e debug -- --debug" to debug test cases when tox is running. Change-Id: I3b7a090042e95ac89d6829495bf42f84e11bbd21 --- tox.ini | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tox.ini b/tox.ini index 1fb767d9..18fb577a 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,17 @@ deps = setuptools<3.2 -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt +[testenv:debug] +commands = oslo_debug_helper -t heatclient/tests {posargs} + +[testenv:debug-py27] +basepython = python2.7 +commands = oslo_debug_helper -t heatclient/tests {posargs} + +[testenv:debug-py34] +basepython = python3.4 +commands = oslo_debug_helper -t heatclient/tests {posargs} + [testenv:pep8] commands = flake8 From 39fb1243af1a3cdaf455f7ce17cba26e1e58b8c5 Mon Sep 17 00:00:00 2001 From: Thomas Herve Date: Tue, 5 Jan 2016 13:10:50 +0100 Subject: [PATCH 094/553] Allow event specification in the environment For additional validation we duplicate environment format on the client side, so to be able to create stacks with event sinks, we need to add the key here as well. Change-Id: Ia77fcdd68ee4020d0aa3d3235d38b2e5f268b7dd --- heatclient/common/environment_format.py | 7 +++++-- heatclient/tests/unit/test_environment_format.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/heatclient/common/environment_format.py b/heatclient/common/environment_format.py index c5f3e942..179a2055 100644 --- a/heatclient/common/environment_format.py +++ b/heatclient/common/environment_format.py @@ -16,8 +16,11 @@ import yaml -SECTIONS = (PARAMETER_DEFAULTS, PARAMETERS, RESOURCE_REGISTRY) = \ - ('parameter_defaults', 'parameters', 'resource_registry') +SECTIONS = ( + PARAMETER_DEFAULTS, PARAMETERS, RESOURCE_REGISTRY, EVENT_SINKS +) = ( + 'parameter_defaults', 'parameters', 'resource_registry', 'event_sinks' +) def parse(env_str): diff --git a/heatclient/tests/unit/test_environment_format.py b/heatclient/tests/unit/test_environment_format.py index faea505d..e4145b95 100644 --- a/heatclient/tests/unit/test_environment_format.py +++ b/heatclient/tests/unit/test_environment_format.py @@ -30,6 +30,7 @@ def test_minimal_yaml(self): parameter_defaults: {} parameters: {} resource_registry: {} +event_sinks: {} ''' tpl1 = environment_format.parse(yaml1) environment_format.default_for_missing(tpl1) From 73a0d69ce110000086004c90da7579c91042c17b Mon Sep 17 00:00:00 2001 From: Oleksii Chuprykov Date: Tue, 12 Jan 2016 15:05:06 +0200 Subject: [PATCH 095/553] Add resolve_outputs parameter to stack get method Also add --no-resolve-outputs parameter to heat stack-show command to let user get stack without outputs. @APIImpact, @DOCImpact Depends-On: I03104efc535a7bd4326cbec56c01cd887db38ab6 Change-Id: I6c715fe90e42259e744b76924673194d0a81c07e --- heatclient/tests/unit/test_shell.py | 40 +++++++++++++++++++++++++++++ heatclient/v1/shell.py | 5 +++- heatclient/v1/stacks.py | 9 +++++-- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 908f4b7a..e3008ddf 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -784,6 +784,46 @@ def test_stack_show(self): for r in required: self.assertRegexpMatches(list_text, r) + def test_stack_show_without_outputs(self): + self.register_keystone_auth_fixture() + resp_dict = {"stack": { + "id": "1", + "stack_name": "teststack", + "stack_status": 'CREATE_COMPLETE', + "creation_time": "2012-10-25T01:58:47Z" + }} + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {'content-type': 'application/json'}, + jsonutils.dumps(resp_dict)) + params = {'resolve_outputs': False} + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack/1', + 'GET', params=params).AndReturn(resp) + else: + self.client.json_request( + 'GET', '/stacks/teststack/1', params=params + ).AndReturn((resp, resp_dict)) + + self.m.ReplayAll() + + list_text = self.shell( + 'stack-show teststack/1 --no-resolve-outputs') + + required = [ + 'id', + 'stack_name', + 'stack_status', + 'creation_time', + 'teststack', + 'CREATE_COMPLETE', + '2012-10-25T01:58:47Z' + ] + for r in required: + self.assertRegexpMatches(list_text, r) + def _output_fake_response(self, output_key): outputs = [ diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 1e8a69af..998065f4 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -387,9 +387,12 @@ def do_action_check(hc, args): @utils.arg('id', metavar='', help=_('Name or ID of stack to describe.')) +@utils.arg('--no-resolve-outputs', action="store_true", + help='Do not resolve outputs of the stack.') def do_stack_show(hc, args): '''Describe the stack.''' - fields = {'stack_id': args.id} + fields = {'stack_id': args.id, + 'resolve_outputs': not args.no_resolve_outputs} _do_stack_show(hc, fields) diff --git a/heatclient/v1/stacks.py b/heatclient/v1/stacks.py index bd1cf805..5a4f9c59 100644 --- a/heatclient/v1/stacks.py +++ b/heatclient/v1/stacks.py @@ -252,12 +252,17 @@ def output_show(self, stack_id, output_key): body = utils.get_response_body(resp) return body - def get(self, stack_id): + def get(self, stack_id, resolve_outputs=True): """Get the metadata for a specific stack. :param stack_id: Stack ID to lookup + :param resolve_outputs: If True, then outputs for this + stack will be resolved """ - resp = self.client.get('/stacks/%s' % stack_id) + kwargs = {} + if not resolve_outputs: + kwargs['params'] = {"resolve_outputs": False} + resp = self.client.get('/stacks/%s' % stack_id, **kwargs) body = utils.get_response_body(resp) return Stack(self, body.get('stack')) From 8da7f954d30ad1ff55c44d22a9e32568df0d5db9 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 13 Jan 2016 03:12:48 +0000 Subject: [PATCH 096/553] Updated from global requirements Change-Id: I96af2211b89dd22176f6ccae16b39ddb52c163af --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 342a75f7..49501063 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ mox3>=0.7.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -tempest-lib>=0.12.0 +tempest-lib>=0.13.0 testrepository>=0.0.18 testscenarios>=0.4 testtools>=1.4.0 From 1e5b0f8fbf74384c6f16bbd78f8067034fbbcee9 Mon Sep 17 00:00:00 2001 From: Herman Ge Date: Wed, 13 Jan 2016 05:59:16 -0500 Subject: [PATCH 097/553] Misspelling in message Misspelling in following message: "# refactor this code once this functionality is avaible in" Should be: "# refactor this code once this functionality is available in" Totally 1 occurrence in python-heatclient base code. Change-Id: Icc6148125e63bbd16811dad77718267dc8092605 --- heatclient/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heatclient/shell.py b/heatclient/shell.py index 5eeb95a5..f5ab78c3 100644 --- a/heatclient/shell.py +++ b/heatclient/shell.py @@ -47,7 +47,7 @@ def _append_global_identity_args(self, parser): # FIXME(gyee): these are global identity (Keystone) arguments which # should be consistent and shared by all service clients. Therefore, # they should be provided by python-keystoneclient. We will need to - # refactor this code once this functionality is avaible in + # refactor this code once this functionality is available in # python-keystoneclient. parser.add_argument( '-k', '--insecure', default=False, action='store_true', From c2d40fe05c373428ff4edd04904aec5152a0b695 Mon Sep 17 00:00:00 2001 From: neetu Date: Wed, 13 Jan 2016 15:36:37 -0600 Subject: [PATCH 098/553] Validate for empty file for stack-adopt Check if adopt-file has content and if not then display error Change-Id: I40efd6ae3ca36f0331a1e4239c5139e0af654272 Closes-Bug: #1299161 --- heatclient/tests/unit/test_shell.py | 10 ++++++++++ heatclient/v1/shell.py | 3 +++ 2 files changed, 13 insertions(+) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 908f4b7a..04503bf7 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -1765,6 +1765,16 @@ def test_stack_adopt_without_data(self): self.shell_error('stack-adopt teststack ', failed_msg, exception=exc.CommandError) + def test_stack_adopt_empty_data_file(self): + failed_msg = 'Invalid adopt-file, no data!' + self.register_keystone_auth_fixture() + self.m.ReplayAll() + with tempfile.NamedTemporaryFile() as file_obj: + self.shell_error( + 'stack-adopt teststack ' + '--adopt-file=%s ' % (file_obj.name), + failed_msg, exception=exc.CommandError) + def test_stack_update_enable_rollback(self): self.register_keystone_auth_fixture() template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 1e8a69af..5a33dc79 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -202,6 +202,9 @@ def do_stack_adopt(hc, args): adopt_url = utils.normalise_file_path_to_url(args.adopt_file) adopt_data = request.urlopen(adopt_url).read() + if not len(adopt_data): + raise exc.CommandError('Invalid adopt-file, no data!') + if args.create_timeout: logger.warning(_LW('%(arg1)s is deprecated, ' 'please use %(arg2)s instead'), From b9c176efa70dba547b5df7022b18c125490594ec Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 3 Nov 2015 19:14:07 +0000 Subject: [PATCH 099/553] OpenstackClient plugin for stack create This change implements the "openstack stack create" command. Blueprint: heat-support-python-openstackclient Change-Id: I32289f53d482a817f7449724cfbabc2c154b1ffd --- heatclient/common/template_utils.py | 38 +++++ heatclient/osc/v1/stack.py | 161 +++++++++++++++++- heatclient/tests/test_templates/empty.yaml | 1 + .../tests/test_templates/parameters.yaml | 7 + heatclient/tests/unit/osc/v1/test_stack.py | 118 ++++++++++++- heatclient/v1/shell.py | 31 +--- setup.cfg | 1 + 7 files changed, 318 insertions(+), 39 deletions(-) create mode 100644 heatclient/tests/test_templates/empty.yaml create mode 100644 heatclient/tests/test_templates/parameters.yaml diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index 2134a761..d6e366f8 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -16,6 +16,7 @@ import collections from oslo_serialization import jsonutils import six +from six.moves.urllib import error from six.moves.urllib import parse from six.moves.urllib import request @@ -26,6 +27,19 @@ from heatclient.openstack.common._i18n import _ +def process_template_path(template_path, object_request=None): + """Read template from template path. + + Attempt to read template first as a file or url. If that is unsuccessful, + try again to assuming path is to a template object. + """ + try: + return get_template_contents(template_file=template_path) + except error.URLError: + return get_template_contents(template_object=template_path, + object_request=object_request) + + def get_template_contents(template_file=None, template_url=None, template_object=None, object_request=None, files=None, existing=False): @@ -235,3 +249,27 @@ def ignore_if(key, value): res_base_url = res_dict.get('base_url', base_url) get_file_contents( res_dict, files, res_base_url, ignore_if) + + +def hooks_to_env(env, arg_hooks, hook): + """Add hooks from args to environment's resource_registry section. + + Hooks are either "resource_name" (if it's a top-level resource) or + "nested_stack/resource_name" (if the resource is in a nested stack). + + The environment expects each hook to be associated with the resource + within `resource_registry/resources` using the `hooks: pre-create` format. + """ + if 'resource_registry' not in env: + env['resource_registry'] = {} + if 'resources' not in env['resource_registry']: + env['resource_registry']['resources'] = {} + for hook_declaration in arg_hooks: + hook_path = [r for r in hook_declaration.split('/') if r] + resources = env['resource_registry']['resources'] + for nested_stack in hook_path: + if nested_stack not in resources: + resources[nested_stack] = {} + resources = resources[nested_stack] + else: + resources['hooks'] = hook diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 6e471f08..34f7ccbf 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -21,11 +21,148 @@ from openstackclient.common import parseractions from openstackclient.common import utils +from heatclient.common import http +from heatclient.common import template_utils from heatclient.common import utils as heat_utils from heatclient import exc as heat_exc from heatclient.openstack.common._i18n import _ +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.""" + + log = logging.getLogger(__name__ + '.CreateStack') + + def get_parser(self, prog_name): + parser = super(CreateStack, self).get_parser(prog_name) + parser.add_argument( + '-t', '--template', + metavar='', + required=True, + help=_('Path to the template') + ) + parser.add_argument( + '-e', '--environment', + metavar='', + action='append', + help=_('Path to the environment. Can be specified multiple times') + ) + parser.add_argument( + '--timeout', + metavar='', + type=int, + help=_('Stack creating timeout in minutes') + ) + parser.add_argument( + '--pre-create', + metavar='', + default=None, + action='append', + help=_('Name of a resource to set a pre-create hook to. Resources ' + 'in nested stacks can be set using slash as a separator: ' + 'nested_stack/another/my_resource. You can use wildcards ' + 'to match multiple stacks or resources: ' + 'nested_stack/an*/*_resource. This can be specified ' + 'multiple times') + ) + parser.add_argument( + '--enable-rollback', + action='store_true', + help=_('Enable rollback on create/update failure') + ) + parser.add_argument( + '--parameter', + metavar='', + action='append', + help=_('Parameter values used to create the stack. This can be ' + 'specified multiple times') + ) + parser.add_argument( + '--parameter-file', + metavar='', + action='append', + help=_('Parameter values from file used to create the stack. ' + 'This can be specified multiple times. Parameter values ' + 'would be the content of the file') + ) + parser.add_argument( + '--wait', + action='store_true', + help=_('Wait until stack completes') + ) + parser.add_argument( + '--tags', + metavar='', + help=_('A list of tags to associate with the stack') + ) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the stack to create') + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + tpl_files, template = template_utils.process_template_path( + parsed_args.template, + object_request=_authenticated_fetcher(client)) + + env_files, env = ( + template_utils.process_multiple_environments_and_files( + env_paths=parsed_args.environment)) + + parameters = heat_utils.format_all_parameters( + parsed_args.parameter, + parsed_args.parameter_file, + parsed_args.template) + + if parsed_args.pre_create: + template_utils.hooks_to_env(env, parsed_args.pre_create, + 'pre-create') + + fields = { + 'stack_name': parsed_args.name, + 'disable_rollback': not parsed_args.enable_rollback, + 'parameters': parameters, + 'template': template, + 'files': dict(list(tpl_files.items()) + list(env_files.items())), + 'environment': env + } + + if parsed_args.tags: + fields['tags'] = parsed_args.tags + if parsed_args.timeout: + fields['timeout_mins'] = parsed_args.timeout + + stack = client.stacks.create(**fields)['stack'] + if parsed_args.wait: + if not utils.wait_for_status(client.stacks.get, parsed_args.name, + status_field='stack_status', + success_status='create_complete', + error_status='create_failed'): + + msg = _('Stack %s failed to create.') % parsed_args.name + raise exc.CommandError(msg) + + return _show_stack(client, stack['id'], format='table', short=True) + + class ShowStack(show.ShowOne): """Show stack details""" @@ -48,7 +185,7 @@ def take_action(self, parsed_args): format=parsed_args.formatter) -def _show_stack(heat_client, stack_id, format): +def _show_stack(heat_client, stack_id, format='', short=False): try: data = heat_client.stacks.get(stack_id=stack_id) except heat_exc.HTTPNotFound: @@ -63,15 +200,21 @@ def _show_stack(heat_client, stack_id, format): 'updated_time', 'stack_status', 'stack_status_reason', - 'parameters', - 'outputs', - 'links', ] - exclude_columns = ('template_description',) - for key in data.to_dict(): - # add remaining columns without an explicit order - if key not in columns and key not in exclude_columns: - columns.append(key) + + if not short: + columns += [ + 'parameters', + 'outputs', + 'links', + ] + + exclude_columns = ('template_description',) + for key in data.to_dict(): + # add remaining columns without an explicit order + if key not in columns and key not in exclude_columns: + columns.append(key) + formatters = {} complex_formatter = None if format in 'table': diff --git a/heatclient/tests/test_templates/empty.yaml b/heatclient/tests/test_templates/empty.yaml new file mode 100644 index 00000000..d7e80909 --- /dev/null +++ b/heatclient/tests/test_templates/empty.yaml @@ -0,0 +1 @@ +heat_template_version: 2013-05-23 diff --git a/heatclient/tests/test_templates/parameters.yaml b/heatclient/tests/test_templates/parameters.yaml new file mode 100644 index 00000000..6be23211 --- /dev/null +++ b/heatclient/tests/test_templates/parameters.yaml @@ -0,0 +1,7 @@ +heat_template_version: 2013-05-23 + +parameters: + p1: + type: string + p2: + type: number diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index 2f33ed1a..bf2fa5f3 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -32,6 +32,111 @@ def setUp(self): self.stack_client = self.app.client_manager.orchestration.stacks +class TestStackCreate(TestStack): + + template_path = 'heatclient/tests/test_templates/empty.yaml' + + defaults = { + 'stack_name': 'my_stack', + 'disable_rollback': True, + 'parameters': {}, + 'template': {'heat_template_version': '2013-05-23'}, + 'files': {}, + 'environment': {} + } + + def setUp(self): + super(TestStackCreate, self).setUp() + self.cmd = stack.CreateStack(self.app, None) + self.stack_client.create = mock.MagicMock( + return_value={'stack': {'id': '1234'}}) + self.stack_client.get = mock.MagicMock( + return_value={'stack_status': 'create_complete'}) + stack._authenticated_fetcher = mock.MagicMock() + + def test_stack_create_defaults(self): + arglist = ['my_stack', '-t', self.template_path] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**self.defaults) + + def test_stack_create_rollback(self): + arglist = ['my_stack', '-t', self.template_path, '--enable-rollback'] + kwargs = copy.deepcopy(self.defaults) + kwargs['disable_rollback'] = False + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**kwargs) + + def test_stack_create_parameters(self): + template_path = ('/'.join(self.template_path.split('/')[:-1]) + + '/parameters.yaml') + arglist = ['my_stack', '-t', template_path, '--parameter', 'p1=a', + '--parameter', 'p2=6'] + kwargs = copy.deepcopy(self.defaults) + kwargs['parameters'] = {'p1': 'a', 'p2': '6'} + kwargs['template']['parameters'] = {'p1': {'type': 'string'}, + 'p2': {'type': 'number'}} + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**kwargs) + + def test_stack_create_tags(self): + arglist = ['my_stack', '-t', self.template_path, '--tags', 'tag1,tag2'] + kwargs = copy.deepcopy(self.defaults) + kwargs['tags'] = 'tag1,tag2' + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**kwargs) + + def test_stack_create_timeout(self): + arglist = ['my_stack', '-t', self.template_path, '--timeout', '60'] + kwargs = copy.deepcopy(self.defaults) + kwargs['timeout_mins'] = 60 + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**kwargs) + + def test_stack_create_pre_create(self): + arglist = ['my_stack', '-t', self.template_path, '--pre-create', 'a'] + kwargs = copy.deepcopy(self.defaults) + kwargs['environment'] = { + 'resource_registry': {'resources': {'a': {'hooks': 'pre-create'}}} + } + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**kwargs) + + def test_stack_create_wait(self): + arglist = ['my_stack', '-t', self.template_path, '--wait'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**self.defaults) + self.stack_client.get.assert_called_with(**{'stack_id': '1234'}) + + @mock.patch('openstackclient.common.utils.wait_for_status', + return_value=False) + def test_stack_create_wait_fail(self, mock_wait): + arglist = ['my_stack', '-t', self.template_path, '--wait'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + class TestStackShow(TestStack): scenarios = [ @@ -75,17 +180,26 @@ class TestStackShow(TestStack): def setUp(self): super(TestStackShow, self).setUp() self.cmd = stack.ShowStack(self.app, None) + self.stack_client.get = mock.Mock( + return_value=stacks.Stack(None, self.get_response)) def test_stack_show(self): arglist = ['--format', self.format, 'my_stack'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.stack_client.get = mock.Mock( - return_value=stacks.Stack(None, self.get_response)) self.cmd.take_action(parsed_args) self.stack_client.get.assert_called_with(**{ 'stack_id': 'my_stack', }) + def test_stack_show_short(self): + expected = ['id', 'stack_name', 'description', 'creation_time', + 'updated_time', 'stack_status', 'stack_status_reason'] + + columns, data = stack._show_stack(self.mock_client, 'my_stack', + short=True) + + self.assertEqual(expected, columns) + class TestStackList(TestStack): diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index d8576027..1fc6b4e1 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -116,7 +116,7 @@ def do_stack_create(hc, args): 'arg2': '-t/--timeout'}) if args.pre_create: - hooks_to_env(env, args.pre_create, 'pre-create') + template_utils.hooks_to_env(env, args.pre_create, 'pre-create') fields = { 'stack_name': args.name, @@ -139,32 +139,7 @@ def do_stack_create(hc, args): hc.stacks.create(**fields) do_stack_list(hc) if args.poll is not None: - _poll_for_events(hc, args.name, 'CREATE', poll_period=args.poll) - - -def hooks_to_env(env, arg_hooks, hook): - '''Add hooks from args to environment's resource_registry section. - - Hooks are either "resource_name" (if it's a top-level resource) or - "nested_stack/resource_name" (if the resource is in a nested stack). - - The environment expects each hook to be associated with the resource - within `resource_registry/resources` using the `hooks: pre-create` format. - - ''' - if 'resource_registry' not in env: - env['resource_registry'] = {} - if 'resources' not in env['resource_registry']: - env['resource_registry']['resources'] = {} - for hook_declaration in arg_hooks: - hook_path = [r for r in hook_declaration.split('/') if r] - resources = env['resource_registry']['resources'] - for nested_stack in hook_path: - if nested_stack not in resources: - resources[nested_stack] = {} - resources = resources[nested_stack] - else: - resources['hooks'] = hook + _poll_for_events(hc, args.name, 'CREATE', args.poll) @utils.arg('-e', '--environment-file', metavar='', @@ -477,7 +452,7 @@ def do_stack_update(hc, args): env_paths=args.environment_file) if args.pre_update: - hooks_to_env(env, args.pre_update, 'pre-update') + template_utils.hooks_to_env(env, args.pre_update, 'pre-update') fields = { 'stack_id': args.id, diff --git a/setup.cfg b/setup.cfg index be9a2a41..f7ebbb37 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ openstack.cli.extension = openstack.orchestration.v1 = stack_show = heatclient.osc.v1.stack:ShowStack stack_list = heatclient.osc.v1.stack:ListStack + stack_create = heatclient.osc.v1.stack:CreateStack [global] From f5c37cf0f7f2ae103c3d6379420db63d4281b02d Mon Sep 17 00:00:00 2001 From: Jason Dunsmore Date: Mon, 4 Jan 2016 16:50:42 -0600 Subject: [PATCH 100/553] OpenstackClient plugin for stack update This change implements the "openstack stack update" command. blueprint: heat-support-python-openstackclient Change-Id: I54cd9974674c65a1fb29604afaa13300f17c104d --- heatclient/common/template_utils.py | 8 +- heatclient/osc/v1/stack.py | 164 +++++++++++++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 174 +++++++++++++++++++++ setup.cfg | 1 + 4 files changed, 344 insertions(+), 3 deletions(-) diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index d6e366f8..aa4583c0 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -27,17 +27,19 @@ from heatclient.openstack.common._i18n import _ -def process_template_path(template_path, object_request=None): +def process_template_path(template_path, object_request=None, existing=False): """Read template from template path. Attempt to read template first as a file or url. If that is unsuccessful, try again to assuming path is to a template object. """ try: - return get_template_contents(template_file=template_path) + return get_template_contents(template_file=template_path, + existing=existing) except error.URLError: return get_template_contents(template_object=template_path, - object_request=object_request) + object_request=object_request, + existing=existing) def get_template_contents(template_file=None, template_url=None, diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 34f7ccbf..d7f905cf 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -163,6 +163,170 @@ def take_action(self, parsed_args): return _show_stack(client, stack['id'], format='table', short=True) +class UpdateStack(show.ShowOne): + """Update a stack.""" + + log = logging.getLogger(__name__ + '.UpdateStack') + + def get_parser(self, prog_name): + parser = super(UpdateStack, self).get_parser(prog_name) + parser.add_argument( + '-t', '--template', metavar='', + help=_('Path to the template') + ) + parser.add_argument( + '-e', '--environment', metavar='', + action='append', + help=_('Path to the environment. Can be specified multiple times') + ) + parser.add_argument( + '--pre-update', metavar='', action='append', + help=_('Name of a resource to set a pre-update hook to. Resources ' + 'in nested stacks can be set using slash as a separator: ' + 'nested_stack/another/my_resource. You can use wildcards ' + 'to match multiple stacks or resources: ' + 'nested_stack/an*/*_resource. This can be specified ' + 'multiple times') + ) + parser.add_argument( + '--timeout', metavar='', type=int, + help=_('Stack update timeout in minutes') + ) + parser.add_argument( + '--rollback', metavar='', + help=_('Set rollback on update failure. ' + 'Value "enabled" sets rollback to enabled. ' + 'Value "disabled" sets rollback to disabled. ' + 'Value "keep" uses the value of existing stack to be ' + 'updated (default)') + ) + parser.add_argument( + '--dry-run', action="store_true", + help=_('Do not actually perform the stack update, but show what ' + 'would be changed') + ) + parser.add_argument( + '--parameter', metavar='', + help=_('Parameter values used to create the stack. ' + 'This can be specified multiple times'), + action='append' + ) + parser.add_argument( + '--parameter-file', metavar='', + help=_('Parameter values from file used to create the stack. ' + 'This can be specified multiple times. Parameter value ' + 'would be the content of the file'), + action='append' + ) + parser.add_argument( + '--existing', action="store_true", + help=_('Re-use the template, parameters and environment of the ' + 'current stack. If the template argument is omitted then ' + 'the existing template is used. If no %(env_arg)s is ' + 'specified then the existing environment is used. ' + 'Parameters specified in %(arg)s will patch over the ' + 'existing values in the current stack. Parameters omitted ' + 'will keep the existing values') % { + 'arg': '--parameter', 'env_arg': '--environment'} + ) + parser.add_argument( + '--clear-parameter', metavar='', + help=_('Remove the parameters from the set of parameters of ' + 'current stack for the %(cmd)s. The default value in the ' + 'template will be used. This can be specified multiple ' + 'times') % {'cmd': 'stack-update'}, + action='append' + ) + parser.add_argument( + 'stack', metavar='', + help=_('Name or ID of stack to update') + ) + parser.add_argument( + '--tags', metavar='', + help=_('An updated list of tags to associate with the stack') + ) + parser.add_argument( + '--wait', + action='store_true', + help=_('Wait until stack goes to UPDATE_COMPLETE or ' + 'UPDATE_FAILED') + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + tpl_files, template = template_utils.process_template_path( + parsed_args.template, + object_request=_authenticated_fetcher(client), + existing=parsed_args.existing) + + env_files, env = ( + template_utils.process_multiple_environments_and_files( + env_paths=parsed_args.environment)) + + parameters = heat_utils.format_all_parameters( + parsed_args.parameter, + parsed_args.parameter_file, + parsed_args.template) + + if parsed_args.pre_update: + template_utils.hooks_to_env(env, parsed_args.pre_update, + 'pre-update') + + fields = { + 'stack_id': parsed_args.stack, + 'parameters': parameters, + 'existing': parsed_args.existing, + 'template': template, + 'files': dict(list(tpl_files.items()) + list(env_files.items())), + 'environment': env + } + + if parsed_args.tags: + fields['tags'] = parsed_args.tags + if parsed_args.timeout: + fields['timeout_mins'] = parsed_args.timeout + if parsed_args.clear_parameter: + fields['clear_parameters'] = list(parsed_args.clear_parameter) + + if parsed_args.rollback: + rollback = parsed_args.rollback.strip().lower() + if rollback not in ('enabled', 'disabled', 'keep'): + msg = _('--rollback invalid value: %s') % parsed_args.rollback + raise exc.CommandError(msg) + if rollback != 'keep': + fields['disable_rollback'] = rollback == 'disabled' + + if parsed_args.dry_run: + changes = client.stacks.preview_update(**fields) + + fields = ['state', 'resource_name', 'resource_type', + 'resource_identity'] + + columns = sorted(changes.get("resource_changes", {}).keys()) + data = [heat_utils.json_formatter(changes["resource_changes"][key]) + for key in columns] + + return columns, data + + client.stacks.update(**fields) + if parsed_args.wait: + if not utils.wait_for_status(client.stacks.get, parsed_args.stack, + status_field='stack_status', + success_status='update_complete', + error_status='update_failed'): + + msg = _('Stack %s failed to update.') % parsed_args.stack + raise exc.CommandError(msg) + + return _show_stack(client, parsed_args.stack, format='table', + short=True) + + class ShowStack(show.ShowOne): """Show stack details""" diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index bf2fa5f3..cdec6cd6 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -13,6 +13,7 @@ import copy import mock +import six import testscenarios from openstackclient.common import exceptions as exc @@ -137,6 +138,179 @@ def test_stack_create_wait_fail(self, mock_wait): self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) +class TestStackUpdate(TestStack): + + template_path = 'heatclient/tests/test_templates/empty.yaml' + + defaults = { + 'stack_id': 'my_stack', + 'environment': {}, + 'existing': False, + 'files': {}, + 'template': {'heat_template_version': '2013-05-23'}, + 'parameters': {}, + } + + def setUp(self): + super(TestStackUpdate, self).setUp() + self.cmd = stack.UpdateStack(self.app, None) + self.stack_client.update = mock.MagicMock( + return_value={'stack': {'id': '1234'}}) + self.stack_client.preview_update = mock.MagicMock( + return_value={'resource_changes': {'added': [], + 'deleted': [], + 'replaced': [], + 'unchanged': [], + 'updated': []}}) + self.stack_client.get = mock.MagicMock( + return_value={'stack_status': 'create_complete'}) + stack._authenticated_fetcher = mock.MagicMock() + + def test_stack_update_defaults(self): + arglist = ['my_stack', '-t', self.template_path] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.update.assert_called_with(**self.defaults) + + def test_stack_update_rollback_enabled(self): + arglist = ['my_stack', '-t', self.template_path, '--rollback', + 'enabled'] + kwargs = copy.deepcopy(self.defaults) + kwargs['disable_rollback'] = False + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.update.assert_called_with(**kwargs) + + def test_stack_update_rollback_disabled(self): + arglist = ['my_stack', '-t', self.template_path, '--rollback', + 'disabled'] + kwargs = copy.deepcopy(self.defaults) + kwargs['disable_rollback'] = True + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.update.assert_called_with(**kwargs) + + def test_stack_update_rollback_keep(self): + arglist = ['my_stack', '-t', self.template_path, '--rollback', + 'keep'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.assertNotIn('disable_rollback', self.defaults) + self.stack_client.update.assert_called_with(**self.defaults) + + def test_stack_update_rollback_invalid(self): + arglist = ['my_stack', '-t', self.template_path, '--rollback', 'foo'] + kwargs = copy.deepcopy(self.defaults) + kwargs['disable_rollback'] = False + parsed_args = self.check_parser(self.cmd, arglist, []) + + ex = self.assertRaises(exc.CommandError, self.cmd.take_action, + parsed_args) + self.assertEqual("--rollback invalid value: foo", six.text_type(ex)) + + def test_stack_update_parameters(self): + template_path = ('/'.join(self.template_path.split('/')[:-1]) + + '/parameters.yaml') + arglist = ['my_stack', '-t', template_path, '--parameter', 'p1=a', + '--parameter', 'p2=6'] + kwargs = copy.deepcopy(self.defaults) + kwargs['parameters'] = {'p1': 'a', 'p2': '6'} + kwargs['template']['parameters'] = {'p1': {'type': 'string'}, + 'p2': {'type': 'number'}} + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.update.assert_called_with(**kwargs) + + def test_stack_update_clear_parameters(self): + arglist = ['my_stack', '-t', self.template_path, '--clear-parameter', + 'a'] + kwargs = copy.deepcopy(self.defaults) + kwargs['clear_parameters'] = ['a'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.update.assert_called_with(**kwargs) + + def test_stack_update_tags(self): + arglist = ['my_stack', '-t', self.template_path, '--tags', 'tag1,tag2'] + kwargs = copy.deepcopy(self.defaults) + kwargs['tags'] = 'tag1,tag2' + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.update.assert_called_with(**kwargs) + + def test_stack_update_timeout(self): + arglist = ['my_stack', '-t', self.template_path, '--timeout', '60'] + kwargs = copy.deepcopy(self.defaults) + kwargs['timeout_mins'] = 60 + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.update.assert_called_with(**kwargs) + + def test_stack_update_pre_update(self): + arglist = ['my_stack', '-t', self.template_path, '--pre-update', 'a'] + kwargs = copy.deepcopy(self.defaults) + kwargs['environment'] = { + 'resource_registry': {'resources': {'a': {'hooks': 'pre-update'}}} + } + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.update.assert_called_with(**kwargs) + + def test_stack_update_existing(self): + arglist = ['my_stack', '-t', self.template_path, '--existing'] + kwargs = copy.deepcopy(self.defaults) + kwargs['existing'] = True + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.update.assert_called_with(**kwargs) + + def test_stack_update_dry_run(self): + arglist = ['my_stack', '-t', self.template_path, '--dry-run'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.preview_update.assert_called_with(**self.defaults) + self.stack_client.update.assert_not_called() + + def test_stack_update_wait(self): + arglist = ['my_stack', '-t', self.template_path, '--wait'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.update.assert_called_with(**self.defaults) + self.stack_client.get.assert_called_with(**{'stack_id': 'my_stack'}) + + @mock.patch('openstackclient.common.utils.wait_for_status', + return_value=False) + def test_stack_update_wait_fail(self, mock_wait): + arglist = ['my_stack', '-t', self.template_path, '--wait'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + class TestStackShow(TestStack): scenarios = [ diff --git a/setup.cfg b/setup.cfg index f7ebbb37..d11fa7ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ openstack.orchestration.v1 = stack_show = heatclient.osc.v1.stack:ShowStack stack_list = heatclient.osc.v1.stack:ListStack stack_create = heatclient.osc.v1.stack:CreateStack + stack_update = heatclient.osc.v1.stack:UpdateStack [global] From b00e12900a7df89a11429f05db72c0778a262d6e Mon Sep 17 00:00:00 2001 From: Jason Dunsmore Date: Thu, 14 Jan 2016 16:14:12 -0600 Subject: [PATCH 101/553] Improve "openstack stack create --wait" help message Change-Id: I5dae96c769c4540117626417fa71b3afca558e49 --- heatclient/osc/v1/stack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index d7f905cf..37366e1d 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -99,7 +99,7 @@ def get_parser(self, prog_name): parser.add_argument( '--wait', action='store_true', - help=_('Wait until stack completes') + help=_('Wait until stack goes to CREATE_COMPLETE or CREATE_FAILED') ) parser.add_argument( '--tags', From bc860bf7c62176c927602e969c1f55d4b72277b8 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 8 Jan 2016 16:48:34 -0500 Subject: [PATCH 102/553] Update template_utils to support object env's This patch updates several functions in template_utils.py so that they support loading environments from Swift containers (objects) in the same manner we currently load template files. The primary interfaces is process_multiple_environments_and_files which contains two new parameters: -env_path_is_object: a function to determine if the URL is an object -object_request: a function used to load the object Change-Id: If60a3ad44531bc6ca4f7a03a77f236173d3137ba --- heatclient/common/template_utils.py | 35 ++++++++++++++---- heatclient/tests/unit/test_template_utils.py | 39 ++++++++++++++++++++ 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index aa4583c0..a14f35cc 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -187,14 +187,19 @@ def deep_update(old, new): def process_multiple_environments_and_files(env_paths=None, template=None, - template_url=None): + template_url=None, + env_path_is_object=None, + object_request=None): + merged_files = {} merged_env = {} if env_paths: for env_path in env_paths: files, env = process_environment_and_files(env_path, template, - template_url) + template_url, + env_path_is_object, + object_request) # 'files' looks like {"filename1": contents, "filename2": contents} # so a simple update is enough for merging @@ -208,11 +213,24 @@ def process_multiple_environments_and_files(env_paths=None, template=None, def process_environment_and_files(env_path=None, template=None, - template_url=None): + template_url=None, env_path_is_object=None, + object_request=None): files = {} env = {} - if env_path: + is_object = env_path_is_object and env_path_is_object(env_path) + + if is_object: + raw_env = object_request and object_request('GET', env_path) + env = environment_format.parse(raw_env) + env_base_url = utils.base_url_for_url(env_path) + + resolve_environment_urls( + env.get('resource_registry'), + files, + env_base_url, is_object=True, object_request=object_request) + + elif env_path: env_url = utils.normalise_file_path_to_url(env_path) env_base_url = utils.base_url_for_url(env_url) raw_env = request.urlopen(env_url).read() @@ -226,7 +244,8 @@ def process_environment_and_files(env_path=None, template=None, return files, env -def resolve_environment_urls(resource_registry, files, env_base_url): +def resolve_environment_urls(resource_registry, files, env_base_url, + is_object=False, object_request=None): if resource_registry is None: return @@ -245,12 +264,14 @@ def ignore_if(key, value): if key == 'hooks': return True - get_file_contents(rr, files, base_url, ignore_if) + get_file_contents(rr, files, base_url, ignore_if, + is_object=is_object, object_request=object_request) for res_name, res_dict in six.iteritems(rr.get('resources', {})): res_base_url = res_dict.get('base_url', base_url) get_file_contents( - res_dict, files, res_base_url, ignore_if) + res_dict, files, res_base_url, ignore_if, + is_object=is_object, object_request=object_request) def hooks_to_env(env, arg_hooks, hook): diff --git a/heatclient/tests/unit/test_template_utils.py b/heatclient/tests/unit/test_template_utils.py index 29935480..f82aa891 100644 --- a/heatclient/tests/unit/test_template_utils.py +++ b/heatclient/tests/unit/test_template_utils.py @@ -313,6 +313,45 @@ def test_no_process_multiple_environments_and_files(self): self.assertEqual({}, env) self.assertEqual({}, files) + def test_process_multiple_environments_and_files_from_object(self): + + env_object = 'http://no.where/path/to/env.yaml' + env1 = b''' + parameters: + "param1": "value1" + resource_registry: + "OS::Thingy1": "b/a.yaml" + ''' + + self.m.ReplayAll() + + self.object_requested = False + + def env_path_is_object(object_url): + return True + + def object_request(method, object_url): + self.object_requested = True + self.assertEqual('GET', method) + self.assertTrue(object_url.startswith("http://no.where/path/to/")) + if object_url == env_object: + return env1 + else: + return self.template_a + + files, env = template_utils.process_multiple_environments_and_files( + env_paths=[env_object], env_path_is_object=env_path_is_object, + object_request=object_request) + self.assertEqual( + { + 'resource_registry': { + 'OS::Thingy1': 'http://no.where/path/to/b/a.yaml'}, + 'parameters': {'param1': 'value1'} + }, + env) + self.assertEqual(self.template_a.decode('utf-8'), + files['http://no.where/path/to/b/a.yaml']) + def test_global_files(self): url = 'file:///home/b/a.yaml' env = ''' From c7b606a060d2ad0631ffb3eef0f5c456b682e388 Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Tue, 15 Dec 2015 20:58:07 +0000 Subject: [PATCH 103/553] Add show_nested support to update --dry-run Currently update preview provides no way to show the updates for nested stack resources, so this adds a flag --show-nested, which is similar to other interfaces e.g template-validate. Closes-Bug: #1521971 Change-Id: I06f3b52d5d48dd5e6e266321e58ca8e6116d6017 --- heatclient/tests/unit/test_shell.py | 35 +++++++++++++++++++++++++---- heatclient/v1/shell.py | 5 +++++ heatclient/v1/stacks.py | 11 +++++---- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 735b0879..b4980925 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -2317,7 +2317,8 @@ def test_stack_update_with_tags(self): for r in required: self.assertRegexpMatches(update_text, r) - def _setup_stubs_update_dry_run(self, template_file, existing=False): + def _setup_stubs_update_dry_run(self, template_file, existing=False, + show_nested=False): self.register_keystone_auth_fixture() template_data = open(template_file).read() @@ -2365,13 +2366,17 @@ def _setup_stubs_update_dry_run(self, template_file, existing=False): else: method = 'PUT' + if show_nested: + path = '/stacks/teststack2/2/preview?show_nested=True' + else: + path = '/stacks/teststack2/2/preview' + if self.client == http.SessionClient: self.client.request( - '/stacks/teststack2/2/preview', method, - data=expected_data, headers={}).AndReturn(resp) + path, method, data=expected_data, headers={}).AndReturn(resp) else: http.HTTPClient.json_request( - method, '/stacks/teststack2/2/preview', + method, path, data=expected_data, headers={'X-Auth-Key': 'password', 'X-Auth-User': 'username'} ).AndReturn((resp, None)) @@ -2399,6 +2404,28 @@ def test_stack_update_dry_run(self): for r in required: self.assertRegexpMatches(update_preview_text, r) + def test_stack_update_dry_run_show_nested(self): + template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') + self._setup_stubs_update_dry_run(template_file, show_nested=True) + update_preview_text = self.shell( + 'stack-update teststack2/2 ' + '--template-file=%s ' + '--enable-rollback ' + '--show-nested ' + '--parameters="KeyPairName=updated_key" ' + '--dry-run ' % template_file) + + required = [ + 'stack_name', + 'id', + 'teststack2', + '2', + 'state', + 'replaced' + ] + for r in required: + self.assertRegexpMatches(update_preview_text, r) + def test_stack_update_dry_run_patch(self): template_file = os.path.join(TEST_VAR_DIR, 'minimal.template') self._setup_stubs_update_dry_run(template_file, existing=True) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 1fc6b4e1..3bcd4782 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -408,6 +408,8 @@ def do_stack_show(hc, args): @utils.arg('-y', '--dry-run', default=False, action="store_true", help='Do not actually perform the stack update, but show what ' 'would be changed') +@utils.arg('-n', '--show-nested', default=False, action="store_true", + help='Show nested stacks when performing --dry-run') @utils.arg('-P', '--parameters', metavar='', help=_('Parameter values used to create the stack. ' 'This can be specified multiple times, or once with ' @@ -487,6 +489,9 @@ def do_stack_update(hc, args): fields['disable_rollback'] = False if args.dry_run is True: + if args.show_nested: + fields['show_nested'] = args.show_nested + resource_changes = hc.stacks.preview_update(**fields) formatters = { diff --git a/heatclient/v1/stacks.py b/heatclient/v1/stacks.py index 5a4f9c59..f93086b0 100644 --- a/heatclient/v1/stacks.py +++ b/heatclient/v1/stacks.py @@ -177,14 +177,13 @@ def preview_update(self, stack_id, **kwargs): """Preview a stack update.""" stack_identifier = self._resolve_stack_id(stack_id) headers = self.client.credentials_headers() + path = '/stacks/%s/preview' % stack_identifier + if kwargs.pop('show_nested', False): + path += '?show_nested=True' if kwargs.pop('existing', None): - resp = self.client.patch('/stacks/%s/preview' % - stack_identifier, - data=kwargs, headers=headers) + resp = self.client.patch(path, data=kwargs, headers=headers) else: - resp = self.client.put('/stacks/%s/preview' % - stack_identifier, - data=kwargs, headers=headers) + resp = self.client.put(path, data=kwargs, headers=headers) body = utils.get_response_body(resp) return body From 39195c4b5dcb61e2d5d1d611860041c9239a315b Mon Sep 17 00:00:00 2001 From: neetu Date: Thu, 14 Jan 2016 10:09:41 -0600 Subject: [PATCH 104/553] Include keystone message when authentication failed This was lost in the fix for bug #1526944 but it might be useful. Change-Id: Ifa8a1b3c2ff2ce6ec79328fb77065eb216e0f538 Related-Bug: #1526944 --- heatclient/common/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/heatclient/common/http.py b/heatclient/common/http.py index d08bdef3..4b46c71d 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -209,7 +209,8 @@ def _http_request(self, url, method, **kwargs): if not ('X-Auth-Key' in kwargs['headers']) and ( resp.status_code == 401 or (resp.status_code == 500 and "(HTTP 401)" in resp.content)): - raise exc.HTTPUnauthorized("Authentication failed") + raise exc.HTTPUnauthorized(_("Authentication failed: %s") + % resp.content) elif 400 <= resp.status_code < 600: raise exc.from_response(resp) elif resp.status_code in (301, 302, 305): From 3c558d14852858ab927c465375fda5acd063ee5c Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Wed, 11 Nov 2015 19:20:00 +0000 Subject: [PATCH 105/553] Add --dry-run option to openstack stack create Implements the --dry-run flag for stack create, which calls the client manager's stack.preview method. Blueprint: heat-support-python-openstackclient Change-Id: I646f5fcdb7d70514ecfa4e4d27a59a8ef4283d76 --- heatclient/osc/v1/stack.py | 30 ++++++++++++++++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 11 ++++++++ 2 files changed, 41 insertions(+) diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 37366e1d..75978e6f 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -106,6 +106,12 @@ def get_parser(self, prog_name): metavar='', help=_('A list of tags to associate with the stack') ) + parser.add_argument( + '--dry-run', + action='store_true', + help=_('Do not actually perform the stack create, but show what ' + 'would be created') + ) parser.add_argument( 'name', metavar='', @@ -150,6 +156,30 @@ def take_action(self, parsed_args): if parsed_args.timeout: fields['timeout_mins'] = parsed_args.timeout + if parsed_args.dry_run: + stack = client.stacks.preview(**fields) + + formatters = { + 'description': heat_utils.text_wrap_formatter, + 'template_description': heat_utils.text_wrap_formatter, + 'stack_status_reason': heat_utils.text_wrap_formatter, + 'parameters': heat_utils.json_formatter, + 'outputs': heat_utils.json_formatter, + 'resources': heat_utils.json_formatter, + 'links': heat_utils.link_formatter, + } + + columns = [] + for key in stack.to_dict(): + columns.append(key) + columns.sort() + + return ( + columns, + utils.get_item_properties(stack, columns, + formatters=formatters) + ) + stack = client.stacks.create(**fields)['stack'] if parsed_args.wait: if not utils.wait_for_status(client.stacks.get, parsed_args.name, diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index cdec6cd6..b6fd99c4 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -53,6 +53,8 @@ def setUp(self): return_value={'stack': {'id': '1234'}}) self.stack_client.get = mock.MagicMock( return_value={'stack_status': 'create_complete'}) + self.stack_client.preview = mock.MagicMock( + return_value=stacks.Stack(None, {'stack': {'id', '1234'}})) stack._authenticated_fetcher = mock.MagicMock() def test_stack_create_defaults(self): @@ -137,6 +139,15 @@ def test_stack_create_wait_fail(self, mock_wait): self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + def test_stack_create_dry_run(self): + arglist = ['my_stack', '-t', self.template_path, '--dry-run'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.preview.assert_called_with(**self.defaults) + self.stack_client.create.assert_not_called() + class TestStackUpdate(TestStack): From e25b5bf54f71d50461b5f0ba3173c3d3b36124a1 Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Thu, 14 Jan 2016 12:58:02 -0600 Subject: [PATCH 106/553] Add openstack client stack snapshot list Based from the existing heat command: heat snapshot-list Change-Id: I9966641e200343bf19518487f8084d94008ac84d Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/snapshot.py | 58 +++++++++++++++++++ heatclient/tests/unit/osc/v1/test_snapshot.py | 54 +++++++++++++++++ setup.cfg | 1 + 3 files changed, 113 insertions(+) create mode 100644 heatclient/osc/v1/snapshot.py create mode 100644 heatclient/tests/unit/osc/v1/test_snapshot.py diff --git a/heatclient/osc/v1/snapshot.py b/heatclient/osc/v1/snapshot.py new file mode 100644 index 00000000..d75248dd --- /dev/null +++ b/heatclient/osc/v1/snapshot.py @@ -0,0 +1,58 @@ +# 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. +# + +"""Orchestration v1 Stack Snapshot implementations""" + +import logging + +from cliff import lister +from openstackclient.common import exceptions as exc +from openstackclient.common import utils + +from heatclient import exc as heat_exc +from openstackclient.i18n import _ + + +class ListSnapshot(lister.Lister): + """List stack snapshots""" + + log = logging.getLogger(__name__ + ".ListSnapshot") + + def get_parser(self, prog_name): + parser = super(ListSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack containing the snapshots') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + heat_client = self.app.client_manager.orchestration + return self._list_snapshot(heat_client, parsed_args) + + def _list_snapshot(self, heat_client, parsed_args): + fields = {'stack_id': parsed_args.stack} + try: + snapshots = heat_client.stacks.snapshot_list(**fields) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Stack not found: %s') % + parsed_args.stack) + + columns = ['id', 'name', 'status', 'status_reason', 'creation_time'] + return ( + columns, + (utils.get_dict_properties(s, columns) + for s in snapshots['snapshots']) + ) diff --git a/heatclient/tests/unit/osc/v1/test_snapshot.py b/heatclient/tests/unit/osc/v1/test_snapshot.py new file mode 100644 index 00000000..3c242002 --- /dev/null +++ b/heatclient/tests/unit/osc/v1/test_snapshot.py @@ -0,0 +1,54 @@ +# 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 mock + +from openstackclient.common import exceptions as exc + +from heatclient import exc as heat_exc +from heatclient.osc.v1 import snapshot +from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes + + +class TestStack(orchestration_fakes.TestOrchestrationv1): + def setUp(self): + super(TestStack, self).setUp() + self.mock_client = self.app.client_manager.orchestration + self.stack_client = self.app.client_manager.orchestration.stacks + + +class TestListSnapshot(TestStack): + def setUp(self): + super(TestListSnapshot, self).setUp() + self.cmd = snapshot.ListSnapshot(self.app, None) + self.stack_client.snapshot_list = mock.Mock( + return_value={'snapshots': []} + ) + + def test_snapshot_list(self): + arglist = ['my_stack'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.stack_client.snapshot_list.assert_called_with( + stack_id='my_stack') + + def test_snapshot_list_error(self): + self.stack_client.snapshot_list.side_effect = heat_exc.HTTPNotFound() + arglist = ['my_stack'] + 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)) diff --git a/setup.cfg b/setup.cfg index d11fa7ae..f450abcb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,7 @@ openstack.orchestration.v1 = stack_list = heatclient.osc.v1.stack:ListStack stack_create = heatclient.osc.v1.stack:CreateStack stack_update = heatclient.osc.v1.stack:UpdateStack + stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot [global] From a1f9d8bd8f4a4371945b63a2aba53d4b8a13cf56 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Wed, 20 Jan 2016 19:19:54 +0100 Subject: [PATCH 107/553] Remove argparse from requirements argparse was external in python 2.6 but not anymore, remove it from requirements. This should help with pip 8.0 that gets confused in this situation. Installation of the external argparse is not needed. Change-Id: Ib7e74912b36c1b5ccb514e31fac35efeff57378d --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 80f910f6..33711988 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,6 @@ Babel>=1.3 pbr>=1.6 cliff>=1.15.0 # Apache-2.0 -argparse iso8601>=0.1.9 PrettyTable<0.8,>=0.7 oslo.i18n>=1.5.0 # Apache-2.0 From ae4a7615d1d0c06ddb9dd9b4497fa209dbdf7fef Mon Sep 17 00:00:00 2001 From: Sergey Kraynev Date: Fri, 22 Jan 2016 02:37:13 -0500 Subject: [PATCH 108/553] Disable osc extension for 0.9.0 release In this patch osc extension is commented for releasing heatclient. This patch will be used for tagging as 0.9.0. Next patch in master should enable osc extension again. Change-Id: Iaf74c264fb9d92a9fce9d5d19c48b54b88a40ad7 --- setup.cfg | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index d11fa7ae..17445170 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,8 +26,11 @@ packages = console_scripts = heat = heatclient.shell:main -openstack.cli.extension = - orchestration = heatclient.osc.plugin +# NOTE(skraynev): This extension is commented for release 0.9.0 and +# will be enabled in future release, when full support for openstackclient +# be implemented. +#openstack.cli.extension = +# orchestration = heatclient.osc.plugin openstack.orchestration.v1 = stack_show = heatclient.osc.v1.stack:ShowStack From 7f35a553b897b48b9ecc235035980eaf62f94ab9 Mon Sep 17 00:00:00 2001 From: Sergey Kraynev Date: Fri, 22 Jan 2016 02:42:42 -0500 Subject: [PATCH 109/553] Enable osc extension in setup.cfg This patch removes changes from previous patch Iaf74c264fb9d92a9fce9d5d19c48b54b88a40ad7 . Now osc extension is enabled and can be used for continue development support of openstackclient. Change-Id: Iccff66327bc29075d85801d8b6fa418963fa5c88 --- setup.cfg | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 17445170..d11fa7ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,11 +26,8 @@ packages = console_scripts = heat = heatclient.shell:main -# NOTE(skraynev): This extension is commented for release 0.9.0 and -# will be enabled in future release, when full support for openstackclient -# be implemented. -#openstack.cli.extension = -# orchestration = heatclient.osc.plugin +openstack.cli.extension = + orchestration = heatclient.osc.plugin openstack.orchestration.v1 = stack_show = heatclient.osc.v1.stack:ShowStack From 6fbcb655619f5b985cb73f1710c3399c98975976 Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Thu, 21 Jan 2016 01:34:59 -0600 Subject: [PATCH 110/553] Add formatters for 'tags' in method _do_stack_show This change added json_formatter for 'tags' in method _do_stack_show. Change-Id: Icdd3c90d237804926657103dcc846f48f4355fde Closes-Bug: #1536492 --- heatclient/osc/v1/stack.py | 1 + heatclient/tests/unit/test_shell.py | 7 +++++-- heatclient/v1/shell.py | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 75978e6f..5cc5deab 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -419,6 +419,7 @@ def _show_stack(heat_client, stack_id, format='', short=False): formatters['parameters'] = complex_formatter formatters['outputs'] = complex_formatter formatters['links'] = complex_formatter + formatters['tags'] = complex_formatter return columns, utils.get_item_properties(data, columns, formatters=formatters) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 735b0879..356aebf5 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -754,7 +754,8 @@ def test_stack_show(self): "id": "1", "stack_name": "teststack", "stack_status": 'CREATE_COMPLETE', - "creation_time": "2012-10-25T01:58:47Z" + "creation_time": "2012-10-25T01:58:47Z", + "tags": [u'tag1', u'tag2'] }} resp = fakes.FakeHTTPResponse( 200, @@ -777,9 +778,11 @@ def test_stack_show(self): 'stack_name', 'stack_status', 'creation_time', + 'tags', 'teststack', 'CREATE_COMPLETE', - '2012-10-25T01:58:47Z' + '2012-10-25T01:58:47Z', + "['tag1', 'tag2']", ] for r in required: self.assertRegexpMatches(list_text, r) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 1fc6b4e1..b8c6e1e4 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -1575,7 +1575,8 @@ def _do_stack_show(hc, fields): 'stack_status_reason': utils.text_wrap_formatter, 'parameters': utils.json_formatter, 'outputs': utils.json_formatter, - 'links': utils.link_formatter + 'links': utils.link_formatter, + 'tags': utils.json_formatter } utils.print_dict(stack.to_dict(), formatters=formatters) From d6c3e09680404d3e0fc7bb0daca9160bcf0dc5b4 Mon Sep 17 00:00:00 2001 From: Rabi Mishra Date: Mon, 25 Jan 2016 17:48:02 +0530 Subject: [PATCH 111/553] Ignore restricted_actions key Ignore restricted_actions key in resolve_environment_urls. Change-Id: Ia29441751be7a7f0752ce73c71d34e34b62c9d5a Blueprint: stack-update-restrict --- heatclient/common/template_utils.py | 2 +- heatclient/tests/unit/test_template_utils.py | 22 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index a14f35cc..b80878d2 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -261,7 +261,7 @@ def ignore_if(key, value): # Built in providers like: "X::Compute::Server" # don't need downloading. return True - if key == 'hooks': + if key in ['hooks', 'restricted_actions']: return True get_file_contents(rr, files, base_url, ignore_if, diff --git a/heatclient/tests/unit/test_template_utils.py b/heatclient/tests/unit/test_template_utils.py index f82aa891..bcf50501 100644 --- a/heatclient/tests/unit/test_template_utils.py +++ b/heatclient/tests/unit/test_template_utils.py @@ -52,6 +52,28 @@ def collect_links(self, env, content, url, env_base_url=''): if url: self.assertEqual(content.decode('utf-8'), files[url]) + def test_ignore_env_keys(self): + self.m.StubOutWithMock(request, 'urlopen') + env_file = '/home/my/dir/env.yaml' + env = b''' + resource_registry: + resources: + bar: + hooks: pre_create + restricted_actions: replace + ''' + request.urlopen('file://%s' % env_file).AndReturn( + six.BytesIO(env)) + self.m.ReplayAll() + _, env_dict = template_utils.process_environment_and_files( + env_file) + self.assertEqual( + {u'resource_registry': {u'resources': { + u'bar': {u'hooks': u'pre_create', + u'restricted_actions': u'replace'}}}}, + env_dict) + self.m.VerifyAll() + def test_process_environment_file(self): self.m.StubOutWithMock(request, 'urlopen') From a65d4c5378499be197bd9edea4b7811f964bd473 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 25 Jan 2016 19:02:55 +0000 Subject: [PATCH 112/553] Updated from global requirements Change-Id: I39b0b5673b88143484b6e672857cd8ff4c76a485 --- requirements.txt | 24 ++++++++++++------------ test-requirements.txt | 20 ++++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/requirements.txt b/requirements.txt index 33711988..6d72672d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,17 +2,17 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -Babel>=1.3 -pbr>=1.6 +Babel>=1.3 # BSD +pbr>=1.6 # Apache-2.0 cliff>=1.15.0 # Apache-2.0 -iso8601>=0.1.9 -PrettyTable<0.8,>=0.7 -oslo.i18n>=1.5.0 # Apache-2.0 +iso8601>=0.1.9 # MIT +PrettyTable<0.8,>=0.7 # BSD +oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.2.0 # Apache-2.0 -python-keystoneclient!=1.8.0,>=1.6.0 -python-swiftclient>=2.2.0 -python-openstackclient>=2.0.0 -PyYAML>=3.1.0 -requests!=2.9.0,>=2.8.1 -six>=1.9.0 +oslo.utils>=3.4.0 # Apache-2.0 +python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 +python-swiftclient>=2.2.0 # Apache-2.0 +python-openstackclient>=2.0.0 # Apache-2.0 +PyYAML>=3.1.0 # MIT +requests!=2.9.0,>=2.8.1 # Apache-2.0 +six>=1.9.0 # MIT diff --git a/test-requirements.txt b/test-requirements.txt index 49501063..5112cddd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,16 +4,16 @@ # Hacking already pins down pep8, pyflakes and flake8 hacking<0.11,>=0.10.0 -coverage>=3.6 -discover -fixtures>=1.3.1 +coverage>=3.6 # Apache-2.0 +discover # BSD +fixtures>=1.3.1 # Apache-2.0/BSD requests-mock>=0.7.0 # Apache-2.0 -mock>=1.2 -mox3>=0.7.0 +mock>=1.2 # BSD +mox3>=0.7.0 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 -sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -tempest-lib>=0.13.0 -testrepository>=0.0.18 -testscenarios>=0.4 -testtools>=1.4.0 +sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD +tempest-lib>=0.13.0 # Apache-2.0 +testrepository>=0.0.18 # Apache-2.0/BSD +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=1.4.0 # MIT From 6acd25e5d5085422ebbc325844f585f416941b6b Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Thu, 28 Jan 2016 13:45:41 +0800 Subject: [PATCH 113/553] Raise CommandError when Any of stack/deployment/config delete failed For heat command: heat stack-delete,config-delete and heat deployment-delete, they will raise CommandError when *All* of them end up failed. It is a strange behavior,this change is to raise CommandError when *Any* of them failed. Change-Id: I57b5efc9018e3f1f07858ecbf1ef9ffba21bf1aa Closes-Bug: #1538862 --- heatclient/tests/unit/test_shell.py | 44 ++++++++++++++++++++++------- heatclient/v1/shell.py | 35 +++++++++++++---------- 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 735b0879..66cdbf49 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -2458,29 +2458,47 @@ def test_stack_delete_multiple(self): None) if self.client == http.SessionClient: self.client.request( - '/stacks/teststack1/1', 'DELETE').AndReturn(resp) + '/stacks/teststack/1', 'DELETE').AndReturn(resp) self.client.request( '/stacks/teststack2/2', 'DELETE').AndReturn(resp) else: self.client.raw_request( - 'DELETE', '/stacks/teststack1/1').AndReturn((resp, None)) + '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 teststack1/1 teststack2/2') + delete_text = self.shell('stack-delete teststack/1 teststack2/2') required = [ 'stack_name', 'id', 'teststack', - '1' + '1', + 'teststack2', + '2' ] for r in required: self.assertRegexpMatches(delete_text, r) + def test_stack_delete_failed(self): + self.register_keystone_auth_fixture() + + if self.client == http.SessionClient: + self.client.request( + '/stacks/teststack1/1', 'DELETE').AndRaise(exc.HTTPNotFound()) + else: + http.HTTPClient.raw_request( + 'DELETE', + '/stacks/teststack1/1').AndRaise(exc.HTTPNotFound()) + self.m.ReplayAll() + error = self.assertRaises( + exc.CommandError, self.shell, 'stack-delete teststack1/1') + self.assertIn('Unable to delete 1 of the 1 stacks.', + str(error)) + def test_build_info(self): self.register_keystone_auth_fixture() resp_dict = { @@ -4206,8 +4224,11 @@ def test_config_delete(self): self.m.ReplayAll() self.assertEqual('', self.shell('config-delete abcd qwer')) - self.assertRaises( + + error = self.assertRaises( exc.CommandError, self.shell, 'config-delete abcd qwer') + self.assertIn('Unable to delete 2 of the 2 configs.', + str(error)) class ShellTestDeployment(ShellBase): @@ -4527,11 +4548,14 @@ def _delete_request_success(id): self.m.ReplayAll() - self.assertRaises(exc.CommandError, self.shell, - 'deployment-delete defg qwer') - self.assertRaises(exc.CommandError, self.shell, - 'deployment-delete defg qwer') - + error = self.assertRaises( + exc.CommandError, self.shell, 'deployment-delete defg qwer') + self.assertIn('Unable to delete 2 of the 2 deployments.', + str(error)) + error2 = self.assertRaises( + exc.CommandError, self.shell, 'deployment-delete defg qwer') + self.assertIn('Unable to delete 2 of the 2 deployments.', + str(error2)) output = self.shell('deployment-delete defg qwer') self.assertRegexpMatches(output, 'Failed to delete the correlative ' 'config dummy_config_id of ' diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 1fc6b4e1..b12b0eb9 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -284,9 +284,11 @@ def do_stack_delete(hc, args): except exc.HTTPNotFound as e: failure_count += 1 print(e) - if failure_count == len(args.id): - raise exc.CommandError(_("Unable to delete any of the specified " - "stacks.")) + if failure_count: + raise exc.CommandError(_("Unable to delete %(count)d of the %(total)d " + "stacks.") % + {'count': failure_count, + 'total': len(args.id)}) do_stack_list(hc) @@ -1266,20 +1268,22 @@ def do_config_show(hc, args): @utils.arg('id', metavar='', nargs='+', - help=_('IDs of the configurations to delete.')) + help=_('ID of the configuration(s) to delete.')) def do_config_delete(hc, args): - '''Delete a software configuration.''' + '''Delete the software configuration(s).''' failure_count = 0 for config_id in args.id: try: hc.software_configs.delete(config_id=config_id) - except exc.HTTPNotFound as e: + except exc.HTTPNotFound: failure_count += 1 - print(e) - if failure_count == len(args.id): - raise exc.CommandError(_("Unable to delete any of the specified " - "configs.")) + print(_('Software config with ID %s not found') % config_id) + if failure_count: + raise exc.CommandError(_("Unable to delete %(count)d of the %(total)d " + "configs.") % + {'count': failure_count, + 'total': len(args.id)}) @utils.arg('-i', '--input-value', metavar='', @@ -1377,9 +1381,9 @@ def do_deployment_metadata_show(hc, args): @utils.arg('id', metavar='', nargs='+', - help=_('IDs of the deployments to delete.')) + help=_('ID of the deployment(s) to delete.')) def do_deployment_delete(hc, args): - '''Delete software deployments.''' + '''Delete the software deployment(s).''' failure_count = 0 for deploy_id in args.id: @@ -1401,9 +1405,10 @@ def do_deployment_delete(hc, args): '%(config_id)s of deployment %(deploy_id)s') % {'config_id': config_id, 'deploy_id': deploy_id}) - if failure_count == len(args.id): - raise exc.CommandError(_("Unable to delete any of the specified " - "deployments.")) + if failure_count: + raise exc.CommandError(_("Unable to delete %(count)d of the %(total)d " + "deployments.") % + {'count': failure_count, 'total': len(args.id)}) @utils.arg('id', metavar='', From f5beab9f8a58ed3891e882cbc648ddc33b16d6bf Mon Sep 17 00:00:00 2001 From: Oleg Khavroshin Date: Fri, 15 Jan 2016 17:55:01 +0300 Subject: [PATCH 114/553] Edit backslashes for lines continuations It is preferred to wrap long lines in parentheses and not a backslash for line continuation due to OpenStack Style Guidelines http://docs.openstack.org/developer/hacking/#general, paragraph 2 Change-Id: I83cd72a06bcee750876fb9e127959b16107e2f79 --- heatclient/common/utils.py | 9 +++++---- heatclient/tests/unit/test_template_utils.py | 4 ++-- heatclient/tests/unit/test_utils.py | 12 ++++++------ heatclient/tests/unit/v1/test_hooks.py | 15 +++++++-------- heatclient/v1/client.py | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/heatclient/common/utils.py b/heatclient/common/utils.py index 53a84c03..a36ab38c 100644 --- a/heatclient/common/utils.py +++ b/heatclient/common/utils.py @@ -161,12 +161,13 @@ def find_resource(manager, name_or_id): try: return manager.find(name=name_or_id) except exc.NotFound: - msg = _("No %(name)s with a name or ID of " - "'%(name_or_id)s' exists.") % \ - { + msg = ( + _("No %(name)s with a name or ID of " + "'%(name_or_id)s' exists.") + % { 'name': manager.resource_class.__name__.lower(), 'name_or_id': name_or_id - } + }) raise exc.CommandError(msg) diff --git a/heatclient/tests/unit/test_template_utils.py b/heatclient/tests/unit/test_template_utils.py index 29935480..4be15186 100644 --- a/heatclient/tests/unit/test_template_utils.py +++ b/heatclient/tests/unit/test_template_utils.py @@ -405,8 +405,8 @@ def setUp(self): def test_get_template_contents_file(self): with tempfile.NamedTemporaryFile() as tmpl_file: - tmpl = b'{"AWSTemplateFormatVersion" : "2010-09-09",' \ - b' "foo": "bar"}' + tmpl = (b'{"AWSTemplateFormatVersion" : "2010-09-09",' + b' "foo": "bar"}') tmpl_file.write(tmpl) tmpl_file.flush() diff --git a/heatclient/tests/unit/test_utils.py b/heatclient/tests/unit/test_utils.py index 96605a52..8237b4fc 100644 --- a/heatclient/tests/unit/test_utils.py +++ b/heatclient/tests/unit/test_utils.py @@ -198,8 +198,8 @@ def test_format_parameter_file(self): tmpl_file = '/opt/stack/template.yaml' contents = 'DBUsername=wp\nDBPassword=verybadpassword' utils.read_url_content = mock.MagicMock() - utils.read_url_content.return_value = 'DBUsername=wp\n' \ - 'DBPassword=verybadpassword' + utils.read_url_content.return_value = ('DBUsername=wp\n' + 'DBPassword=verybadpassword') p = utils.format_parameter_file([ 'env_file1=test_file1'], tmpl_file) @@ -210,8 +210,8 @@ def test_format_parameter_file_no_template(self): tmpl_file = None contents = 'DBUsername=wp\nDBPassword=verybadpassword' utils.read_url_content = mock.MagicMock() - utils.read_url_content.return_value = 'DBUsername=wp\n' \ - 'DBPassword=verybadpassword' + utils.read_url_content.return_value = ('DBUsername=wp\n' + 'DBPassword=verybadpassword') p = utils.format_parameter_file([ 'env_file1=test_file1'], tmpl_file) self.assertEqual({'env_file1': contents @@ -222,8 +222,8 @@ def test_format_all_parameters(self): contents = 'DBUsername=wp\nDBPassword=verybadpassword' params = ['KeyName=heat_key;UpstreamDNS=8.8.8.8'] utils.read_url_content = mock.MagicMock() - utils.read_url_content.return_value = 'DBUsername=wp\n' \ - 'DBPassword=verybadpassword' + utils.read_url_content.return_value = ('DBUsername=wp\n' + 'DBPassword=verybadpassword') p = utils.format_all_parameters(params, [ 'env_file1=test_file1'], template_file=tmpl_file) self.assertEqual({'KeyName': 'heat_key', diff --git a/heatclient/tests/unit/v1/test_hooks.py b/heatclient/tests/unit/v1/test_hooks.py index 7fc0de2e..095af908 100644 --- a/heatclient/tests/unit/v1/test_hooks.py +++ b/heatclient/tests/unit/v1/test_hooks.py @@ -32,8 +32,8 @@ def setUp(self): type(self.args).id = stack_name_p shell.template_utils.get_template_contents = mock.Mock( return_value=({}, "")) - shell.template_utils.process_multiple_environments_and_files = \ - mock.Mock(return_value=({}, {})) + shell.template_utils.process_multiple_environments_and_files = ( + mock.Mock(return_value=({}, {}))) shell.utils.format_all_parameters = mock.Mock(return_value=[]) shell.do_stack_list = mock.Mock() shell.logger = mock.Mock() @@ -100,8 +100,8 @@ def test_create_hooks_in_env_and_args(self): } } } - shell.template_utils.process_multiple_environments_and_files = \ - mock.Mock(return_value=({}, env)) + shell.template_utils.process_multiple_environments_and_files = ( + mock.Mock(return_value=({}, env))) shell.do_stack_create(self.client, self.args) self.assertEqual(1, self.client.stacks.create.call_count) @@ -188,8 +188,8 @@ def test_update_hooks_in_env_and_args(self): } } } - shell.template_utils.process_multiple_environments_and_files = \ - mock.Mock(return_value=({}, env)) + shell.template_utils.process_multiple_environments_and_files = ( + mock.Mock(return_value=({}, env))) shell.do_stack_update(self.client, self.args) self.assertEqual(1, self.client.stacks.update.call_count) @@ -220,8 +220,7 @@ def test_update_hooks_in_env_and_args(self): self.assertEqual(expected_hooks, actual_hooks) def test_clear_all_hooks(self): - shell._get_hook_type_via_status =\ - mock.Mock(return_value='pre-create') + shell._get_hook_type_via_status = mock.Mock(return_value='pre-create') type(self.args).hook = mock.PropertyMock( return_value=['bp']) type(self.args).pre_create = mock.PropertyMock(return_value=True) diff --git a/heatclient/v1/client.py b/heatclient/v1/client.py index 09a5eb87..3d3c1e9e 100644 --- a/heatclient/v1/client.py +++ b/heatclient/v1/client.py @@ -46,9 +46,9 @@ def __init__(self, *args, **kwargs): self.events = events.EventManager(self.http_client) self.actions = actions.ActionManager(self.http_client) self.build_info = build_info.BuildInfoManager(self.http_client) - self.software_deployments = \ + self.software_deployments = ( software_deployments.SoftwareDeploymentManager( - self.http_client) + self.http_client)) self.software_configs = software_configs.SoftwareConfigManager( self.http_client) self.services = services.ServiceManager(self.http_client) From 00ba1ec9b4286669f9abad436b4b8bb9ee8bb120 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Thu, 28 Jan 2016 19:13:49 +0100 Subject: [PATCH 115/553] Update translation setup Follow new infra setup for translations, see spec http://specs.openstack.org/openstack-infra/infra-specs/specs/translation_setup.html for full details. This basically renames python-heatclient/locale/python-heatclient.pot to heatclient/locale/heatclient.pot. For this we need to update setup.cfg. The domain name is already correct in i18n.py. The project has no translations currently, let's remove the outdated pot file, the updated scripts work without them. So, we can just delete the file and once there are translations, an updated pot file together with translations can be imported automatically. Remove test for valid translation files in tox.ini since there are now no more pot files - and the test fails if there are no files at all. Change-Id: I6f5e77b9cd633f6edb503ff225ca0b507153946c --- python-heatclient/locale/python-heatclient.pot | 0 setup.cfg | 12 ++++++------ tox.ini | 3 --- 3 files changed, 6 insertions(+), 9 deletions(-) delete mode 100644 python-heatclient/locale/python-heatclient.pot diff --git a/python-heatclient/locale/python-heatclient.pot b/python-heatclient/locale/python-heatclient.pot deleted file mode 100644 index e69de29b..00000000 diff --git a/setup.cfg b/setup.cfg index d11fa7ae..9ca65449 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,13 +54,13 @@ universal = 1 [extract_messages] keywords = _ gettext ngettext l_ lazy_gettext mapping_file = babel.cfg -output_file = python-heatclient/locale/python-heatclient.pot +output_file = heatclient/locale/heatclient.pot [compile_catalog] -directory = python-heatclient/locale -domain = python-heatclient +directory = heatclient/locale +domain = heatclient [update_catalog] -domain = python-heatclient -output_dir = python-heatclient/locale -input_file = python-heatclient/locale/python-heatclient.pot +domain = heatclient +output_dir = heatclient/locale +input_file = heatclient/locale/heatclient.pot diff --git a/tox.ini b/tox.ini index 18fb577a..47371be0 100644 --- a/tox.ini +++ b/tox.ini @@ -32,9 +32,6 @@ commands = oslo_debug_helper -t heatclient/tests {posargs} [testenv:pep8] commands = flake8 - # Check that .po and .pot files are valid: - bash -c "find python-heatclient -type f -regex '.*\.pot?' -print0|xargs -0 -n 1 msgfmt --check-format -o /dev/null" -whitelist_externals = bash [testenv:venv] commands = {posargs} From 3298ad98c88309c0aab972284b2a403573224c94 Mon Sep 17 00:00:00 2001 From: Bo Wang Date: Mon, 1 Feb 2016 16:32:12 +0800 Subject: [PATCH 116/553] Remove incorrectly used "# flake8: noqa" "# flake8: noqa" option disables all checks for the whole file. To disable one line we should use "# noqa". Remove unused "# flake8: noqa" and fix hidden hacking errors. Change-Id: I624e96784ae7a22b5a5766f38bdf1fb8ade2d0a2 Closes-Bug: #1540254 --- tools/install_venv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/install_venv.py b/tools/install_venv.py index cc218437..1dab63e8 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -22,7 +22,7 @@ import os import sys -import install_venv_common as install_venv # flake8: noqa +import install_venv_common as install_venv def print_help(project, venv, root): @@ -42,7 +42,7 @@ def print_help(project, venv, root): $ %(root)s/tools/with_venv.sh """ - print help % dict(project=project, venv=venv, root=root) + print (help % dict(project=project, venv=venv, root=root)) def main(argv): From bb866d4d7505a7323662c238fb3f855ee04f99dc Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Fri, 29 Jan 2016 15:52:59 -0600 Subject: [PATCH 117/553] Add openstack client stack basic funtion tests Based from the existing heat function tests. Can be expanded in the future with more then read-only tests. For now just get the tests working with openstack client. Change-Id: I1b84085b89c429b4b25f75d76d5c4b7b56d25326 Blueprint: heat-support-python-openstackclient --- heatclient/tests/functional/base.py | 3 ++ heatclient/tests/functional/osc/__init__.py | 0 .../tests/functional/osc/v1/__init__.py | 0 .../tests/functional/osc/v1/test_readonly.py | 40 +++++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 heatclient/tests/functional/osc/__init__.py create mode 100644 heatclient/tests/functional/osc/v1/__init__.py create mode 100644 heatclient/tests/functional/osc/v1/test_readonly.py diff --git a/heatclient/tests/functional/base.py b/heatclient/tests/functional/base.py index 44718cad..b0d8a8ed 100644 --- a/heatclient/tests/functional/base.py +++ b/heatclient/tests/functional/base.py @@ -40,3 +40,6 @@ 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/__init__.py b/heatclient/tests/functional/osc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/heatclient/tests/functional/osc/v1/__init__.py b/heatclient/tests/functional/osc/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/heatclient/tests/functional/osc/v1/test_readonly.py b/heatclient/tests/functional/osc/v1/test_readonly.py new file mode 100644 index 00000000..3c70a47d --- /dev/null +++ b/heatclient/tests/functional/osc/v1/test_readonly.py @@ -0,0 +1,40 @@ +# 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 tempest_lib import exceptions + +from heatclient.tests.functional import base + + +class SimpleReadOnlyOpenStackClientTest(base.ClientTestBase): + """Basic, read-only tests for Openstack CLI client heat plugin. + + Basic smoke test for the openstack CLI commands which do not require + creating or modifying stacks. + """ + + def test_openstack_fake_action(self): + self.assertRaises(exceptions.CommandFailed, + self.openstack, + 'this-does-not-exist') + + def test_openstack_stack_list(self): + self.openstack('stack list') + + def test_openstack_stack_list_debug(self): + self.openstack('stack list', flags='--debug') + + def test_openstack_help_cmd(self): + self.openstack('help stack') + + def test_openstack_version(self): + self.openstack('', flags='--version') From 26774cfc72d53ecdd9940ecc382d8314b4124e2a Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Wed, 25 Nov 2015 05:53:58 -0600 Subject: [PATCH 118/553] OpenstackClient plugin for software deployment delete This change implements the "openstack software deployment delete" command Based from the existing heat command: heat deployment-delete Change-Id: I0288386117b7efa7cd19206264fbcfebafd97480 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/software_deployment.py | 71 ++++++++++++++++++ .../unit/osc/v1/test_software_deployment.py | 74 +++++++++++++++++++ setup.cfg | 1 + 3 files changed, 146 insertions(+) create mode 100644 heatclient/osc/v1/software_deployment.py create mode 100644 heatclient/tests/unit/osc/v1/test_software_deployment.py diff --git a/heatclient/osc/v1/software_deployment.py b/heatclient/osc/v1/software_deployment.py new file mode 100644 index 00000000..c8183ccf --- /dev/null +++ b/heatclient/osc/v1/software_deployment.py @@ -0,0 +1,71 @@ +# 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. +# + +"""Orchestration v1 Software Deployment action implementations""" + +import logging + +from cliff import command +from openstackclient.common import exceptions as exc + +from heatclient import exc as heat_exc +from heatclient.openstack.common._i18n import _ + + +class DeleteDeployment(command.Command): + """Delete software deployment(s) and correlative config(s).""" + + log = logging.getLogger(__name__ + '.DeleteDeployment') + + def get_parser(self, prog_name): + parser = super(DeleteDeployment, self).get_parser(prog_name) + parser.add_argument( + 'id', + metavar='', + nargs='+', + help=_('ID of the deployment(s) to delete.') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + hc = self.app.client_manager.orchestration + failure_count = 0 + + for deploy_id in parsed_args.id: + try: + sd = hc.software_deployments.get(deployment_id=deploy_id) + hc.software_deployments.delete( + deployment_id=deploy_id) + except Exception as e: + if isinstance(e, heat_exc.HTTPNotFound): + print(_('Deployment with ID %s not found') % deploy_id) + else: + print(_('Deployment with ID %s failed to delete') + % deploy_id) + failure_count += 1 + continue + # just try best to delete the corresponding config + try: + config_id = getattr(sd, 'config_id') + hc.software_configs.delete(config_id=config_id) + except Exception: + print(_('Failed to delete the correlative config' + ' %(config_id)s of deployment %(deploy_id)s') % + {'config_id': config_id, 'deploy_id': deploy_id}) + + if failure_count: + raise exc.CommandError(_('Unable to delete %(count)s of the ' + '%(total)s deployments.') % + {'count': failure_count, + 'total': len(parsed_args.id)}) diff --git a/heatclient/tests/unit/osc/v1/test_software_deployment.py b/heatclient/tests/unit/osc/v1/test_software_deployment.py new file mode 100644 index 00000000..c4e9d31a --- /dev/null +++ b/heatclient/tests/unit/osc/v1/test_software_deployment.py @@ -0,0 +1,74 @@ +# 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 mock + +from openstackclient.common import exceptions as exc + +from heatclient import exc as heat_exc +from heatclient.osc.v1 import software_deployment +from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes + + +class TestDeployment(orchestration_fakes.TestOrchestrationv1): + def setUp(self): + super(TestDeployment, self).setUp() + sd_client = self.app.client_manager.orchestration.software_deployments + self.mock_client = sd_client + sc_client = self.app.client_manager.orchestration.software_configs + self.mock_config_client = sc_client + + +class TestDeploymentDelete(TestDeployment): + + def setUp(self): + super(TestDeploymentDelete, self).setUp() + self.cmd = software_deployment.DeleteDeployment(self.app, None) + + def test_deployment_delete_success(self): + arglist = ['test_deployment'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.get = mock.Mock() + self.mock_client.delete = mock.Mock() + self.cmd.take_action(parsed_args) + self.mock_client.delete.assert_called_with( + deployment_id='test_deployment') + + def test_deployment_delete_multiple(self): + arglist = ['test_deployment', 'test_deployment2'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.get = mock.Mock() + self.mock_client.delete = mock.Mock() + self.cmd.take_action(parsed_args) + self.mock_client.delete.assert_has_calls( + [mock.call(deployment_id='test_deployment'), + mock.call(deployment_id='test_deployment2')]) + + def test_deployment_delete_not_found(self): + arglist = ['test_deployment', 'test_deployment2'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.delete = mock.Mock() + self.mock_client.delete.side_effect = heat_exc.HTTPNotFound() + error = self.assertRaises( + exc.CommandError, self.cmd.take_action, parsed_args) + self.assertIn("Unable to delete 2 of the 2 deployments.", str(error)) + + def test_deployment_config_delete_failed(self): + arglist = ['test_deployment'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_config_client.delete = mock.Mock() + self.mock_config_client.delete.side_effect = heat_exc.HTTPNotFound() + error = self.assertRaises( + exc.CommandError, self.cmd.take_action, parsed_args) + self.assertEqual("Unable to delete 1 of the 1 deployments.", + str(error)) diff --git a/setup.cfg b/setup.cfg index 1d034e45..85c7d391 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ openstack.cli.extension = orchestration = heatclient.osc.plugin openstack.orchestration.v1 = + software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment stack_show = heatclient.osc.v1.stack:ShowStack stack_list = heatclient.osc.v1.stack:ListStack stack_create = heatclient.osc.v1.stack:CreateStack From f225c4e956f5d27b7d9207ac5a31e8efacf4e20f Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Wed, 3 Feb 2016 14:30:02 +1300 Subject: [PATCH 119/553] OpenstackClient plugin for stack adopt This change implements the "openstack stack adopt" command. Blueprint: heat-support-python-openstackclient Change-Id: Id2e74970937c04f095b4f14be047cff6e2bf3891 --- heatclient/osc/v1/stack.py | 86 ++++++++++++++++++++++ heatclient/tests/test_templates/adopt.json | 32 ++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 65 ++++++++++++++++ setup.cfg | 1 + 4 files changed, 184 insertions(+) create mode 100644 heatclient/tests/test_templates/adopt.json diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 5cc5deab..5d8ed2a6 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -20,6 +20,7 @@ from openstackclient.common import exceptions as exc from openstackclient.common import parseractions from openstackclient.common import utils +from six.moves.urllib import request from heatclient.common import http from heatclient.common import template_utils @@ -566,3 +567,88 @@ def _list(client, args=None): columns, (utils.get_item_properties(s, columns) for s in data) ) + + +class AdoptStack(show.ShowOne): + """Adopt a stack.""" + + log = logging.getLogger(__name__ + '.AdoptStack') + + def get_parser(self, prog_name): + parser = super(AdoptStack, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the stack to adopt') + ) + parser.add_argument( + '-e', '--environment', + metavar='', + action='append', + help=_('Path to the environment. Can be specified multiple times') + ) + parser.add_argument( + '--timeout', + metavar='', + type=int, + help=_('Stack creation timeout in minutes') + ) + parser.add_argument( + '--adopt-file', + metavar='', + required=True, + help=_('Path to adopt stack data file') + ) + parser.add_argument( + '--enable-rollback', + action='store_true', + help=_('Enable rollback on create/update failure') + ) + parser.add_argument( + '--parameter', + metavar='', + action='append', + help=_('Parameter values used to create the stack. Can be ' + 'specified multiple times') + ) + parser.add_argument( + '--wait', + action='store_true', + help=_('Wait until stack adopt completes') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + env_files, env = ( + template_utils.process_multiple_environments_and_files( + env_paths=parsed_args.environment)) + + adopt_url = heat_utils.normalise_file_path_to_url( + parsed_args.adopt_file) + adopt_data = request.urlopen(adopt_url).read().decode('utf-8') + + fields = { + 'stack_name': parsed_args.name, + 'disable_rollback': not parsed_args.enable_rollback, + 'adopt_stack_data': adopt_data, + 'parameters': heat_utils.format_parameters(parsed_args.parameter), + 'files': dict(list(env_files.items())), + 'environment': env, + 'timeout': parsed_args.timeout + } + + stack = client.stacks.create(**fields)['stack'] + + if parsed_args.wait: + if not utils.wait_for_status(client.stacks.get, parsed_args.name, + status_field='stack_status', + success_status='create_complete', + error_status=['create_failed']): + msg = _('Stack %s failed to create.') % parsed_args.name + raise exc.CommandError(msg) + + return _show_stack(client, stack['id'], format='table', short=True) diff --git a/heatclient/tests/test_templates/adopt.json b/heatclient/tests/test_templates/adopt.json new file mode 100644 index 00000000..ac57ec76 --- /dev/null +++ b/heatclient/tests/test_templates/adopt.json @@ -0,0 +1,32 @@ +{ + 'files': {}, + 'status': 'COMPLETE', + 'name': 'my_stack', + 'tags': None, + 'stack_user_project_id': '123456', + 'environment': {}, + 'template': { + 'heat_template_version': '2016-04-08', + 'resources': { + 'thing': { + 'type': 'OS::Heat::TestResource' + } + } + }, + 'action': 'CREATE', + 'project_id': '56789', + 'id': '2468', + 'resources': { + 'thing': { + 'status': 'COMPLETE', + 'name': 'thing', + 'resource_data': { + 'value': 'test_string', + }, + 'resource_id': 'my_stack-thing-1234', + 'action': 'CREATE', + 'type': 'OS::Heat::TestResource', + 'metadata': {} + } + } +} diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index b6fd99c4..25e297f9 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -520,3 +520,68 @@ def test_stack_list_tags_bad_mode(self): parsed_args = self.check_parser(self.cmd, arglist, []) self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + +class TestStackAdopt(TestStack): + + adopt_file = 'heatclient/tests/test_templates/adopt.json' + + with open(adopt_file, 'r') as f: + adopt_data = f.read() + + defaults = { + 'stack_name': 'my_stack', + 'disable_rollback': True, + 'adopt_stack_data': adopt_data, + 'parameters': {}, + 'files': {}, + 'environment': {}, + 'timeout': None + } + + def setUp(self): + super(TestStackAdopt, self).setUp() + self.cmd = stack.AdoptStack(self.app, None) + self.stack_client.create = mock.MagicMock( + return_value={'stack': {'id': '1234'}}) + + def test_stack_adopt_defaults(self): + arglist = ['my_stack', '--adopt-file', self.adopt_file] + cols = ['id', 'stack_name', 'description', 'creation_time', + 'updated_time', 'stack_status', 'stack_status_reason'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**self.defaults) + self.assertEqual(cols, columns) + + def test_stack_adopt_enable_rollback(self): + arglist = ['my_stack', '--adopt-file', self.adopt_file, + '--enable-rollback'] + kwargs = copy.deepcopy(self.defaults) + kwargs['disable_rollback'] = False + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**kwargs) + + def test_stack_adopt_wait(self): + arglist = ['my_stack', '--adopt-file', self.adopt_file, '--wait'] + self.stack_client.get = mock.MagicMock(return_value=( + stacks.Stack(None, {'stack_status': 'CREATE_COMPLETE'}))) + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**self.defaults) + self.stack_client.get.assert_called_with(**{'stack_id': '1234'}) + + def test_stack_adopt_wait_fail(self): + arglist = ['my_stack', '--adopt-file', self.adopt_file, '--wait'] + self.stack_client.get = mock.MagicMock(return_value=( + stacks.Stack(None, {'stack_status': 'CREATE_FAILED'}))) + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) diff --git a/setup.cfg b/setup.cfg index 1d034e45..19e12b7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,7 @@ openstack.orchestration.v1 = stack_create = heatclient.osc.v1.stack:CreateStack stack_update = heatclient.osc.v1.stack:UpdateStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot + stack_adopt = heatclient.osc.v1.stack:AdoptStack [global] From 1970d2a32eba7eb530e3a76b12305c7676bb1cee Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Wed, 3 Feb 2016 14:31:46 +1300 Subject: [PATCH 120/553] OpenstackClient plugin for stack abandon This change implements the "openstack stack abandon" command Blueprint: heat-support-python-openstackclient Change-Id: I64a3f0848e9d355b4fdbc7d63e8f6dca762de00e --- heatclient/osc/v1/stack.py | 47 +++++++++++++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 53 ++++++++++++++++++++++ setup.cfg | 1 + 3 files changed, 101 insertions(+) diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 5d8ed2a6..1b086915 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -20,8 +20,11 @@ from openstackclient.common import exceptions as exc from openstackclient.common import parseractions from openstackclient.common import utils +from oslo_serialization import jsonutils +import six from six.moves.urllib import request +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 @@ -652,3 +655,47 @@ def take_action(self, parsed_args): raise exc.CommandError(msg) return _show_stack(client, stack['id'], format='table', short=True) + + +class AbandonStack(format_utils.JsonFormat): + """Abandon stack and output results.""" + + log = logging.getLogger(__name__ + '.AbandonStack') + + def get_parser(self, prog_name): + parser = super(AbandonStack, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack to abandon') + ) + parser.add_argument( + '--output-file', + metavar='', + help=_('File to output abandon results') + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + try: + stack = client.stacks.abandon(stack_id=parsed_args.stack) + except heat_exc.HTTPNotFound: + msg = _('Stack not found: %s') % parsed_args.stack + raise exc.CommandError(msg) + + if parsed_args.output_file is not None: + try: + with open(parsed_args.output_file, 'w') as f: + f.write(jsonutils.dumps(stack, indent=2)) + return [], None + except IOError as e: + raise exc.CommandError(str(e)) + + data = list(six.itervalues(stack)) + columns = list(six.iterkeys(stack)) + return columns, data diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index 25e297f9..d52157ec 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -12,6 +12,7 @@ # import copy +import io import mock import six import testscenarios @@ -19,6 +20,7 @@ from openstackclient.common import exceptions as exc from openstackclient.common import utils +from heatclient import exc as heat_exc from heatclient.osc.v1 import stack from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes from heatclient.v1 import stacks @@ -585,3 +587,54 @@ def test_stack_adopt_wait_fail(self): parsed_args = self.check_parser(self.cmd, arglist, []) self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + +class TestStackAbandon(TestStack): + + columns = ['stack_name', 'stack_status', 'id'] + data = ['my_stack', 'ABANDONED', '1234'] + + response = dict(zip(columns, data)) + + def setUp(self): + super(TestStackAbandon, self).setUp() + self.cmd = stack.AbandonStack(self.app, None) + self.stack_client.abandon = mock.MagicMock(return_value=self.response) + + def test_stack_abandon(self): + arglist = ['my_stack'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + for column in self.columns: + self.assertIn(column, columns) + for datum in self.data: + self.assertIn(datum, data) + + def test_stack_abandon_not_found(self): + arglist = ['my_stack'] + self.stack_client.abandon.side_effect = heat_exc.HTTPNotFound + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + @mock.patch('heatclient.osc.v1.stack.open', create=True) + def test_stack_abandon_output_file(self, mock_open): + arglist = ['my_stack', '--output-file', 'file.json'] + mock_open.return_value = mock.MagicMock(spec=io.IOBase) + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + mock_open.assert_called_once_with('file.json', 'w') + self.assertEqual([], columns) + self.assertIsNone(data) + + @mock.patch('heatclient.osc.v1.stack.open', create=True, + side_effect=IOError) + def test_stack_abandon_output_file_error(self, mock_open): + arglist = ['my_stack', '--output-file', 'file.json'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) diff --git a/setup.cfg b/setup.cfg index 19e12b7b..58f06888 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,6 +36,7 @@ openstack.orchestration.v1 = stack_update = heatclient.osc.v1.stack:UpdateStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot stack_adopt = heatclient.osc.v1.stack:AdoptStack + stack_abandon = heatclient.osc.v1.stack:AbandonStack [global] From c90ae85964d492e0ad6db53a0898365c4b6072ec Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Wed, 3 Feb 2016 17:06:33 +1300 Subject: [PATCH 121/553] Add openstack client software config delete Add config delete based upon heat clis: heat config-delete Change-Id: I37d37b530803cf8bb1d5e7852d8fb39202d4ff71 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/software_config.py | 64 +++++++++++++++++++ .../tests/unit/osc/v1/test_sotware_config.py | 64 +++++++++++++++++++ setup.cfg | 1 + 3 files changed, 129 insertions(+) create mode 100644 heatclient/osc/v1/software_config.py create mode 100644 heatclient/tests/unit/osc/v1/test_sotware_config.py diff --git a/heatclient/osc/v1/software_config.py b/heatclient/osc/v1/software_config.py new file mode 100644 index 00000000..033fc6dc --- /dev/null +++ b/heatclient/osc/v1/software_config.py @@ -0,0 +1,64 @@ +# 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. +# + +"""Orchestration v1 software config action implementations""" + +import logging + +from cliff import command +from openstackclient.common import exceptions as exc + +from heatclient import exc as heat_exc +from heatclient.openstack.common._i18n import _ + + +class DeleteConfig(command.Command): + """Delete software configs""" + + log = logging.getLogger(__name__ + ".DeleteConfig") + + def get_parser(self, prog_name): + parser = super(DeleteConfig, self).get_parser(prog_name) + parser.add_argument( + 'id', + metavar='', + nargs='+', + help=_('IDs of the software configs to delete') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + return _delete_config(heat_client, parsed_args) + + +def _delete_config(heat_client, args): + failure_count = 0 + + for config_id in args.id: + try: + heat_client.software_configs.delete( + config_id=config_id) + except Exception as e: + if isinstance(e, heat_exc.HTTPNotFound): + print(_('Software config with ID %s not found') % config_id) + failure_count += 1 + continue + + if failure_count: + raise exc.CommandError(_('Unable to delete %(count)s of the ' + '%(total)s software configs.') % + {'count': failure_count, + 'total': len(args.id)}) diff --git a/heatclient/tests/unit/osc/v1/test_sotware_config.py b/heatclient/tests/unit/osc/v1/test_sotware_config.py new file mode 100644 index 00000000..6f018045 --- /dev/null +++ b/heatclient/tests/unit/osc/v1/test_sotware_config.py @@ -0,0 +1,64 @@ +# 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 mock + +from openstackclient.common import exceptions as exc + +from heatclient import exc as heat_exc +from heatclient.osc.v1 import software_config +from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes + + +class TestConfig(orchestration_fakes.TestOrchestrationv1): + def setUp(self): + super(TestConfig, self).setUp() + self.mock_client = self.app.client_manager.orchestration + + +class TestDeleteConfig(TestConfig): + + def setUp(self): + super(TestDeleteConfig, self).setUp() + self.cmd = software_config.DeleteConfig(self.app, None) + self.mock_delete = mock.Mock() + self.mock_client.software_configs.delete = self.mock_delete + + def test_config_delete(self): + arglist = ['id_123'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_delete.assert_called_with( + config_id='id_123') + + def test_config_delete_multi(self): + arglist = ['id_123', 'id_456'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_delete.assert_has_calls( + [mock.call(config_id='id_123'), + mock.call(config_id='id_456')]) + + def test_config_delete_not_found(self): + arglist = ['id_123', 'id_456', 'id_789'] + self.mock_client.software_configs.delete.side_effect = [ + None, heat_exc.HTTPNotFound, None] + parsed_args = self.check_parser(self.cmd, arglist, []) + error = self.assertRaises(exc.CommandError, self.cmd.take_action, + parsed_args) + self.mock_delete.assert_has_calls( + [mock.call(config_id='id_123'), + mock.call(config_id='id_456'), + mock.call(config_id='id_789')]) + self.assertEqual('Unable to delete 1 of the 3 software configs.', + str(error)) diff --git a/setup.cfg b/setup.cfg index 85c7d391..a8ca467b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ openstack.cli.extension = orchestration = heatclient.osc.plugin openstack.orchestration.v1 = + software_config_delete = heatclient.osc.v1.software_config:DeleteConfig software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment stack_show = heatclient.osc.v1.stack:ShowStack stack_list = heatclient.osc.v1.stack:ListStack From c5841cb6242502af94964a1189fe8fb2060327a5 Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Tue, 24 Nov 2015 05:02:13 -0600 Subject: [PATCH 122/553] Add OpenstackClient plugin for software deployment list This change implements the "openstack software deployment list" command Base on the existing heat command: heat deployment-list Change-Id: I811aea76eb31b252021e33dd6c49298ede5d9500 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/software_deployment.py | 43 +++++++++++++ .../unit/osc/v1/test_software_deployment.py | 64 +++++++++++++++++++ setup.cfg | 1 + 3 files changed, 108 insertions(+) diff --git a/heatclient/osc/v1/software_deployment.py b/heatclient/osc/v1/software_deployment.py index c8183ccf..a0259bae 100644 --- a/heatclient/osc/v1/software_deployment.py +++ b/heatclient/osc/v1/software_deployment.py @@ -16,7 +16,10 @@ import logging from cliff import command +from cliff import lister + from openstackclient.common import exceptions as exc +from openstackclient.common import utils from heatclient import exc as heat_exc from heatclient.openstack.common._i18n import _ @@ -69,3 +72,43 @@ def take_action(self, parsed_args): '%(total)s deployments.') % {'count': failure_count, 'total': len(parsed_args.id)}) + + +class ListDeployment(lister.Lister): + """List software deployments.""" + + log = logging.getLogger(__name__ + '.ListDeployment') + + def get_parser(self, prog_name): + parser = super(ListDeployment, self).get_parser(prog_name) + parser.add_argument( + '--server', + metavar='', + help=_('ID of the server to fetch deployments for') + ) + parser.add_argument( + '--long', + action='store_true', + help=_('List more fields in output') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + return _list_deployment(heat_client, args=parsed_args) + + +def _list_deployment(heat_client, args=None): + kwargs = {'server_id': args.server} if args.server else {} + columns = ['id', 'config_id', 'server_id', 'action', 'status'] + if args.long: + columns.append('creation_time') + columns.append('status_reason') + + deployments = heat_client.software_deployments.list(**kwargs) + return ( + columns, + (utils.get_item_properties(s, columns) for s in deployments) + ) diff --git a/heatclient/tests/unit/osc/v1/test_software_deployment.py b/heatclient/tests/unit/osc/v1/test_software_deployment.py index c4e9d31a..36408b40 100644 --- a/heatclient/tests/unit/osc/v1/test_software_deployment.py +++ b/heatclient/tests/unit/osc/v1/test_software_deployment.py @@ -72,3 +72,67 @@ def test_deployment_config_delete_failed(self): exc.CommandError, self.cmd.take_action, parsed_args) self.assertEqual("Unable to delete 1 of the 1 deployments.", str(error)) + + +class TestDeploymentList(TestDeployment): + + columns = ['id', 'config_id', 'server_id', 'action', 'status'] + + data = {"software_deployments": [ + { + "status": "COMPLETE", + "server_id": "ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5", + "config_id": "8da95794-2ad9-4979-8ae5-739ce314c5cd", + "output_values": { + "deploy_stdout": "Writing to /tmp/barmy Written to /tmp/barmy", + "deploy_stderr": "+ echo Writing to /tmp/barmy\n+ echo fu\n+ c" + "at /tmp/barmy\n+ echo -n The file /tmp/barmy" + "contains for server ec14c864-096e-4e27-bb8a-" + "2c2b4dc6f3f5 during CREATE\n+" + "echo Output to stderr\nOutput to stderr\n", + "deploy_status_code": 0, + "result": "The file /tmp/barmy contains fu for server " + "ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5 during CREATE" + }, + "input_values": None, + "action": "CREATE", + "status_reason": "Outputs received", + "id": "ef422fa5-719a-419e-a10c-72e3a367b0b8", + "creation_time": "2015-01-31T15:12:36Z", + "updated_time": "2015-01-31T15:18:21Z" + } + ] + } + + def setUp(self): + super(TestDeploymentList, self).setUp() + self.cmd = software_deployment.ListDeployment(self.app, None) + self.mock_client.list = mock.MagicMock(return_value=[self.data]) + + def test_deployment_list(self): + arglist = [] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.list.assert_called_with() + self.assertEqual(self.columns, columns) + + def test_deployment_list_server(self): + kwargs = {} + kwargs['server_id'] = 'ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5' + arglist = ['--server', 'ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.list.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) + + def test_deployment_list_long(self): + kwargs = {} + cols = ['id', 'config_id', 'server_id', 'action', 'status', + 'creation_time', 'status_reason'] + arglist = ['--long'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.mock_client.list.assert_called_with(**kwargs) + self.assertEqual(cols, columns) diff --git a/setup.cfg b/setup.cfg index 17618453..94929b65 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ openstack.cli.extension = openstack.orchestration.v1 = software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment + software_deployment_list = heatclient.osc.v1.software_deployment:ListDeployment stack_show = heatclient.osc.v1.stack:ShowStack stack_list = heatclient.osc.v1.stack:ListStack stack_create = heatclient.osc.v1.stack:CreateStack From acef0751344c6e5ac7dbd10c4fa9d197750b5651 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Mon, 23 Nov 2015 21:46:33 +0000 Subject: [PATCH 123/553] OpenstackClient plugin for template version list This change implements the "openstack orchestration template version list" command. Blueprint: heat-support-python-openstackclient Change-Id: I2c2ab5369d9f3066eff8fed0ba44ec9d3f5e421b --- heatclient/osc/v1/template.py | 37 ++++++++++++++ heatclient/tests/unit/osc/v1/fakes.py | 1 + heatclient/tests/unit/osc/v1/test_template.py | 49 +++++++++++++++++++ setup.cfg | 1 + 4 files changed, 88 insertions(+) create mode 100644 heatclient/osc/v1/template.py create mode 100644 heatclient/tests/unit/osc/v1/test_template.py diff --git a/heatclient/osc/v1/template.py b/heatclient/osc/v1/template.py new file mode 100644 index 00000000..c6e24b99 --- /dev/null +++ b/heatclient/osc/v1/template.py @@ -0,0 +1,37 @@ +# 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. +# +# Copyright 2015 IBM Corp. + +import logging + +from cliff import lister +from openstackclient.common import utils + + +class VersionList(lister.Lister): + """List the available template versions.""" + + log = logging.getLogger(__name__ + '.VersionList') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + versions = client.template_versions.list() + fields = ['version', 'type'] + + return ( + fields, + (utils.get_item_properties(s, fields) for s in versions) + ) diff --git a/heatclient/tests/unit/osc/v1/fakes.py b/heatclient/tests/unit/osc/v1/fakes.py index 1888c135..ee7f2382 100644 --- a/heatclient/tests/unit/osc/v1/fakes.py +++ b/heatclient/tests/unit/osc/v1/fakes.py @@ -33,6 +33,7 @@ def __init__(self, **kwargs): self.build_info = fakes.FakeResource(None, {}) self.software_deployments = fakes.FakeResource(None, {}) self.software_configs = fakes.FakeResource(None, {}) + self.template_versions = fakes.FakeResource(None, {}) class TestOrchestrationv1(utils.TestCommand): diff --git a/heatclient/tests/unit/osc/v1/test_template.py b/heatclient/tests/unit/osc/v1/test_template.py new file mode 100644 index 00000000..f5d0169c --- /dev/null +++ b/heatclient/tests/unit/osc/v1/test_template.py @@ -0,0 +1,49 @@ +# 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. +# +# Copyright 2015 IBM Corp. + +import mock + +from heatclient.osc.v1 import template +from heatclient.tests.unit.osc.v1 import fakes +from heatclient.v1 import template_versions + + +class TestTemplate(fakes.TestOrchestrationv1): + def setUp(self): + super(TestTemplate, self).setUp() + self.mock_client = self.app.client_manager.orchestration + self.template_versions = self.mock_client.template_versions + + +class TestTemplateVersionList(TestTemplate): + + defaults = [ + {'version': 'HOT123', 'type': 'hot'}, + {'version': 'CFN456', 'type': 'cfn'} + ] + + def setUp(self): + super(TestTemplateVersionList, self).setUp() + tv1 = template_versions.TemplateVersion(None, self.defaults[0]) + tv2 = template_versions.TemplateVersion(None, self.defaults[1]) + self.template_versions.list = mock.MagicMock(return_value=[tv1, tv2]) + self.cmd = template.VersionList(self.app, None) + + def test_version_list(self): + parsed_args = self.check_parser(self.cmd, [], []) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(['version', 'type'], columns) + self.assertEqual([('HOT123', 'hot'), ('CFN456', 'cfn')], list(data)) diff --git a/setup.cfg b/setup.cfg index 5359ad40..9d07cb1f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ openstack.orchestration.v1 = stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot stack_adopt = heatclient.osc.v1.stack:AdoptStack stack_abandon = heatclient.osc.v1.stack:AbandonStack + orchestration_template_version_list = heatclient.osc.v1.template:VersionList [global] From 8601c3097a575fb33f528ee2be16834b632d184c Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 24 Nov 2015 15:44:36 +0000 Subject: [PATCH 124/553] OpenstackClient plugin for template function list This change implements the "openstack orchestration template function list" command. Blueprint: heat-support-python-openstackclient Change-Id: I7565a1cdd7445f77e69e393902a76dadef440d45 --- heatclient/osc/v1/template.py | 37 +++++++++++++++++++ heatclient/tests/unit/osc/v1/test_template.py | 33 +++++++++++++++++ setup.cfg | 1 + 3 files changed, 71 insertions(+) diff --git a/heatclient/osc/v1/template.py b/heatclient/osc/v1/template.py index c6e24b99..57d4964f 100644 --- a/heatclient/osc/v1/template.py +++ b/heatclient/osc/v1/template.py @@ -17,6 +17,9 @@ from cliff import lister from openstackclient.common import utils +from heatclient import exc +from heatclient.openstack.common._i18n import _ + class VersionList(lister.Lister): """List the available template versions.""" @@ -35,3 +38,37 @@ def take_action(self, parsed_args): fields, (utils.get_item_properties(s, fields) for s in versions) ) + + +class FunctionList(lister.Lister): + """List the available functions.""" + + log = logging.getLogger(__name__ + '.FunctionList') + + def get_parser(self, prog_name): + parser = super(FunctionList, self).get_parser(prog_name) + parser.add_argument( + 'template_version', + metavar='', + help=_('Template version to get the functions for') + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + version = parsed_args.template_version + try: + functions = client.template_versions.get(version) + except exc.HTTPNotFound: + msg = _('Template version not found: %s') % version + raise exc.CommandError(msg) + + fields = ['functions', 'description'] + return ( + fields, + (utils.get_item_properties(s, fields) for s in functions) + ) diff --git a/heatclient/tests/unit/osc/v1/test_template.py b/heatclient/tests/unit/osc/v1/test_template.py index f5d0169c..17b14e79 100644 --- a/heatclient/tests/unit/osc/v1/test_template.py +++ b/heatclient/tests/unit/osc/v1/test_template.py @@ -14,6 +14,7 @@ import mock +from heatclient import exc from heatclient.osc.v1 import template from heatclient.tests.unit.osc.v1 import fakes from heatclient.v1 import template_versions @@ -47,3 +48,35 @@ def test_version_list(self): self.assertEqual(['version', 'type'], columns) self.assertEqual([('HOT123', 'hot'), ('CFN456', 'cfn')], list(data)) + + +class TestTemplateFunctionList(TestTemplate): + + defaults = [ + {'functions': 'func1', 'description': 'Function 1'}, + {'functions': 'func2', 'description': 'Function 2'} + ] + + def setUp(self): + super(TestTemplateFunctionList, self).setUp() + tv1 = template_versions.TemplateVersion(None, self.defaults[0]) + tv2 = template_versions.TemplateVersion(None, self.defaults[1]) + self.template_versions.get = mock.MagicMock(return_value=[tv1, tv2]) + self.cmd = template.FunctionList(self.app, None) + + def test_function_list(self): + arglist = ['version1'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(['functions', 'description'], columns) + self.assertEqual([('func1', 'Function 1'), ('func2', 'Function 2')], + list(data)) + + def test_function_list_not_found(self): + arglist = ['bad_version'] + self.template_versions.get.side_effect = exc.HTTPNotFound + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) diff --git a/setup.cfg b/setup.cfg index 9d07cb1f..417287a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ openstack.orchestration.v1 = stack_adopt = heatclient.osc.v1.stack:AdoptStack stack_abandon = heatclient.osc.v1.stack:AbandonStack orchestration_template_version_list = heatclient.osc.v1.template:VersionList + orchestration_template_function_list = heatclient.osc.v1.template:FunctionList [global] From d1116972d53993b7d2dbef52a6d4c3d57e746d59 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Tue, 9 Feb 2016 11:22:16 +1300 Subject: [PATCH 125/553] Sort osc command entry points Due to the number of reviews adding new commands to setup.cfg openstack.orchestration.v1, most of these reviews have merge conflicts because they append to the openstack.orchestration.v1 list. This change sorts the list. There will be less merge conflicts if all osc changes are rebased so that they maintain this sorting, which will allow the changes to land faster. Change-Id: I834df989e7175dc49098e4a281b5e2afde21fdb1 Blueprint: heat-support-python-openstackclient --- setup.cfg | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/setup.cfg b/setup.cfg index 417287a7..9f320199 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,19 +30,18 @@ openstack.cli.extension = orchestration = heatclient.osc.plugin openstack.orchestration.v1 = + orchestration_template_function_list = heatclient.osc.v1.template:FunctionList + orchestration_template_version_list = heatclient.osc.v1.template:VersionList software_config_delete = heatclient.osc.v1.software_config:DeleteConfig software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment software_deployment_list = heatclient.osc.v1.software_deployment:ListDeployment - stack_show = heatclient.osc.v1.stack:ShowStack - stack_list = heatclient.osc.v1.stack:ListStack + stack_abandon = heatclient.osc.v1.stack:AbandonStack + stack_adopt = heatclient.osc.v1.stack:AdoptStack stack_create = heatclient.osc.v1.stack:CreateStack - stack_update = heatclient.osc.v1.stack:UpdateStack + stack_list = heatclient.osc.v1.stack:ListStack + stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot - stack_adopt = heatclient.osc.v1.stack:AdoptStack - stack_abandon = heatclient.osc.v1.stack:AbandonStack - orchestration_template_version_list = heatclient.osc.v1.template:VersionList - orchestration_template_function_list = heatclient.osc.v1.template:FunctionList - + stack_update = heatclient.osc.v1.stack:UpdateStack [global] setup-hooks = From ad48215db10246fa9d210529b107dc5e0f6008ad Mon Sep 17 00:00:00 2001 From: Ahmed Elkhouly Date: Mon, 8 Feb 2016 17:32:19 -0500 Subject: [PATCH 126/553] Add heat client support for Resource set health Add a new command to set resource's health. The command takes the following format: heat resource-mark-unhealthy [--reset] reason Change-Id: I4e0a127343e591c176f451172a1c8a59bf36f6eb Partially-implements: blueprint mark-unhealthy Depends-On: Ifa48b179723a2100fff548467db9e162bc669d13 --- heatclient/tests/unit/test_resources.py | 14 +++++ heatclient/tests/unit/test_shell.py | 81 +++++++++++++++++++++++++ heatclient/v1/resources.py | 21 +++++++ heatclient/v1/shell.py | 22 +++++++ 4 files changed, 138 insertions(+) diff --git a/heatclient/tests/unit/test_resources.py b/heatclient/tests/unit/test_resources.py index 120b1c4b..e68bdf2b 100644 --- a/heatclient/tests/unit/test_resources.py +++ b/heatclient/tests/unit/test_resources.py @@ -192,6 +192,20 @@ def test_signal(self): manager.signal(**fields) self.m.VerifyAll() + def test_mark_unhealthy(self): + fields = {'stack_id': 'teststack', + 'resource_name': 'testresource', + 'mark_unhealthy': 'True', + 'resource_status_reason': 'Anything'} + expect = ('PATCH', + '/stacks/teststack%2Fabcd1234/resources' + '/testresource') + key = 'mark_unhealthy' + + manager = self._base_test(expect, key) + manager.mark_unhealthy(**fields) + self.m.VerifyAll() + class ResourceStackNameTest(testtools.TestCase): diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index a1056867..75572f7c 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -4032,6 +4032,87 @@ def test_resource_signal_data_file(self): stack_id, resource_name, data_file.name)) self.assertEqual("", text) + def test_resource_mark_unhealthy(self): + self.register_keystone_auth_fixture() + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {}, + '') + stack_id = 'teststack/1' + resource_name = 'aResource' + http.SessionClient.request( + '/stacks/%s/resources/%s' % + ( + parse.quote(stack_id, ''), + parse.quote(encodeutils.safe_encode( + resource_name), '') + ), + 'PATCH', + data={'mark_unhealthy': True, + 'resource_status_reason': 'Any'}).AndReturn(resp) + + self.m.ReplayAll() + + text = self.shell( + 'resource-mark-unhealthy {0} {1} Any'.format( + stack_id, resource_name)) + self.assertEqual("", text) + + def test_resource_mark_unhealthy_reset(self): + self.register_keystone_auth_fixture() + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {}, + '') + stack_id = 'teststack/1' + resource_name = 'aResource' + http.SessionClient.request( + '/stacks/%s/resources/%s' % + ( + parse.quote(stack_id, ''), + parse.quote(encodeutils.safe_encode( + resource_name), '') + ), + 'PATCH', + data={'mark_unhealthy': False, + 'resource_status_reason': 'Any'}).AndReturn(resp) + + self.m.ReplayAll() + + text = self.shell( + 'resource-mark-unhealthy --reset {0} {1} Any'.format( + stack_id, resource_name)) + self.assertEqual("", text) + + def test_resource_mark_unhealthy_no_reason(self): + self.register_keystone_auth_fixture() + resp = fakes.FakeHTTPResponse( + 200, + 'OK', + {}, + '') + stack_id = 'teststack/1' + resource_name = 'aResource' + http.SessionClient.request( + '/stacks/%s/resources/%s' % + ( + parse.quote(stack_id, ''), + parse.quote(encodeutils.safe_encode( + resource_name), '') + ), + 'PATCH', + data={'mark_unhealthy': True, + 'resource_status_reason': ''}).AndReturn(resp) + + self.m.ReplayAll() + + text = self.shell( + 'resource-mark-unhealthy {0} {1}'.format( + stack_id, resource_name)) + self.assertEqual("", text) + class ShellTestResourceTypes(ShellBase): def setUp(self): diff --git a/heatclient/v1/resources.py b/heatclient/v1/resources.py index f063c841..3ad6fdf0 100644 --- a/heatclient/v1/resources.py +++ b/heatclient/v1/resources.py @@ -112,6 +112,27 @@ def signal(self, stack_id, resource_name, data=None): body = utils.get_response_body(resp) return body + def mark_unhealthy(self, stack_id, resource_name, + mark_unhealthy, resource_status_reason): + """Mark a resource as healthy or unhealthy. + + :param stack_id: ID of stack containing the resource + :param resource_name: ID of resource + :param mark_unhealthy: Mark resource unhealthy if set to True + :param resource_status_reason: Reason for resource status change. + """ + stack_id = self._resolve_stack_id(stack_id) + url_str = '/stacks/%s/resources/%s' % ( + parse.quote(stack_id, ''), + parse.quote(encodeutils.safe_encode(resource_name), '')) + resp = self.client.patch( + url_str, + data={"mark_unhealthy": mark_unhealthy, + "resource_status_reason": resource_status_reason}) + + body = utils.get_response_body(resp) + return body + def generate_template(self, resource_name): """Deprecated in favor of generate_template in ResourceTypeManager.""" diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index f0fcd6d8..85b8a3d5 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -962,6 +962,28 @@ def do_resource_signal(hc, args): {'id': args.id, 'resource': args.resource}) +@utils.arg('id', metavar='', + help=_('Name or ID of stack the resource belongs to.')) +@utils.arg('resource', metavar='', + help=_('Name of the resource.')) +@utils.arg('reason', default="", nargs='?', + help=_('Reason for state change.')) +@utils.arg('--reset', default=False, action="store_true", + help=_('Set the resource as healthy.')) +def do_resource_mark_unhealthy(hc, args): + '''Set resource's health.''' + fields = {'stack_id': args.id, + 'resource_name': args.resource, + 'mark_unhealthy': not args.reset, + 'resource_status_reason': args.reason} + try: + hc.resources.mark_unhealthy(**fields) + except exc.HTTPNotFound: + raise exc.CommandError(_('Stack or resource not found: ' + '%(id)s %(resource)s') % + {'id': args.id, 'resource': args.resource}) + + @utils.arg('id', metavar='', help=_('Name or ID of the stack these resources belong to.')) @utils.arg('--pre-create', action='store_true', default=False, From 5975d12b6f125aa9ad387a9b85ad13385ddf1b4c Mon Sep 17 00:00:00 2001 From: Kanagaraj Manickam Date: Fri, 11 Dec 2015 12:30:05 +0530 Subject: [PATCH 127/553] Adds ignore_errors for template-validate command template-validate --ignore-errors implements blueprint heat-template-validate-improvements Change-Id: Ice4f623bc06366ddd600d5cb72d236e5a4459e19 --- heatclient/tests/unit/test_stacks.py | 11 ++++++++--- heatclient/v1/shell.py | 5 +++++ heatclient/v1/stacks.py | 17 +++++++++++++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/heatclient/tests/unit/test_stacks.py b/heatclient/tests/unit/test_stacks.py index b55c7d06..9b6d4e70 100644 --- a/heatclient/tests/unit/test_stacks.py +++ b/heatclient/tests/unit/test_stacks.py @@ -350,7 +350,7 @@ def test_validate_show_nested(self): # Verify self.assertEqual(self.mock_response.json.return_value, result) self.mock_client.post.assert_called_once_with( - '/validate?show_nested=True', data={}) + '/validate', params={'show_nested': True}) def test_validate_show_nested_false(self): # Test @@ -359,7 +359,7 @@ def test_validate_show_nested_false(self): # Verify self.assertEqual(self.mock_response.json.return_value, result) self.mock_client.post.assert_called_once_with( - '/validate', data={}) + '/validate') def test_validate_show_nested_default(self): # Test @@ -368,4 +368,9 @@ def test_validate_show_nested_default(self): # Verify self.assertEqual(self.mock_response.json.return_value, result) self.mock_client.post.assert_called_once_with( - '/validate', data={}) + '/validate') + + def test_validate_ignore_errors(self): + self.manager.validate(ignore_errors='99001,99002') + self.mock_client.post.assert_called_once_with( + '/validate', params={'ignore_errors': '99001,99002'}) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index b8c6e1e4..0632332f 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -799,6 +799,8 @@ def do_template_show(hc, args): 'This can be specified multiple times, or once with ' 'parameters separated by a semicolon.'), action='append') +@utils.arg('-I', '--ignore-errors', metavar='', + help=_('List of heat errors to ignore.')) def do_template_validate(hc, args): """Validate a template with parameters.""" @@ -817,6 +819,9 @@ def do_template_validate(hc, args): 'environment': env, } + if args.ignore_errors: + fields['ignore_errors'] = args.ignore_errors + if args.show_nested: fields['show_nested'] = args.show_nested diff --git a/heatclient/v1/stacks.py b/heatclient/v1/stacks.py index 5a4f9c59..5fbe94af 100644 --- a/heatclient/v1/stacks.py +++ b/heatclient/v1/stacks.py @@ -277,10 +277,23 @@ def template(self, stack_id): def validate(self, **kwargs): """Validate a stack template.""" + url = '/validate' + + params = {} if kwargs.pop('show_nested', False): - url += '?show_nested=True' + params['show_nested'] = True + + ignore_errors = kwargs.pop('ignore_errors', None) + if ignore_errors: + params['ignore_errors'] = ignore_errors + + args = {} + if kwargs: + args['data'] = kwargs + if params: + args['params'] = params - resp = self.client.post(url, data=kwargs) + resp = self.client.post(url, **args) body = utils.get_response_body(resp) return body From ab78ff7ae6d9e38f0721f418095396937e5bc22e Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Wed, 9 Dec 2015 14:28:59 -0600 Subject: [PATCH 128/553] Add openstack client stack resource metadata show Add the openstack stack resource metadata command. Includes support for the json or yaml output. Based from the existing heat commands: heat resource-metadata Change-Id: I7cd657ae912386381e9c766cce74aa5e466d62ec Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/resources.py | 63 +++++++++++++++++++ .../tests/unit/osc/v1/test_resources.py | 60 ++++++++++++++++++ setup.cfg | 1 + 3 files changed, 124 insertions(+) create mode 100644 heatclient/osc/v1/resources.py create mode 100644 heatclient/tests/unit/osc/v1/test_resources.py diff --git a/heatclient/osc/v1/resources.py b/heatclient/osc/v1/resources.py new file mode 100644 index 00000000..9f9a4d05 --- /dev/null +++ b/heatclient/osc/v1/resources.py @@ -0,0 +1,63 @@ +# 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. +# + +"""Orchestration v1 Stack action implementations""" + +import logging +import six + +from openstackclient.common import exceptions as exc +from openstackclient.i18n import _ + +from heatclient.common import format_utils +from heatclient import exc as heat_exc + + +class ResourceMetadata(format_utils.JsonFormat): + """Show resource metadata""" + + log = logging.getLogger(__name__ + ".ResourceMetadata") + + def get_parser(self, prog_name): + parser = super(ResourceMetadata, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Stack to display (name or ID)'), + ) + parser.add_argument( + 'resource', + metavar='', + help=_('Name of the resource to show the metadata for')) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + return _resource_metadata(heat_client, parsed_args) + + +def _resource_metadata(heat_client, args): + fields = {'stack_id': args.stack, + 'resource_name': args.resource} + try: + metadata = heat_client.resources.metadata(**fields) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Stack or resource not found: ' + '%(stack)s %(resource)s') % + {'stack': args.stack, + 'resource': args.resource}) + data = list(six.itervalues(metadata)) + columns = list(six.iterkeys(metadata)) + return columns, data diff --git a/heatclient/tests/unit/osc/v1/test_resources.py b/heatclient/tests/unit/osc/v1/test_resources.py new file mode 100644 index 00000000..3d9a1246 --- /dev/null +++ b/heatclient/tests/unit/osc/v1/test_resources.py @@ -0,0 +1,60 @@ +# 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 mock + +from openstackclient.common import exceptions as exc + +from heatclient import exc as heat_exc +from heatclient.osc.v1 import resources +from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes + + +class TestResource(orchestration_fakes.TestOrchestrationv1): + def setUp(self): + super(TestResource, self).setUp() + self.mock_client = self.app.client_manager.orchestration + + +class TestResourceMetadata(TestResource): + + def setUp(self): + super(TestResourceMetadata, self).setUp() + self.cmd = resources.ResourceMetadata(self.app, None) + self.mock_client.resources.metadata = mock.Mock( + return_value={}) + + def test_resource_metadata(self): + arglist = ['my_stack', 'my_resource'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.resources.metadata.assert_called_with(**{ + 'stack_id': 'my_stack', + 'resource_name': 'my_resource' + }) + + def test_resource_metadata_yaml(self): + arglist = ['my_stack', 'my_resource', '--format', 'yaml'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.resources.metadata.assert_called_with(**{ + 'stack_id': 'my_stack', + 'resource_name': 'my_resource' + }) + + def test_resource_metadata_error(self): + arglist = ['my_stack', 'my_resource'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.resources.metadata = mock.Mock( + side_effect=heat_exc.HTTPNotFound) + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) diff --git a/setup.cfg b/setup.cfg index 9f320199..ea222f5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,6 +39,7 @@ openstack.orchestration.v1 = stack_adopt = heatclient.osc.v1.stack:AdoptStack stack_create = heatclient.osc.v1.stack:CreateStack stack_list = heatclient.osc.v1.stack:ListStack + stack_resource_metadata = heatclient.osc.v1.resources:ResourceMetadata stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot stack_update = heatclient.osc.v1.stack:UpdateStack From 6c8e03798cfee90980752123a1c6f23f25346145 Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Thu, 21 Jan 2016 14:15:18 -0600 Subject: [PATCH 129/553] Add openstack client software config list Add the openstack software config list command. Based from the existing heat commands: heat config-list Change-Id: I2016d859bbfcc4759a9032af73218e3cbc801594 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/software_config.py | 39 +++++++++++++++++++ .../tests/unit/osc/v1/test_sotware_config.py | 29 ++++++++++++++ setup.cfg | 1 + 3 files changed, 69 insertions(+) diff --git a/heatclient/osc/v1/software_config.py b/heatclient/osc/v1/software_config.py index 033fc6dc..c0ea931e 100644 --- a/heatclient/osc/v1/software_config.py +++ b/heatclient/osc/v1/software_config.py @@ -16,7 +16,9 @@ import logging from cliff import command +from cliff import lister from openstackclient.common import exceptions as exc +from openstackclient.common import utils from heatclient import exc as heat_exc from heatclient.openstack.common._i18n import _ @@ -62,3 +64,40 @@ def _delete_config(heat_client, args): '%(total)s software configs.') % {'count': failure_count, 'total': len(args.id)}) + + +class ListConfig(lister.Lister): + """List software configs""" + + log = logging.getLogger(__name__ + ".ListConfig") + + def get_parser(self, prog_name): + parser = super(ListConfig, self).get_parser(prog_name) + parser.add_argument( + '--limit', + metavar='', + help=_('Limit the number of configs returned') + ) + parser.add_argument( + '--marker', + metavar='', + help=_('Return configs that appear after the given config ID') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + heat_client = self.app.client_manager.orchestration + return _list_config(heat_client, parsed_args) + + +def _list_config(heat_client, args): + kwargs = {} + if args.limit: + kwargs['limit'] = args.limit + if args.marker: + kwargs['marker'] = args.marker + scs = heat_client.software_configs.list(**kwargs) + + columns = ['id', 'name', 'group', 'creation_time'] + return (columns, (utils.get_item_properties(s, columns) for s in scs)) diff --git a/heatclient/tests/unit/osc/v1/test_sotware_config.py b/heatclient/tests/unit/osc/v1/test_sotware_config.py index 6f018045..00260fa7 100644 --- a/heatclient/tests/unit/osc/v1/test_sotware_config.py +++ b/heatclient/tests/unit/osc/v1/test_sotware_config.py @@ -18,6 +18,7 @@ from heatclient import exc as heat_exc from heatclient.osc.v1 import software_config from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes +from heatclient.v1 import software_configs class TestConfig(orchestration_fakes.TestOrchestrationv1): @@ -62,3 +63,31 @@ def test_config_delete_not_found(self): mock.call(config_id='id_789')]) self.assertEqual('Unable to delete 1 of the 3 software configs.', str(error)) + + +class TestListConfig(TestConfig): + + def setUp(self): + super(TestListConfig, self).setUp() + self.cmd = software_config.ListConfig(self.app, None) + self.mock_client.software_configs.list = mock.Mock( + return_value=[software_configs.SoftwareConfig(None, {})]) + + def test_config_list(self): + arglist = [] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.software_configs.list.assert_called_once_with() + + def test_config_list_limit(self): + arglist = ['--limit', '3'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.software_configs.list.assert_called_with(limit='3') + + def test_config_list_marker(self): + arglist = ['--marker', 'id123'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.software_configs.list.assert_called_with( + marker='id123') diff --git a/setup.cfg b/setup.cfg index 9f320199..bea4662c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ openstack.orchestration.v1 = orchestration_template_function_list = heatclient.osc.v1.template:FunctionList orchestration_template_version_list = heatclient.osc.v1.template:VersionList software_config_delete = heatclient.osc.v1.software_config:DeleteConfig + software_config_list = heatclient.osc.v1.software_config:ListConfig software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment software_deployment_list = heatclient.osc.v1.software_deployment:ListDeployment stack_abandon = heatclient.osc.v1.stack:AbandonStack From 661ced35b4538789ea989bbc83212763b81c112d Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Mon, 16 Nov 2015 20:14:17 +0000 Subject: [PATCH 130/553] OpenstackClient plugin for stack output show This change implements the "openstack stack output show" command. Blueprint: heat-support-python-openstackclient Change-Id: I9c7538bf25664c46b652b4c9e300272691fb49e7 --- heatclient/osc/v1/stack.py | 75 ++++++++++++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 90 ++++++++++++++++++++++ setup.cfg | 1 + 3 files changed, 166 insertions(+) diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 1b086915..918cd710 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -699,3 +699,78 @@ def take_action(self, parsed_args): data = list(six.itervalues(stack)) columns = list(six.iterkeys(stack)) return columns, data + + +class OutputShowStack(show.ShowOne): + """Show stack output.""" + + log = logging.getLogger(__name__ + '.OutputShowStack') + + def get_parser(self, prog_name): + parser = super(OutputShowStack, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack to query') + ) + parser.add_argument( + 'output', + metavar='', + nargs='?', + default=None, + help=_('Name of an output to display') + ) + parser.add_argument( + '--all', + action='store_true', + help=_('Display all stack outputs') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + if not parsed_args.all and parsed_args.output is None: + msg = _('Either or --all must be specified.') + raise exc.CommandError(msg) + + if parsed_args.all and parsed_args.output is not None: + msg = _('Cannot specify both and --all.') + raise exc.CommandError(msg) + + if parsed_args.all: + try: + stack = client.stacks.get(parsed_args.stack) + except heat_exc.HTTPNotFound: + msg = _('Stack not found: %s') % parsed_args.stack + raise exc.CommandError(msg) + + outputs = stack.to_dict().get('outputs', []) + columns = [] + values = [] + for output in outputs: + columns.append(output['output_key']) + values.append(heat_utils.json_formatter(output)) + + return columns, values + + try: + output = client.stacks.output_show(parsed_args.stack, + parsed_args.output)['output'] + 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) + + if 'output_error' in output: + msg = _('Output error: %s') % output['output_error'] + raise exc.CommandError(msg) + + if (isinstance(output['output_value'], list) or + isinstance(output['output_value'], dict)): + output['output_value'] = heat_utils.json_formatter( + output['output_value']) + + return self.dict2columns(output) diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index d52157ec..cb339775 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -638,3 +638,93 @@ def test_stack_abandon_output_file_error(self, mock_open): parsed_args = self.check_parser(self.cmd, arglist, []) self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + +class TestStackOutputShow(TestStack): + + outputs = [ + {'output_key': 'output1', 'output_value': 'value1'}, + {'output_key': 'output2', 'output_value': 'value2', + 'output_error': 'error'} + ] + + response = { + 'outputs': outputs, + 'stack_name': 'my_stack' + } + + def setUp(self): + super(TestStackOutputShow, self).setUp() + self.cmd = stack.OutputShowStack(self.app, None) + self.stack_client.get = mock.MagicMock( + return_value=stacks.Stack(None, self.response)) + + def test_stack_output_show_no_output(self): + arglist = ['my_stack'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, parsed_args) + self.assertEqual('Either or --all must be specified.', + str(error)) + + def test_stack_output_show_output_and_all(self): + arglist = ['my_stack', 'output1', '--all'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, parsed_args) + self.assertEqual('Cannot specify both and --all.', + str(error)) + + def test_stack_output_show_all(self): + arglist = ['my_stack', '--all'] + 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(['output1', 'output2'], columns) + + def test_stack_output_show_output(self): + arglist = ['my_stack', 'output1'] + self.stack_client.output_show = mock.MagicMock( + return_value={'output': self.outputs[0]}) + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, outputs = self.cmd.take_action(parsed_args) + + self.stack_client.output_show.assert_called_with('my_stack', 'output1') + self.assertEqual(('output_key', 'output_value'), columns) + self.assertEqual(('output1', 'value1'), outputs) + + def test_stack_output_show_not_found(self): + arglist = ['my_stack', '--all'] + 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_show_output_error(self): + arglist = ['my_stack', 'output2'] + self.stack_client.output_show = mock.MagicMock( + return_value={'output': self.outputs[1]}) + parsed_args = self.check_parser(self.cmd, arglist, []) + + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, parsed_args) + self.assertEqual('Output error: error', str(error)) + self.stack_client.output_show.assert_called_with('my_stack', 'output2') + + def test_stack_output_show_bad_output(self): + arglist = ['my_stack', 'output3'] + self.stack_client.output_show.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 my_stack or output output3 not found.', + str(error)) + self.stack_client.output_show.assert_called_with('my_stack', 'output3') diff --git a/setup.cfg b/setup.cfg index 7153464c..5f77f1aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ openstack.orchestration.v1 = stack_adopt = heatclient.osc.v1.stack:AdoptStack stack_create = heatclient.osc.v1.stack:CreateStack stack_list = heatclient.osc.v1.stack:ListStack + stack_output_show = heatclient.osc.v1.stack:OutputShowStack stack_resource_metadata = heatclient.osc.v1.resources:ResourceMetadata stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot From 270d0178f81ce87109adde62d510c493ac1ba9d4 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Wed, 13 Jan 2016 15:32:59 +0000 Subject: [PATCH 131/553] OpenStackClient plugin for stack output list This change implements the 'openstack stack output list' command. Blueprint: heat-support-python-openstackclient Change-Id: I1ca54c47076c7216106fb1c938107cf9016e968d --- heatclient/osc/v1/stack.py | 33 ++++++++++++++++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 29 +++++++++++++++++++ setup.cfg | 1 + 3 files changed, 63 insertions(+) diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 918cd710..dc1c9d35 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -774,3 +774,36 @@ def take_action(self, parsed_args): output['output_value']) return self.dict2columns(output) + + +class OutputListStack(lister.Lister): + """List stack outputs.""" + + log = logging.getLogger(__name__ + '.OutputListStack') + + def get_parser(self, prog_name): + parser = super(OutputListStack, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack to query') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + 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) + + columns = ['output_key', 'description'] + + return ( + columns, + (utils.get_dict_properties(s, columns) for s in outputs) + ) diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index cb339775..8dd30cbc 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -728,3 +728,32 @@ def test_stack_output_show_bad_output(self): self.assertEqual('Stack my_stack or output output3 not found.', str(error)) self.stack_client.output_show.assert_called_with('my_stack', 'output3') + + +class TestStackOutputList(TestStack): + + response = {'outputs': [{'output_key': 'key1', 'description': 'desc1'}, + {'output_key': 'key2', 'description': 'desc2'}]} + + def setUp(self): + super(TestStackOutputList, self).setUp() + self.cmd = stack.OutputListStack(self.app, None) + + def test_stack_output_list(self): + arglist = ['my_stack'] + self.stack_client.output_list.return_value = self.response + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(['output_key', 'description'], columns) + self.stack_client.output_list.assert_called_with('my_stack') + + def test_stack_output_list_not_found(self): + arglist = ['my_stack'] + self.stack_client.output_list.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)) diff --git a/setup.cfg b/setup.cfg index 5f77f1aa..1f6fbb00 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ openstack.orchestration.v1 = stack_adopt = heatclient.osc.v1.stack:AdoptStack stack_create = heatclient.osc.v1.stack:CreateStack stack_list = heatclient.osc.v1.stack:ListStack + stack_output_list = heatclient.osc.v1.stack:OutputListStack stack_output_show = heatclient.osc.v1.stack:OutputShowStack stack_resource_metadata = heatclient.osc.v1.resources:ResourceMetadata stack_show = heatclient.osc.v1.stack:ShowStack From f6373978e5858a19ecf35cf3b73a506601bfe6a2 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Thu, 14 Jan 2016 16:43:58 +0000 Subject: [PATCH 132/553] OpenstackClient plugin for template show This change implements the 'openstack stack template show' command. Blueprint: heat-support-python-openstackclient Change-Id: I4a832435e692fb92a169d01a9e95c5c083c49531 --- heatclient/osc/v1/stack.py | 28 ++++++++ heatclient/tests/inline_templates.py | 78 ++++++++++++++++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 45 +++++++++++++ setup.cfg | 1 + 4 files changed, 152 insertions(+) create mode 100644 heatclient/tests/inline_templates.py diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index dc1c9d35..c0b988cf 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -807,3 +807,31 @@ def take_action(self, parsed_args): columns, (utils.get_dict_properties(s, columns) for s in outputs) ) + + +class TemplateShowStack(format_utils.YamlFormat): + """Display stack template.""" + + log = logging.getLogger(__name__ + '.TemplateShowStack') + + def get_parser(self, prog_name): + parser = super(TemplateShowStack, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack to query') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + try: + template = client.stacks.template(stack_id=parsed_args.stack) + except heat_exc.HTTPNotFound: + msg = _('Stack not found: %s') % parsed_args.stack + raise exc.CommandError(msg) + + return self.dict2columns(template) diff --git a/heatclient/tests/inline_templates.py b/heatclient/tests/inline_templates.py new file mode 100644 index 00000000..56667ac0 --- /dev/null +++ b/heatclient/tests/inline_templates.py @@ -0,0 +1,78 @@ +# +# Copyright 2016 IBM Corp. +# +# 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. + +FULL_TEMPLATE = ''' +heat_template_version: 2016-04-08 + +description: a template + +parameter_groups: + - label: param_group_1 + description: parameter group 1 + parameters: + - param1 + - param2 + - label: param_group_2 + description: parameter group 2 + parameters: + - param3 + +parameters: + param1: + type: string + label: parameter 5 + description: parameter 5 + default: foo + hidden: false + constraints: + - allowed_values: ['foo', 'bar', 'bax'] + param2: + type: number + default: 0 + constraints: + - range: {min: 0, max: 10} + description: must be betwen 0 and 10 + param3: + type: boolean + +resources: + resource1: + type: OS::Heat::None + properties: + prop1: { get_param: param1 } + prop2: { get_param: param2 } + prop3: value + resource2: + type: OS::Heat::None + properties: + prop1: { get_param: param3 } + depends_on: resource1 + +outputs: + output1: + description: resource 1 prop 3 + value: { get_attr: [resource1, prop3] } + output2: + description: resource 2 prop 1 + value: { get_attr: [resource2, prop1] } +''' + +SHORT_TEMPLATE = ''' +heat_template_version: 2016-04-08 + +resources: + res1: + type: OS::Heat::None +''' diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index 8dd30cbc..f74b800d 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -16,12 +16,15 @@ import mock import six import testscenarios +import yaml from openstackclient.common import exceptions as exc from openstackclient.common import utils +from heatclient.common import template_format from heatclient import exc as heat_exc from heatclient.osc.v1 import stack +from heatclient.tests import inline_templates from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes from heatclient.v1 import stacks @@ -757,3 +760,45 @@ def test_stack_output_list_not_found(self): error = self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) self.assertEqual('Stack not found: my_stack', str(error)) + + +class TestStackTemplateShow(TestStack): + + fields = ['heat_template_version', 'description', 'parameter_groups', + 'parameters', 'resources', 'outputs'] + + def setUp(self): + super(TestStackTemplateShow, self).setUp() + self.cmd = stack.TemplateShowStack(self.app, None) + + def test_stack_template_show_full_template(self): + arglist = ['my_stack'] + self.stack_client.template = mock.MagicMock( + return_value=yaml.load(inline_templates.FULL_TEMPLATE, + Loader=template_format.yaml_loader)) + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, outputs = self.cmd.take_action(parsed_args) + + for f in self.fields: + self.assertIn(f, columns) + + def test_stack_template_show_short_template(self): + arglist = ['my_stack'] + self.stack_client.template = mock.MagicMock( + return_value=yaml.load(inline_templates.SHORT_TEMPLATE, + Loader=template_format.yaml_loader)) + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, outputs = self.cmd.take_action(parsed_args) + + for f in ['heat_template_version', 'resources']: + self.assertIn(f, columns) + + def test_stack_template_show_not_found(self): + arglist = ['my_stack'] + self.stack_client.template = mock.MagicMock( + side_effect=heat_exc.HTTPNotFound) + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) diff --git a/setup.cfg b/setup.cfg index 1f6fbb00..bdb82d2b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,7 @@ openstack.orchestration.v1 = stack_resource_metadata = heatclient.osc.v1.resources:ResourceMetadata stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot + stack_template_show = heatclient.osc.v1.stack:TemplateShowStack stack_update = heatclient.osc.v1.stack:UpdateStack [global] From daa8abeb229f86377c8c541b499d9aab4b7e81aa Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Thu, 21 Jan 2016 10:19:34 -0600 Subject: [PATCH 133/553] Add openstack client software config create Add the openstack software config create command. orchestration resource type and show Based from the existing heat commands: heat config-create Change-Id: I2b29653c2780ea1dbf00295aa5684c884013fed5 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/software_config.py | 85 +++++++++++++ .../tests/unit/osc/v1/test_sotware_config.py | 118 ++++++++++++++++++ setup.cfg | 1 + 3 files changed, 204 insertions(+) diff --git a/heatclient/osc/v1/software_config.py b/heatclient/osc/v1/software_config.py index c0ea931e..f25f326a 100644 --- a/heatclient/osc/v1/software_config.py +++ b/heatclient/osc/v1/software_config.py @@ -14,12 +14,19 @@ """Orchestration v1 software config action implementations""" import logging +import six + +from six.moves.urllib import request +import yaml from cliff import command from cliff import lister from openstackclient.common import exceptions as exc from openstackclient.common import utils +from heatclient.common import format_utils +from heatclient.common import template_format +from heatclient.common import utils as heat_utils from heatclient import exc as heat_exc from heatclient.openstack.common._i18n import _ @@ -101,3 +108,81 @@ def _list_config(heat_client, args): columns = ['id', 'name', 'group', 'creation_time'] return (columns, (utils.get_item_properties(s, columns) for s in scs)) + + +class CreateConfig(format_utils.JsonFormat): + """Create software config""" + + log = logging.getLogger(__name__ + ".CreateConfig") + + def get_parser(self, prog_name): + parser = super(CreateConfig, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the software config to create') + ) + parser.add_argument( + '--config-file', + metavar='', + help=_('Path to JSON/YAML containing map defining ' + ', , and ') + ) + parser.add_argument( + '--definition-file', + metavar='', + help=_('Path to software config script/data') + ) + parser.add_argument( + '--group', + metavar='', + default='Heat::Ungrouped', + help=_('Group name of tool expected by the software config') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + heat_client = self.app.client_manager.orchestration + return _create_config(heat_client, parsed_args) + + +def _create_config(heat_client, args): + config = { + 'group': args.group, + 'config': '' + } + + defn = {} + if args.definition_file: + defn_url = heat_utils.normalise_file_path_to_url( + args.definition_file) + defn_raw = request.urlopen(defn_url).read() or '{}' + defn = yaml.load(defn_raw, Loader=template_format.yaml_loader) + + config['inputs'] = defn.get('inputs', []) + config['outputs'] = defn.get('outputs', []) + config['options'] = defn.get('options', {}) + + if args.config_file: + config_url = heat_utils.normalise_file_path_to_url( + args.config_file) + config['config'] = request.urlopen(config_url).read() + + # build a mini-template with a config resource and validate it + validate_template = { + 'heat_template_version': '2013-05-23', + 'resources': { + args.name: { + 'type': 'OS::Heat::SoftwareConfig', + 'properties': config + } + } + } + heat_client.stacks.validate(template=validate_template) + + config['name'] = args.name + sc = heat_client.software_configs.create(**config).to_dict() + rows = list(six.itervalues(sc)) + columns = list(six.iterkeys(sc)) + return columns, rows diff --git a/heatclient/tests/unit/osc/v1/test_sotware_config.py b/heatclient/tests/unit/osc/v1/test_sotware_config.py index 00260fa7..02577477 100644 --- a/heatclient/tests/unit/osc/v1/test_sotware_config.py +++ b/heatclient/tests/unit/osc/v1/test_sotware_config.py @@ -12,6 +12,7 @@ # import mock +import yaml from openstackclient.common import exceptions as exc @@ -91,3 +92,120 @@ def test_config_list_marker(self): self.cmd.take_action(parsed_args) self.mock_client.software_configs.list.assert_called_with( marker='id123') + + +class TestCreateConfig(TestConfig): + + def setUp(self): + super(TestCreateConfig, self).setUp() + self.cmd = software_config.CreateConfig(self.app, None) + self.mock_client.stacks.validate = mock.Mock() + self.mock_client.software_configs.create = mock.Mock( + return_value=software_configs.SoftwareConfig(None, {})) + + def test_config_create(self): + properties = { + 'config': '', + 'group': 'Heat::Ungrouped', + 'name': 'test', + 'options': {}, + 'inputs': [], + 'outputs': [] + } + arglist = ['test'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + self.mock_client.stacks.validate.assert_called_with(**{ + 'template': { + 'heat_template_version': '2013-05-23', + 'resources': { + 'test': { + 'type': 'OS::Heat::SoftwareConfig', + 'properties': properties}}}}) + self.mock_client.software_configs.create.assert_called_with( + **properties) + + def test_config_create_group(self): + properties = { + 'config': '', + 'group': 'group', + 'name': 'test', + 'options': {}, + 'inputs': [], + 'outputs': [] + } + arglist = ['test', '--group', 'group'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + self.mock_client.stacks.validate.assert_called_with(**{ + 'template': { + 'heat_template_version': '2013-05-23', + 'resources': { + 'test': { + 'type': 'OS::Heat::SoftwareConfig', + 'properties': properties}}}}) + self.mock_client.software_configs.create.assert_called_with( + **properties) + + @mock.patch('six.moves.urllib.request.urlopen') + def test_config_create_config_file(self, urlopen): + properties = { + 'config': 'config', + 'group': 'Heat::Ungrouped', + 'name': 'test', + 'options': {}, + 'inputs': [], + 'outputs': [] + } + data = mock.Mock() + data.read.side_effect = ['config'] + urlopen.return_value = data + + arglist = ['test', '--config-file', 'config_file'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + self.mock_client.stacks.validate.assert_called_with(**{ + 'template': { + 'heat_template_version': '2013-05-23', + 'resources': { + 'test': { + 'type': 'OS::Heat::SoftwareConfig', + 'properties': properties}}}}) + self.mock_client.software_configs.create.assert_called_with( + **properties) + + @mock.patch('six.moves.urllib.request.urlopen') + def test_config_create_definition_file(self, urlopen): + definition = { + 'inputs': [ + {'name': 'input'}, + ], + 'outputs': [ + {'name': 'output'} + ], + 'options': {'option': 'value'} + } + + properties = { + 'config': '', + 'group': 'Heat::Ungrouped', + 'name': 'test' + } + properties.update(definition) + + data = mock.Mock() + data.read.side_effect = [yaml.safe_dump(definition)] + urlopen.return_value = data + + arglist = ['test', '--definition-file', 'definition-file'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + self.mock_client.stacks.validate.assert_called_with(**{ + 'template': { + 'heat_template_version': '2013-05-23', + 'resources': { + 'test': { + 'type': 'OS::Heat::SoftwareConfig', + 'properties': properties}}}}) + self.mock_client.software_configs.create.assert_called_with( + **properties) diff --git a/setup.cfg b/setup.cfg index 7153464c..2c1725c1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ openstack.cli.extension = openstack.orchestration.v1 = orchestration_template_function_list = heatclient.osc.v1.template:FunctionList orchestration_template_version_list = heatclient.osc.v1.template:VersionList + software_config_create = heatclient.osc.v1.software_config:CreateConfig software_config_delete = heatclient.osc.v1.software_config:DeleteConfig software_config_list = heatclient.osc.v1.software_config:ListConfig software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment From a364a8c51d84a1261e5b14f077bbad935e01973c Mon Sep 17 00:00:00 2001 From: Amey Bhide Date: Sun, 8 Nov 2015 15:48:01 -0800 Subject: [PATCH 134/553] OSC plugin for stack snapshot show This change implements "openstack stack snapshot show" command Based from the existing heat commands: heat snapshot-show Blueprint: heat-support-python-openstackclient Change-Id: I2f9895c59fb129fd5b13ddc4e24148760faf4e79 --- heatclient/osc/v1/snapshot.py | 43 ++++++++++++++++++- heatclient/tests/unit/osc/v1/test_snapshot.py | 25 +++++++++++ setup.cfg | 1 + 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/heatclient/osc/v1/snapshot.py b/heatclient/osc/v1/snapshot.py index d75248dd..5e891810 100644 --- a/heatclient/osc/v1/snapshot.py +++ b/heatclient/osc/v1/snapshot.py @@ -11,14 +11,16 @@ # under the License. # -"""Orchestration v1 Stack Snapshot implementations""" +"""Orchestration v1 Stack Snapshot implementations.""" import logging +import six from cliff import lister from openstackclient.common import exceptions as exc from openstackclient.common import utils +from heatclient.common import format_utils from heatclient import exc as heat_exc from openstackclient.i18n import _ @@ -56,3 +58,42 @@ def _list_snapshot(self, heat_client, parsed_args): (utils.get_dict_properties(s, columns) for s in snapshots['snapshots']) ) + + +class ShowSnapshot(format_utils.YamlFormat): + """Show stack snapshot.""" + + log = logging.getLogger(__name__ + ".ShowSnapshot") + + def get_parser(self, prog_name): + parser = super(ShowSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack containing the snapshot') + ) + parser.add_argument( + 'snapshot', + metavar='', + help=_('ID of the snapshot to show') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + heat_client = self.app.client_manager.orchestration + return self._show_snapshot(heat_client, parsed_args.stack, + parsed_args.snapshot) + + def _show_snapshot(self, heat_client, stack_id, snapshot_id): + try: + data = heat_client.stacks.snapshot_show(stack_id, snapshot_id) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Snapshot ID <%(snapshot_id)s> not found ' + 'for stack <%(stack_id)s>') + % {'snapshot_id': snapshot_id, + 'stack_id': stack_id}) + + rows = list(six.itervalues(data)) + columns = list(six.iterkeys(data)) + return columns, rows diff --git a/heatclient/tests/unit/osc/v1/test_snapshot.py b/heatclient/tests/unit/osc/v1/test_snapshot.py index 3c242002..88513c78 100644 --- a/heatclient/tests/unit/osc/v1/test_snapshot.py +++ b/heatclient/tests/unit/osc/v1/test_snapshot.py @@ -52,3 +52,28 @@ def test_snapshot_list_error(self): parsed_args) self.assertEqual('Stack not found: my_stack', str(error)) + + +class TestSnapshotShow(TestStack): + def setUp(self): + super(TestSnapshotShow, self).setUp() + self.cmd = snapshot.ShowSnapshot(self.app, None) + + def test_snapshot_show(self): + arglist = ['my_stack', 'snapshot_id'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.stack_client.snapshot_show = mock.Mock( + return_value={}) + self.cmd.take_action(parsed_args) + self.stack_client.snapshot_show.assert_called_with( + 'my_stack', 'snapshot_id') + + def test_snapshot_not_found(self): + arglist = ['my_stack', 'snapshot_id'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.stack_client.snapshot_show = mock.Mock( + side_effect=heat_exc.HTTPNotFound()) + self.assertRaises( + exc.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index 4aa20bb2..6ed411fa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ openstack.orchestration.v1 = stack_resource_metadata = heatclient.osc.v1.resources:ResourceMetadata stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot + stack_snapshot_show = heatclient.osc.v1.snapshot:ShowSnapshot stack_template_show = heatclient.osc.v1.stack:TemplateShowStack stack_update = heatclient.osc.v1.stack:UpdateStack From 410e55234da83324494d7090e1ca01d29385ec95 Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Thu, 3 Dec 2015 15:51:11 -0600 Subject: [PATCH 135/553] Add openstack client resource type list and show Add the openstack orchestration resource type and show commands. Included support for the optional generation of the output in hot or cfn formats. Based from the existing heat commands: heat resource-type-list heat resource-type-show heat resource-type-template Change-Id: I416364904b1f952836cee27c2876e9b1a5d30a45 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/resource_type.py | 107 ++++++++++++ .../tests/unit/osc/v1/test_resource_type.py | 153 ++++++++++++++++++ setup.cfg | 2 + 3 files changed, 262 insertions(+) create mode 100644 heatclient/osc/v1/resource_type.py create mode 100644 heatclient/tests/unit/osc/v1/test_resource_type.py diff --git a/heatclient/osc/v1/resource_type.py b/heatclient/osc/v1/resource_type.py new file mode 100644 index 00000000..1d85345c --- /dev/null +++ b/heatclient/osc/v1/resource_type.py @@ -0,0 +1,107 @@ +# 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. +# + +"""Orchestration v1 resource type implementations""" + +import logging +import six + +from cliff import lister +from heatclient.common import format_utils +from heatclient.common import utils as heat_utils +from heatclient import exc as heat_exc +from openstackclient.common import exceptions as exc +from openstackclient.i18n import _ + + +class ResourceTypeShow(format_utils.YamlFormat): + """Show details and optionally generate a template for a resource type.""" + + log = logging.getLogger(__name__ + ".ResourceTypeShow") + + def get_parser(self, prog_name): + parser = super(ResourceTypeShow, + self).get_parser(prog_name) + parser.add_argument( + 'resource_type', + metavar='', + help=_('Resource type to show details for'), + ) + parser.add_argument( + '--template-type', + metavar='', + help=_('Optional template type to generate, hot or cfn') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + return _show_resourcetype(heat_client, parsed_args) + + +def _show_resourcetype(heat_client, parsed_args): + try: + if parsed_args.template_type: + template_type = parsed_args.template_type.lower() + if template_type not in ('hot', 'cfn'): + raise exc.CommandError( + _('Template type invalid: %s') % parsed_args.template_type) + + fields = {'resource_type': parsed_args.resource_type, + 'template_type': template_type} + data = heat_client.resource_types.generate_template(**fields) + else: + data = heat_client.resource_types.get(parsed_args.resource_type) + except heat_exc.HTTPNotFound: + raise exc.CommandError( + _('Resource type not found: %s') % parsed_args.resource_type) + + rows = list(six.itervalues(data)) + columns = list(six.iterkeys(data)) + return columns, rows + + +class ResourceTypeList(lister.Lister): + """List resource types.""" + + log = logging.getLogger(__name__ + '.ResourceTypeList') + + def get_parser(self, prog_name): + parser = super(ResourceTypeList, + self).get_parser(prog_name) + parser.add_argument( + '--filter', + dest='filter', + metavar='', + help=_('Filter parameters to apply on returned resource types. ' + 'This can be specified multiple times. It can be any of ' + 'name, version or support_status'), + action='append' + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + return _list_resourcetypes(heat_client, parsed_args) + + +def _list_resourcetypes(heat_client, parsed_args): + resource_types = heat_client.resource_types.list( + filters=heat_utils.format_parameters(parsed_args.filter)) + columns = ['Resource Type'] + rows = sorted([r.resource_type] for r in resource_types) + return (columns, rows) diff --git a/heatclient/tests/unit/osc/v1/test_resource_type.py b/heatclient/tests/unit/osc/v1/test_resource_type.py new file mode 100644 index 00000000..12c356f3 --- /dev/null +++ b/heatclient/tests/unit/osc/v1/test_resource_type.py @@ -0,0 +1,153 @@ +# 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 mock + +from openstackclient.common import exceptions as exc + +from heatclient import exc as heat_exc +from heatclient.osc.v1 import resource_type +from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes +from heatclient.v1 import resource_types + + +class TestResourceType(orchestration_fakes.TestOrchestrationv1): + def setUp(self): + super(TestResourceType, self).setUp() + self.mock_client = self.app.client_manager.orchestration + + +class TestResourceTypeShow(TestResourceType): + + def setUp(self): + super(TestResourceTypeShow, self).setUp() + self.cmd = resource_type.ResourceTypeShow(self.app, None) + self.mock_client.resource_types.get = mock.Mock( + return_value={}) + self.mock_client.resource_types.generate_template = mock.Mock( + return_value={}) + + def test_resourcetype_show(self): + arglist = ['OS::Heat::None'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.resource_types.get.assert_called_once_with( + 'OS::Heat::None') + + def test_resourcetype_show_json(self): + arglist = ['OS::Heat::None', + '--format', 'json'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.resource_types.get.assert_called_once_with( + 'OS::Heat::None') + + def test_resourcetype_show_error_get(self): + arglist = ['OS::Heat::None'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.resource_types.get = mock.Mock( + side_effect=heat_exc.HTTPNotFound) + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + def test_resourcetype_show_error_template(self): + arglist = ['OS::Heat::None', + '--template-type', 'hot'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.resource_types.generate_template = mock.Mock( + side_effect=heat_exc.HTTPNotFound) + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + def test_resourcetype_show_template_hot(self): + arglist = ['OS::Heat::None', + '--template-type', 'Hot'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.resource_types.generate_template.assert_called_with( + **{'resource_type': 'OS::Heat::None', + 'template_type': 'hot'}) + + def test_resourcetype_show_template_cfn(self): + arglist = ['OS::Heat::None', + '--template-type', 'cfn'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.resource_types.generate_template.assert_called_with( + **{'resource_type': 'OS::Heat::None', + 'template_type': 'cfn'}) + + def test_resourcetype_show_template_cfn_yaml(self): + arglist = ['OS::Heat::None', + '--template-type', 'Cfn', + '--format', 'yaml'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.resource_types.generate_template.assert_called_with( + **{'resource_type': 'OS::Heat::None', + 'template_type': 'cfn'}) + + def test_resourcetype_show_invalid_template_type(self): + arglist = ['OS::Heat::None', + '--template-type', 'abc'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + +class TestTypeList(TestResourceType): + + expected_columns = ['Resource Type'] + list_response = [ + resource_types.ResourceType(None, 'BBB'), + resource_types.ResourceType(None, 'AAA'), + resource_types.ResourceType(None, 'CCC') + ] + expected_rows = [ + ['AAA'], + ['BBB'], + ['CCC'] + ] + + def setUp(self): + super(TestTypeList, self).setUp() + self.cmd = resource_type.ResourceTypeList(self.app, None) + self.mock_client.resource_types.list = mock.Mock( + return_value=self.list_response) + + def test_resourcetype_list(self): + arglist = [] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + + self.mock_client.resource_types.list.assert_called_with( + filters={}) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_rows, rows) + + def test_resourcetype_list_filter(self): + arglist = ['--filter', 'name=B'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + + self.mock_client.resource_types.list.assert_called_once_with( + filters={'name': 'B'}) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_rows, rows) + + def test_resourcetype_list_filters(self): + arglist = ['--filter', 'name=B', '--filter', 'version=123'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + + self.mock_client.resource_types.list.assert_called_once_with( + filters={'name': 'B', 'version': '123'}) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_rows, rows) diff --git a/setup.cfg b/setup.cfg index 4aa20bb2..2a8680dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,8 @@ openstack.cli.extension = openstack.orchestration.v1 = orchestration_template_function_list = heatclient.osc.v1.template:FunctionList orchestration_template_version_list = heatclient.osc.v1.template:VersionList + orchestration_resource_type_list = heatclient.osc.v1.orchestration:OrchestrationResourceTypeList + orchestration_resource_type_show = heatclient.osc.v1.orchestration:OrchestrationResourceTypeShow software_config_create = heatclient.osc.v1.software_config:CreateConfig software_config_delete = heatclient.osc.v1.software_config:DeleteConfig software_config_list = heatclient.osc.v1.software_config:ListConfig From 9a7ed34aafc008f8880ba6141fd0dcac87244b89 Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Thu, 19 Nov 2015 15:42:27 -0600 Subject: [PATCH 136/553] Add openstack cli stack actions Add suspend, resume, update_cancel and check Allow multiple stack id/names Allow the --wait option Bumped required openstack client version to 2.0.0 to pickup utils.wait_for_status enhancement for heat error status. based upon heat clis: heat stack-cancel-update heat action-suspend heat action-resume heat action-check Change-Id: I109bd155f081087c0fe0cbd8f6601d6ca3a4534e Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/stack.py | 146 +++++++++++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 176 +++++++++++++++++++++ setup.cfg | 4 + 3 files changed, 326 insertions(+) diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index c0b988cf..d71f5f74 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -835,3 +835,149 @@ def take_action(self, parsed_args): raise exc.CommandError(msg) return self.dict2columns(template) + + +class StackActionBase(lister.Lister): + """Stack actions base.""" + + log = logging.getLogger(__name__ + '.StackActionBase') + + def _get_parser(self, prog_name, stack_help, wait_help): + parser = super(StackActionBase, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + nargs="+", + help=stack_help + ) + parser.add_argument( + '--wait', + action='store_true', + help=wait_help + ) + return parser + + def _take_action(self, parsed_args, action, good_status, bad_status): + self.log.debug("take_action(%s)", parsed_args) + heat_client = self.app.client_manager.orchestration + return _stack_action( + parsed_args, + heat_client, + action, + good_status, + bad_status + ) + + +def _stack_action(parsed_args, heat_client, action, good_status, bad_status): + rows = [] + for stack in parsed_args.stack: + try: + action(stack) + except heat_exc.HTTPNotFound: + msg = _('Stack not found: %s') % stack + raise exc.CommandError(msg) + + if parsed_args.wait: + if not utils.wait_for_status(heat_client.stacks.get, stack, + status_field='stack_status', + success_status=good_status, + error_status=bad_status): + err = _("Error waiting for status from stack %s") % stack + raise exc.CommandError(err) + + data = heat_client.stacks.get(stack) + columns = [ + 'ID', + 'Stack Name', + 'Stack Status', + 'Creation Time', + 'Updated Time' + ] + rows += [utils.get_dict_properties(data.to_dict(), columns)] + + return (columns, rows) + + +class SuspendStack(StackActionBase): + """Suspend a stack.""" + + log = logging.getLogger(__name__ + '.SuspendStack') + + def get_parser(self, prog_name): + return self._get_parser( + prog_name, + _('Stack(s) to suspend (name or ID)'), + _('Wait for suspend to complete') + ) + + def take_action(self, parsed_args): + return self._take_action( + parsed_args, + self.app.client_manager.orchestration.actions.suspend, + ['suspend_complete'], + ['suspend_failed'] + ) + + +class ResumeStack(StackActionBase): + """Resume a stack.""" + + log = logging.getLogger(__name__ + '.ResumeStack') + + def get_parser(self, prog_name): + return self._get_parser( + prog_name, + _('Stack(s) to resume (name or ID)'), + _('Wait for resume to complete') + ) + + def take_action(self, parsed_args): + return self._take_action( + parsed_args, + self.app.client_manager.orchestration.actions.resume, + ['resume_complete'], + ['resume_failed'] + ) + + +class UpdateCancelStack(StackActionBase): + """Cancel update for a stack.""" + + log = logging.getLogger(__name__ + '.UpdateCancelStack') + + def get_parser(self, prog_name): + return self._get_parser( + prog_name, + _('Stack(s) to cancel update (name or ID)'), + _('Wait for cancel update to complete') + ) + + def take_action(self, parsed_args): + return self._take_action( + parsed_args, + self.app.client_manager.orchestration.actions.cancel_update, + ['cancel_update_complete'], + ['cancel_update_failed'] + ) + + +class CheckStack(StackActionBase): + """Check a stack.""" + + log = logging.getLogger(__name__ + '.CheckStack') + + def get_parser(self, prog_name): + return self._get_parser( + prog_name, + _('Stack(s) to check update (name or ID)'), + _('Wait for check to complete') + ) + + def take_action(self, parsed_args): + return self._take_action( + parsed_args, + self.app.client_manager.orchestration.actions.check, + ['check_complete'], + ['check_failed'] + ) diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index f74b800d..161de585 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -802,3 +802,179 @@ def test_stack_template_show_not_found(self): parsed_args = self.check_parser(self.cmd, arglist, []) self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + +class _TestStackCheckBase(object): + + stack = stacks.Stack(None, { + "id": '1234', + "stack_name": 'my_stack', + "creation_time": "2013-08-04T20:57:55Z", + "updated_time": "2013-08-04T20:57:55Z", + "stack_status": "CREATE_COMPLETE" + }) + + columns = ['ID', 'Stack Name', 'Stack Status', 'Creation Time', + 'Updated Time'] + + def _setUp(self, cmd, action): + self.cmd = cmd + self.action = action + self.mock_client.stacks.get = mock.Mock( + return_value=self.stack) + + def _test_stack_action(self): + arglist = ['my_stack'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + self.action.assert_called_once_with('my_stack') + self.mock_client.stacks.get.assert_called_once_with('my_stack') + self.assertEqual(self.columns, columns) + self.assertEqual(1, len(rows)) + + def _test_stack_action_multi(self): + arglist = ['my_stack1', 'my_stack2'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + self.assertEqual(2, self.action.call_count) + self.assertEqual(2, self.mock_client.stacks.get.call_count) + self.action.assert_called_with('my_stack2') + self.mock_client.stacks.get.assert_called_with('my_stack2') + self.assertEqual(self.columns, columns) + self.assertEqual(2, len(rows)) + + @mock.patch('openstackclient.common.utils.wait_for_status', + return_value=True) + def _test_stack_action_wait(self, mock_wait): + arglist = ['my_stack', '--wait'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + self.action.assert_called_with('my_stack') + self.mock_client.stacks.get.assert_called_once_with('my_stack') + self.assertEqual(self.columns, columns) + self.assertEqual(1, len(rows)) + + @mock.patch('openstackclient.common.utils.wait_for_status', + return_value=False) + def _test_stack_action_wait_error(self, mock_wait): + arglist = ['my_stack', '--wait'] + parsed_args = self.check_parser(self.cmd, arglist, []) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertEqual('Error waiting for status from stack my_stack', + str(error)) + + def _test_stack_action_exception(self): + self.action.side_effect = heat_exc.HTTPNotFound + arglist = ['my_stack'] + 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)) + + +class TestStackSuspend(_TestStackCheckBase, TestStack): + + def setUp(self): + super(TestStackSuspend, self).setUp() + self.mock_client.actions.suspend = mock.Mock() + self._setUp( + stack.SuspendStack(self.app, None), + self.mock_client.actions.suspend + ) + + def test_stack_suspend(self): + self._test_stack_action() + + def test_stack_suspend_multi(self): + self._test_stack_action_multi() + + def test_stack_suspend_wait(self): + self._test_stack_action_wait() + + def test_stack_suspend_wait_error(self): + self._test_stack_action_wait_error() + + def test_stack_suspend_exception(self): + self._test_stack_action_exception() + + +class TestStackResume(_TestStackCheckBase, TestStack): + + def setUp(self): + super(TestStackResume, self).setUp() + self.mock_client.actions.resume = mock.Mock() + self._setUp( + stack.ResumeStack(self.app, None), + self.mock_client.actions.resume + ) + + def test_stack_resume(self): + self._test_stack_action() + + def test_stack_resume_multi(self): + self._test_stack_action_multi() + + def test_stack_resume_wait(self): + self._test_stack_action_wait() + + def test_stack_resume_wait_error(self): + self._test_stack_action_wait_error() + + def test_stack_resume_exception(self): + self._test_stack_action_exception() + + +class TestStackUpdateCancel(_TestStackCheckBase, TestStack): + + def setUp(self): + super(TestStackUpdateCancel, self).setUp() + self.mock_client.actions.cancel_update = mock.Mock() + self._setUp( + stack.UpdateCancelStack(self.app, None), + self.mock_client.actions.cancel_update + ) + + def test_stack_cancel_update(self): + self._test_stack_action() + + def test_stack_cancel_update_multi(self): + self._test_stack_action_multi() + + def test_stack_cancel_update_wait(self): + self._test_stack_action_wait() + + def test_stack_cancel_update_wait_error(self): + self._test_stack_action_wait_error() + + def test_stack_cancel_update_exception(self): + self._test_stack_action_exception() + + +class TestStackCheck(_TestStackCheckBase, TestStack): + + def setUp(self): + super(TestStackCheck, self).setUp() + self.mock_client.actions.check = mock.Mock() + self._setUp( + stack.CheckStack(self.app, None), + self.mock_client.actions.check + ) + + def test_stack_check(self): + self._test_stack_action() + + def test_stack_check_multi(self): + self._test_stack_action_multi() + + def test_stack_check_wait(self): + self._test_stack_action_wait() + + def test_stack_check_wait_error(self): + self._test_stack_action_wait_error() + + def test_stack_check_exception(self): + self._test_stack_action_exception() diff --git a/setup.cfg b/setup.cfg index 6ed411fa..95584a31 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,16 +39,20 @@ openstack.orchestration.v1 = software_deployment_list = heatclient.osc.v1.software_deployment:ListDeployment stack_abandon = heatclient.osc.v1.stack:AbandonStack stack_adopt = heatclient.osc.v1.stack:AdoptStack + stack_check = heatclient.osc.v1.stack:CheckStack stack_create = heatclient.osc.v1.stack:CreateStack stack_list = heatclient.osc.v1.stack:ListStack stack_output_list = heatclient.osc.v1.stack:OutputListStack stack_output_show = heatclient.osc.v1.stack:OutputShowStack stack_resource_metadata = heatclient.osc.v1.resources:ResourceMetadata + stack_resume = heatclient.osc.v1.stack:ResumeStack stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot stack_snapshot_show = heatclient.osc.v1.snapshot:ShowSnapshot + stack_suspend = heatclient.osc.v1.stack:SuspendStack stack_template_show = heatclient.osc.v1.stack:TemplateShowStack stack_update = heatclient.osc.v1.stack:UpdateStack + stack_update_cancel = heatclient.osc.v1.stack:UpdateCancelStack [global] setup-hooks = From 99fca91c3d63ec5883cc03b11bb9bd9bab2fee0c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 11 Feb 2016 07:44:19 +0000 Subject: [PATCH 137/553] Updated from global requirements Change-Id: I2b1e82abe29c6f22338ef3edf9beac41db8a9a43 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6d72672d..4c3700e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ Babel>=1.3 # BSD pbr>=1.6 # Apache-2.0 -cliff>=1.15.0 # Apache-2.0 +cliff!=1.16.0,>=1.15.0 # Apache-2.0 iso8601>=0.1.9 # MIT PrettyTable<0.8,>=0.7 # BSD oslo.i18n>=2.1.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 5112cddd..4eba3eb2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ mox3>=0.7.0 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD -tempest-lib>=0.13.0 # Apache-2.0 +tempest-lib>=0.14.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT From 0ea973243327d3c45b3e5bf3396ece7a7731248a Mon Sep 17 00:00:00 2001 From: zengyingzhe Date: Fri, 15 Jan 2016 17:40:19 +0800 Subject: [PATCH 138/553] OSC plugin for software config show This change implements "openstack software config show" command Blueprint: heat-support-python-openstackclient Change-Id: I851662206b71d9912eea197f6c82bbd3723bdeba --- heatclient/osc/v1/software_config.py | 54 ++++++++++++++++ ...ware_config.py => test_software_config.py} | 63 +++++++++++++++++++ setup.cfg | 1 + 3 files changed, 118 insertions(+) rename heatclient/tests/unit/osc/v1/{test_sotware_config.py => test_software_config.py} (79%) diff --git a/heatclient/osc/v1/software_config.py b/heatclient/osc/v1/software_config.py index f25f326a..facab0e4 100644 --- a/heatclient/osc/v1/software_config.py +++ b/heatclient/osc/v1/software_config.py @@ -186,3 +186,57 @@ def _create_config(heat_client, args): rows = list(six.itervalues(sc)) columns = list(six.iterkeys(sc)) return columns, rows + + +class ShowConfig(format_utils.YamlFormat): + """Show software config details""" + + log = logging.getLogger(__name__ + ".ShowConfig") + + def get_parser(self, prog_name): + parser = super(ShowConfig, self).get_parser(prog_name) + parser.add_argument( + 'id', + metavar='', + help=_('ID of the config') + ) + parser.add_argument( + '--config-only', + default=False, + action="store_true", + help=_('Only display the value of the property.') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + heat_client = self.app.client_manager.orchestration + return _show_config(heat_client, config_id=parsed_args.id, + config_only=parsed_args.config_only) + + +def _show_config(heat_client, config_id, config_only): + try: + sc = heat_client.software_configs.get(config_id=config_id) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Configuration not found: %s') % config_id) + + columns = None + rows = None + + if config_only: + print(sc.config) + else: + columns = ( + 'id', + 'name', + 'group', + 'config', + 'inputs', + 'outputs', + 'options', + 'creation_time', + ) + rows = utils.get_dict_properties(sc.to_dict(), columns) + + return columns, rows diff --git a/heatclient/tests/unit/osc/v1/test_sotware_config.py b/heatclient/tests/unit/osc/v1/test_software_config.py similarity index 79% rename from heatclient/tests/unit/osc/v1/test_sotware_config.py rename to heatclient/tests/unit/osc/v1/test_software_config.py index 02577477..1b4a9a5b 100644 --- a/heatclient/tests/unit/osc/v1/test_sotware_config.py +++ b/heatclient/tests/unit/osc/v1/test_software_config.py @@ -209,3 +209,66 @@ def test_config_create_definition_file(self, urlopen): 'properties': properties}}}}) self.mock_client.software_configs.create.assert_called_with( **properties) + + +class TestConfigShow(TestConfig): + columns = ( + 'id', + 'name', + 'group', + 'config', + 'inputs', + 'outputs', + 'options', + 'creation_time', + ) + + data = ( + '96dfee3f-27b7-42ae-a03e-966226871ae6', + 'test', + 'Heat::Ungrouped', + '', + [], + [], + {}, + '2015-12-09T11:55:06', + ) + + response = dict(zip(columns, data)) + + def setUp(self): + super(TestConfigShow, self).setUp() + self.cmd = software_config.ShowConfig(self.app, None) + self.mock_client.software_configs.get = mock.Mock( + return_value=software_configs.SoftwareConfig(None, + self.response)) + + def test_config_show(self): + arglist = ['96dfee3f-27b7-42ae-a03e-966226871ae6'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.software_configs.get.assert_called_with(**{ + 'config_id': '96dfee3f-27b7-42ae-a03e-966226871ae6', + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_config_show_config_only(self): + arglist = ['--config-only', '96dfee3f-27b7-42ae-a03e-966226871ae6'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.software_configs.get.assert_called_with(**{ + 'config_id': '96dfee3f-27b7-42ae-a03e-966226871ae6', + }) + self.assertEqual(None, columns) + self.assertEqual(None, data) + + def test_config_show_not_found(self): + arglist = ['96dfee3f-27b7-42ae-a03e-966226871ae6'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.software_configs.get = mock.Mock( + side_effect=heat_exc.HTTPNotFound()) + self.assertRaises( + exc.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index 95584a31..c7a26c6c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,7 @@ openstack.orchestration.v1 = software_config_create = heatclient.osc.v1.software_config:CreateConfig software_config_delete = heatclient.osc.v1.software_config:DeleteConfig software_config_list = heatclient.osc.v1.software_config:ListConfig + software_config_show = heatclient.osc.v1.software_config:ShowConfig software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment software_deployment_list = heatclient.osc.v1.software_deployment:ListDeployment stack_abandon = heatclient.osc.v1.stack:AbandonStack From bc6cd2262e514761c7fe1f21d25aebd99e4a3561 Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Fri, 12 Feb 2016 09:04:40 -0600 Subject: [PATCH 139/553] Fix resource_type osc entrypoint With all the rebase churn, I missed updating the entrypoint when the file name changed. Change-Id: Ibd017aacaf0007242da2b9f2f09085401a3f723a --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index b88d0678..93bfa464 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,8 +32,8 @@ openstack.cli.extension = openstack.orchestration.v1 = orchestration_template_function_list = heatclient.osc.v1.template:FunctionList orchestration_template_version_list = heatclient.osc.v1.template:VersionList - orchestration_resource_type_list = heatclient.osc.v1.orchestration:OrchestrationResourceTypeList - orchestration_resource_type_show = heatclient.osc.v1.orchestration:OrchestrationResourceTypeShow + orchestration_resource_type_list = heatclient.osc.v1.resource_type:ResourceTypeList + orchestration_resource_type_show = heatclient.osc.v1.resource_type:ResourceTypeShow software_config_create = heatclient.osc.v1.software_config:CreateConfig software_config_delete = heatclient.osc.v1.software_config:DeleteConfig software_config_list = heatclient.osc.v1.software_config:ListConfig From b696c52554b54cbe4cb74c7c45195718fb56014e Mon Sep 17 00:00:00 2001 From: Amey Bhide Date: Thu, 5 Nov 2015 17:54:18 -0800 Subject: [PATCH 140/553] OpenstackClient plugin for stack delete This change implements the 'openstack stack delete' command. Blueprint: heat-support-python-openstackclient Change-Id: I95df1390a9daee7115ccda68b261e0a76530ade4 --- heatclient/common/utils.py | 25 ++++++ heatclient/osc/v1/stack.py | 81 +++++++++++++++++- heatclient/tests/unit/osc/v1/test_stack.py | 96 ++++++++++++++++++++++ heatclient/tests/unit/test_utils.py | 11 +++ setup.cfg | 1 + 5 files changed, 213 insertions(+), 1 deletion(-) diff --git a/heatclient/common/utils.py b/heatclient/common/utils.py index a36ab38c..b646e599 100644 --- a/heatclient/common/utils.py +++ b/heatclient/common/utils.py @@ -17,6 +17,7 @@ import logging import os import textwrap +import time import uuid from oslo_serialization import jsonutils @@ -115,6 +116,30 @@ def event_log_formatter(events): return "\n".join(event_log) +def wait_for_delete(status_f, + res_id, + status_field='status', + sleep_time=5, + timeout=300): + """Wait for resource deletion.""" + + total_time = 0 + while total_time < timeout: + try: + res = status_f(res_id) + except exc.HTTPNotFound: + return True + + status = res.get(status_field, '').lower() + if 'failed' in status: + return False + + time.sleep(sleep_time) + total_time += sleep_time + + return False + + def print_update_list(lst, fields, formatters=None): """Print the stack-update --dry-run output as a table. diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index d71f5f74..d0370a5a 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -14,7 +14,9 @@ """Orchestration v1 Stack action implementations""" import logging +import sys +from cliff import command from cliff import lister from cliff import show from openstackclient.common import exceptions as exc @@ -30,6 +32,7 @@ from heatclient.common import utils as heat_utils from heatclient import exc as heat_exc from heatclient.openstack.common._i18n import _ +from heatclient.openstack.common._i18n import _LI def _authenticated_fetcher(client): @@ -505,7 +508,6 @@ def get_parser(self, prog_name): help=_('List additional fields in output, this is implied by ' '--all-projects') ) - return parser def take_action(self, parsed_args): @@ -572,6 +574,83 @@ def _list(client, args=None): ) +class DeleteStack(command.Command): + """Delete stack(s).""" + + log = logging.getLogger(__name__ + ".DeleteStack") + + def get_parser(self, prog_name): + parser = super(DeleteStack, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + nargs='+', + help=_('Stack(s) to delete (name or ID)') + ) + parser.add_argument( + '--yes', + action='store_true', + help=_('Skip yes/no prompt (assume yes)') + ) + parser.add_argument( + '--wait', + action='store_true', + help=_('Wait for stack delete to complete') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + + try: + if not parsed_args.yes and sys.stdin.isatty(): + sys.stdout.write( + _("Are you sure you want to delete this stack(s) [y/N]? ")) + prompt_response = sys.stdin.readline().lower() + if not prompt_response.startswith('y'): + self.log.info(_LI('User did not confirm stack delete so ' + 'taking no action.')) + return + except KeyboardInterrupt: # ctrl-c + self.log.info(_LI('User did not confirm stack delete ' + '(ctrl-c) so taking no action.')) + return + except EOFError: # ctrl-d + self.log.info(_LI('User did not confirm stack delete ' + '(ctrl-d) so taking no action.')) + return + + failure_count = 0 + stacks_waiting = [] + for sid in parsed_args.stack: + try: + heat_client.stacks.delete(sid) + stacks_waiting.append(sid) + except heat_exc.HTTPNotFound: + failure_count += 1 + print(_('Stack not found: %s') % sid) + + if parsed_args.wait: + for sid in stacks_waiting: + def status_f(id): + return heat_client.stacks.get(id).to_dict() + + # TODO(jonesbr): switch to use openstack client wait_for_delete + # when version 2.1.0 is adopted. + if not heat_utils.wait_for_delete(status_f, + sid, + status_field='stack_status'): + failure_count += 1 + print(_('Stack failed to delete: %s') % sid) + + if failure_count: + msg = (_('Unable to delete %(count)d of the %(total)d stacks.') % + {'count': failure_count, 'total': len(parsed_args.stack)}) + raise exc.CommandError(msg) + + class AdoptStack(show.ShowOne): """Adopt a stack.""" diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index 161de585..7a1fd367 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -527,6 +527,102 @@ def test_stack_list_tags_bad_mode(self): self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) +class TestStackDelete(TestStack): + + def setUp(self): + super(TestStackDelete, self).setUp() + self.cmd = stack.DeleteStack(self.app, None) + self.stack_client.delete = mock.MagicMock() + self.stack_client.get = mock.MagicMock( + side_effect=heat_exc.HTTPNotFound) + + def test_stack_delete(self): + arglist = ['stack1', 'stack2', 'stack3'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.delete.assert_any_call('stack1') + self.stack_client.delete.assert_any_call('stack2') + self.stack_client.delete.assert_any_call('stack3') + + def test_stack_delete_not_found(self): + arglist = ['my_stack'] + self.stack_client.delete.side_effect = heat_exc.HTTPNotFound + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + def test_stack_delete_one_found_one_not_found(self): + arglist = ['stack1', 'stack2'] + self.stack_client.delete.side_effect = [None, heat_exc.HTTPNotFound] + parsed_args = self.check_parser(self.cmd, arglist, []) + + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, parsed_args) + + self.stack_client.delete.assert_any_call('stack1') + self.stack_client.delete.assert_any_call('stack2') + self.assertEqual('Unable to delete 1 of the 2 stacks.', str(error)) + + def test_stack_delete_wait(self): + arglist = ['stack1', 'stack2', 'stack3', '--wait'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.delete.assert_any_call('stack1') + self.stack_client.get.assert_any_call('stack1') + self.stack_client.delete.assert_any_call('stack2') + self.stack_client.get.assert_any_call('stack2') + self.stack_client.delete.assert_any_call('stack3') + self.stack_client.get.assert_any_call('stack3') + + def test_stack_delete_wait_one_pass_one_fail(self): + arglist = ['stack1', 'stack2', 'stack3', '--wait'] + self.stack_client.get.side_effect = [ + stacks.Stack(None, {'stack_status': 'DELETE_FAILED'}), + heat_exc.HTTPNotFound, + stacks.Stack(None, {'stack_status': 'DELETE_FAILED'}), + ] + parsed_args = self.check_parser(self.cmd, arglist, []) + + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, parsed_args) + + self.stack_client.delete.assert_any_call('stack1') + self.stack_client.get.assert_any_call('stack1') + self.stack_client.delete.assert_any_call('stack2') + self.stack_client.get.assert_any_call('stack2') + self.stack_client.delete.assert_any_call('stack3') + self.stack_client.get.assert_any_call('stack3') + self.assertEqual('Unable to delete 2 of the 3 stacks.', str(error)) + + @mock.patch('sys.stdin', spec=six.StringIO) + def test_stack_delete_prompt(self, mock_stdin): + arglist = ['my_stack'] + mock_stdin.isatty.return_value = True + mock_stdin.readline.return_value = 'y' + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + mock_stdin.readline.assert_called_with() + self.stack_client.delete.assert_called_with('my_stack') + + @mock.patch('sys.stdin', spec=six.StringIO) + def test_stack_delete_prompt_no(self, mock_stdin): + arglist = ['my_stack'] + mock_stdin.isatty.return_value = True + mock_stdin.readline.return_value = 'n' + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + mock_stdin.readline.assert_called_with() + self.stack_client.delete.assert_not_called() + + class TestStackAdopt(TestStack): adopt_file = 'heatclient/tests/test_templates/adopt.json' diff --git a/heatclient/tests/unit/test_utils.py b/heatclient/tests/unit/test_utils.py index 8237b4fc..c58f7888 100644 --- a/heatclient/tests/unit/test_utils.py +++ b/heatclient/tests/unit/test_utils.py @@ -188,6 +188,17 @@ def test_event_log_formatter(self): self.assertEqual(expected, utils.event_log_formatter(events_list)) self.assertEqual('', utils.event_log_formatter([])) + def test_wait_for_delete(self): + def status_f(id): + raise exc.HTTPNotFound + + def bad_status_f(id): + return {'status': 'failed'} + + self.assertTrue(utils.wait_for_delete(status_f, 123)) + self.assertFalse(utils.wait_for_delete(status_f, 123, timeout=0)) + self.assertFalse(utils.wait_for_delete(bad_status_f, 123)) + class ShellTestParameterFiles(testtools.TestCase): diff --git a/setup.cfg b/setup.cfg index b88d0678..f6e7cad2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,7 @@ openstack.orchestration.v1 = stack_adopt = heatclient.osc.v1.stack:AdoptStack stack_check = heatclient.osc.v1.stack:CheckStack stack_create = heatclient.osc.v1.stack:CreateStack + stack_delete = heatclient.osc.v1.stack:DeleteStack stack_list = heatclient.osc.v1.stack:ListStack stack_output_list = heatclient.osc.v1.stack:OutputListStack stack_output_show = heatclient.osc.v1.stack:OutputShowStack From 55417bb44fd83b5265456ec02902f539132e830b Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Wed, 11 Nov 2015 21:31:49 +0000 Subject: [PATCH 141/553] OpenstackClient plugin for event show This change implements the "openstack event show" command. Blueprint: heat-support-python-openstackclient Change-Id: Ie9614626969612e954270b1d2eb00ccd0034d274 --- heatclient/osc/v1/event.py | 78 ++++++++++++++++ heatclient/tests/unit/osc/v1/test_event.py | 102 +++++++++++++++++++++ setup.cfg | 1 + 3 files changed, 181 insertions(+) create mode 100644 heatclient/osc/v1/event.py create mode 100644 heatclient/tests/unit/osc/v1/test_event.py diff --git a/heatclient/osc/v1/event.py b/heatclient/osc/v1/event.py new file mode 100644 index 00000000..5cc00cc3 --- /dev/null +++ b/heatclient/osc/v1/event.py @@ -0,0 +1,78 @@ +# 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. +# +# Copyright 2015 IBM Corp. + +import logging + +from cliff import show +from openstackclient.common import utils + +from heatclient.common import utils as heat_utils +from heatclient import exc +from heatclient.openstack.common._i18n import _ + + +class ShowEvent(show.ShowOne): + """Show event details.""" + + log = logging.getLogger(__name__ + '.ShowEvent') + + def get_parser(self, prog_name): + parser = super(ShowEvent, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack to show events for') + ) + parser.add_argument( + 'resource', + metavar='', + help=_('Name of the resource event belongs to') + ) + parser.add_argument( + 'event', + metavar='', + help=_('ID of event to display details for') + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + fields = { + 'stack_id': parsed_args.stack, + 'resource_name': parsed_args.resource, + 'event_id': parsed_args.event + } + + try: + client.stacks.get(parsed_args.stack) + client.resources.get(parsed_args.stack, parsed_args.resource) + event = client.events.get(**fields) + except exc.HTTPNotFound as ex: + raise exc.CommandError(str(ex)) + + formatters = { + 'links': heat_utils.link_formatter, + 'resource_properties': heat_utils.json_formatter + } + + columns = [] + for key in event.to_dict(): + columns.append(key) + + return columns, utils.get_item_properties(event, columns, + formatters=formatters) diff --git a/heatclient/tests/unit/osc/v1/test_event.py b/heatclient/tests/unit/osc/v1/test_event.py new file mode 100644 index 00000000..7739b177 --- /dev/null +++ b/heatclient/tests/unit/osc/v1/test_event.py @@ -0,0 +1,102 @@ +# 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. +# +# Copyright 2015 IBM Corp. + +import mock +import testscenarios + +from heatclient import exc +from heatclient.osc.v1 import event +from heatclient.tests.unit.osc.v1 import fakes +from heatclient.v1 import events + +load_tests = testscenarios.load_tests_apply_scenarios + + +class TestEvent(fakes.TestOrchestrationv1): + def setUp(self): + super(TestEvent, self).setUp() + self.mock_client = self.app.client_manager.orchestration + self.event_client = self.app.client_manager.orchestration.events + self.stack_client = self.app.client_manager.orchestration.stacks + self.resource_client = self.app.client_manager.orchestration.resources + + +class TestEventShow(TestEvent): + + scenarios = [ + ('table', dict(format='table')), + ('shell', dict(format='shell')), + ('value', dict(format='value')), + ] + + response = { + 'event': { + "resource_name": "my_resource", + "event_time": "2015-11-11T15:23:47Z", + "links": [], + "logical_resource_id": "my_resource", + "resource_status": "CREATE_FAILED", + "resource_status_reason": "NotFound", + "physical_resource_id": "null", + "id": "474bfdf0-a450-46ec-a78a-0c7faa404073" + } + } + + def setUp(self): + super(TestEventShow, self).setUp() + self.cmd = event.ShowEvent(self.app, None) + + def test_event_show(self): + arglist = ['--format', self.format, 'my_stack', 'my_resource', '1234'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.stack_client.get = mock.MagicMock() + self.resource_client.get = mock.MagicMock() + self.event_client.get = mock.MagicMock( + return_value=events.Event(None, self.response)) + + self.cmd.take_action(parsed_args) + + self.event_client.get.assert_called_with(**{ + 'stack_id': 'my_stack', + 'resource_name': 'my_resource', + 'event_id': '1234' + }) + + def _test_not_found(self, error): + arglist = ['my_stack', 'my_resource', '1234'] + parsed_args = self.check_parser(self.cmd, arglist, []) + ex = self.assertRaises(exc.CommandError, self.cmd.take_action, + parsed_args) + self.assertIn(error, str(ex)) + + def test_event_show_stack_not_found(self): + error = 'Stack not found' + self.stack_client.get = mock.MagicMock( + side_effect=exc.HTTPNotFound(error)) + self._test_not_found(error) + + def test_event_show_resource_not_found(self): + error = 'Resource not found' + self.stack_client.get = mock.MagicMock() + self.resource_client.get = mock.MagicMock( + side_effect=exc.HTTPNotFound(error)) + self._test_not_found(error) + + def test_event_show_event_not_found(self): + error = 'Event not found' + self.stack_client.get = mock.MagicMock() + self.resource_client.get = mock.MagicMock() + self.event_client.get = mock.MagicMock( + side_effect=exc.HTTPNotFound(error)) + self._test_not_found(error) diff --git a/setup.cfg b/setup.cfg index f6e7cad2..858dfb29 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,6 +44,7 @@ openstack.orchestration.v1 = stack_check = heatclient.osc.v1.stack:CheckStack stack_create = heatclient.osc.v1.stack:CreateStack stack_delete = heatclient.osc.v1.stack:DeleteStack + stack_event_show = heatclient.osc.v1.event:ShowEvent stack_list = heatclient.osc.v1.stack:ListStack stack_output_list = heatclient.osc.v1.stack:OutputListStack stack_output_show = heatclient.osc.v1.stack:OutputShowStack From efec30e2904a9b5a5f2bbdedd2972e21601cb87a Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Wed, 11 Nov 2015 21:34:11 +0000 Subject: [PATCH 142/553] OpenstackClient plugin for event list This change implements the "openstack event list" command. Blueprint: heat-support-python-openstackclient Change-Id: I052be7ab510c40dac26a1fb7662016241976a2f7 --- heatclient/osc/v1/event.py | 113 +++++++++++++++++++++ heatclient/tests/unit/osc/v1/test_event.py | 110 ++++++++++++++++++++ setup.cfg | 1 + 3 files changed, 224 insertions(+) diff --git a/heatclient/osc/v1/event.py b/heatclient/osc/v1/event.py index 5cc00cc3..930e7e62 100644 --- a/heatclient/osc/v1/event.py +++ b/heatclient/osc/v1/event.py @@ -14,9 +14,11 @@ import logging +from cliff import lister from cliff import show from openstackclient.common import utils +from heatclient.common import event_utils from heatclient.common import utils as heat_utils from heatclient import exc from heatclient.openstack.common._i18n import _ @@ -76,3 +78,114 @@ def take_action(self, parsed_args): return columns, utils.get_item_properties(event, columns, formatters=formatters) + + +class ListEvent(lister.Lister): + """List events.""" + + log = logging.getLogger(__name__ + '.ListEvent') + + @property + def formatter_default(self): + return 'value' + + def get_parser(self, prog_name): + parser = super(ListEvent, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack to show events for') + ) + parser.add_argument( + '--resource', + metavar='', + help=_('Name of resource to show events for. Note: this cannot ' + 'be specified with --nested-depth') + ) + parser.add_argument( + '--filter', + metavar='', + action='append', + help=_('Filter parameters to apply on returned events') + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Limit the number of events returned') + ) + parser.add_argument( + '--marker', + metavar='', + help=_('Only return events that appear after the given ID') + ) + parser.add_argument( + '--nested-depth', + metavar='', + type=int, + help=_('Depth of nested stacks from which to display events. ' + 'Note: this cannot be specified with --resource') + ) + parser.add_argument( + '--sort', + metavar='[:]', + action='append', + help=_('Sort output by selected keys and directions (asc or desc) ' + '(default: asc). Specify multiple times to sort on ' + 'multiple keys') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + columns = ['id', 'resource_status', 'resource_status_reason', + 'event_time', 'physical_resource_id'] + + kwargs = { + 'resource_name': parsed_args.resource, + 'limit': parsed_args.limit, + 'marker': parsed_args.marker, + 'filters': heat_utils.format_parameters(parsed_args.filter), + } + + if parsed_args.resource and parsed_args.nested_depth: + msg = _('--nested-depth cannot be specified with --resource') + raise exc.CommandError(msg) + + if parsed_args.nested_depth: + # Until the API supports recursive event listing we'll have to do + # the marker/limit filtering client-side + del kwargs['marker'] + del kwargs['limit'] + columns.append('stack_name') + nested_depth = parsed_args.nested_depth + else: + nested_depth = 0 + + events = event_utils.get_events( + client, stack_id=parsed_args.stack, event_args=kwargs, + nested_depth=nested_depth, marker=parsed_args.marker, + limit=parsed_args.limit) + + 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') + events.reverse() + return [], [e.split(' ') for e in events] + + if len(events): + if hasattr(events[0], 'resource_name'): + columns.insert(0, 'resource_name') + columns.append('logical_resource_id') + else: + columns.insert(0, 'logical_resource_id') + + return ( + columns, + (utils.get_item_properties(s, columns) for s in events) + ) diff --git a/heatclient/tests/unit/osc/v1/test_event.py b/heatclient/tests/unit/osc/v1/test_event.py index 7739b177..c18457aa 100644 --- a/heatclient/tests/unit/osc/v1/test_event.py +++ b/heatclient/tests/unit/osc/v1/test_event.py @@ -12,6 +12,7 @@ # # Copyright 2015 IBM Corp. +import copy import mock import testscenarios @@ -100,3 +101,112 @@ def test_event_show_event_not_found(self): self.event_client.get = mock.MagicMock( side_effect=exc.HTTPNotFound(error)) self._test_not_found(error) + + +class TestEventList(TestEvent): + + defaults = { + 'stack_id': 'my_stack', + 'resource_name': None, + 'limit': None, + 'marker': None, + 'filters': {}, + } + + fields = ['resource_name', 'id', 'resource_status', + 'resource_status_reason', 'event_time', 'physical_resource_id', + 'logical_resource_id'] + + class MockEvent(object): + + data = { + 'event_time': '2015-11-13T10:02:17', + 'id': '1234', + 'logical_resource_id': 'resource1', + 'physical_resource_id': '', + 'resource_name': 'resource1', + 'resource_status': 'CREATE_COMPLETE', + 'resource_status_reason': 'state changed', + 'stack_name': 'my_stack', + } + + def __getattr__(self, key): + try: + return self.data[key] + except KeyError: + # hasattr() in python 3 expects an AttributeError to be raised + raise AttributeError + + def setUp(self): + super(TestEventList, self).setUp() + self.cmd = event.ListEvent(self.app, None) + self.event = self.MockEvent() + self.event_client.list = mock.MagicMock(return_value=[self.event]) + self.resource_client.list = mock.MagicMock(return_value={}) + + def test_event_list_defaults(self): + arglist = ['my_stack', '--format', 'table'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.event_client.list.assert_called_with(**self.defaults) + self.assertEqual(self.fields, columns) + + def test_event_list_resource_nested_depth(self): + arglist = ['my_stack', '--resource', 'my_resource', + '--nested-depth', '3', '--format', 'table'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + def test_event_list_logical_resource_id(self): + arglist = ['my_stack', '--format', 'table'] + del self.event.data['resource_name'] + cols = copy.deepcopy(self.fields) + cols.pop() + cols[0] = 'logical_resource_id' + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.event_client.list.assert_called_with(**self.defaults) + self.assertEqual(cols, columns) + self.event.data['resource_name'] = 'resource1' + + def test_event_list_nested_depth(self): + arglist = ['my_stack', '--nested-depth', '3', '--format', 'table'] + kwargs = copy.deepcopy(self.defaults) + del kwargs['marker'] + del kwargs['limit'] + cols = copy.deepcopy(self.fields) + cols[-1] = 'stack_name' + cols.append('logical_resource_id') + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.event_client.list.assert_called_with(**kwargs) + self.assertEqual(cols, columns) + + def test_event_list_sort(self): + arglist = ['my_stack', '--sort', 'resource_name:desc', + '--format', 'table'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.event_client.list.assert_called_with(**self.defaults) + self.assertEqual(self.fields, columns) + + def test_event_list_value_format(self): + arglist = ['my_stack'] + expected = ('2015-11-13 10:02:17 [resource1]: CREATE_COMPLETE ' + 'state changed') + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.event_client.list.assert_called_with(**self.defaults) + self.assertEqual([], columns) + self.assertEqual([expected.split(' ')], data) diff --git a/setup.cfg b/setup.cfg index 858dfb29..1ae897fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,6 +44,7 @@ openstack.orchestration.v1 = stack_check = heatclient.osc.v1.stack:CheckStack stack_create = heatclient.osc.v1.stack:CreateStack stack_delete = heatclient.osc.v1.stack:DeleteStack + stack_event_list = heatclient.osc.v1.event:ListEvent stack_event_show = heatclient.osc.v1.event:ShowEvent stack_list = heatclient.osc.v1.stack:ListStack stack_output_list = heatclient.osc.v1.stack:OutputListStack From 1d302a93bbad93127665d078dffcc049f03d7996 Mon Sep 17 00:00:00 2001 From: Amey Bhide Date: Mon, 9 Nov 2015 14:59:21 -0800 Subject: [PATCH 143/553] OSC plugin for stack resource show and list This change implements the 'openstack stack resource show' and 'openstack stack resource list' commands. Blueprint: heat-support-python-openstackclient Change-Id: I3f9d64e4d76616e201374522f06a4e8938f00d4d --- heatclient/osc/v1/resources.py | 107 +++++++++++ heatclient/osc/v1/stack.py | 2 +- .../tests/unit/osc/v1/test_resources.py | 174 +++++++++++++++++- setup.cfg | 2 + 4 files changed, 278 insertions(+), 7 deletions(-) diff --git a/heatclient/osc/v1/resources.py b/heatclient/osc/v1/resources.py index 9f9a4d05..685629cc 100644 --- a/heatclient/osc/v1/resources.py +++ b/heatclient/osc/v1/resources.py @@ -16,13 +16,120 @@ import logging import six +from cliff import lister +from cliff import show from openstackclient.common import exceptions as exc +from openstackclient.common import utils from openstackclient.i18n import _ from heatclient.common import format_utils from heatclient import exc as heat_exc +class ResourceShow(show.ShowOne): + """Display stack resource.""" + + log = logging.getLogger(__name__ + '.ResourceShowStack') + + def get_parser(self, prog_name): + parser = super(ResourceShow, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack to query') + ) + parser.add_argument( + 'resource', + metavar='', + help=_('Name or ID of resource') + ) + parser.add_argument( + '--with-attr', + metavar='', + action='append', + help=_('Attribute to show, can be specified multiple times') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + try: + resource = client.resources.get(parsed_args.stack, + parsed_args.resource, + with_attr=parsed_args.with_attr) + except heat_exc.HTTPNotFound: + msg = (_('Stack or resource not found: %(stack)s %(resource)s') % + {'stack': parsed_args.stack, + 'resource': parsed_args.resource}) + raise exc.CommandError(msg) + + return self.dict2columns(resource.to_dict()) + + +class ResourceList(lister.Lister): + """List stack resources.""" + + log = logging.getLogger(__name__ + '.ResourceListStack') + + def get_parser(self, prog_name): + parser = super(ResourceList, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack to query') + ) + parser.add_argument( + '--long', + action='store_true', + help=_('Enable detailed information presented for each resource ' + 'in resource list') + ) + parser.add_argument( + '-n', '--nested-depth', + metavar='', + type=int, + help=_('Depth of nested stacks from which to display resources') + ) + # TODO(jonesbr): + # Add --filter once https://review.openstack.org/#/c/257864/ is merged + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + fields = { + 'nested_depth': parsed_args.nested_depth, + 'with_detail': parsed_args.long, + } + + try: + resources = client.resources.list(parsed_args.stack, **fields) + except heat_exc.HTTPNotFound: + msg = _('Stack not found: %s') % parsed_args.stack + raise exc.CommandError(msg) + + columns = ['physical_resource_id', 'resource_type', 'resource_status', + 'updated_time'] + + if len(resources) >= 1 and not hasattr(resources[0], 'resource_name'): + columns.insert(0, 'logical_resource_id') + else: + columns.insert(0, 'resource_name') + + if parsed_args.nested_depth or parsed_args.long: + columns.append('stack_name') + + return ( + columns, + (utils.get_item_properties(r, columns) for r in resources) + ) + + class ResourceMetadata(format_utils.JsonFormat): """Show resource metadata""" diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index d0370a5a..98f417e3 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -365,7 +365,7 @@ def take_action(self, parsed_args): class ShowStack(show.ShowOne): - """Show stack details""" + """Show stack details.""" log = logging.getLogger(__name__ + ".ShowStack") diff --git a/heatclient/tests/unit/osc/v1/test_resources.py b/heatclient/tests/unit/osc/v1/test_resources.py index 3d9a1246..498b8b9b 100644 --- a/heatclient/tests/unit/osc/v1/test_resources.py +++ b/heatclient/tests/unit/osc/v1/test_resources.py @@ -11,6 +11,7 @@ # under the License. # +import copy import mock from openstackclient.common import exceptions as exc @@ -18,12 +19,174 @@ from heatclient import exc as heat_exc from heatclient.osc.v1 import resources from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes +from heatclient.v1 import resources as v1_resources class TestResource(orchestration_fakes.TestOrchestrationv1): def setUp(self): super(TestResource, self).setUp() - self.mock_client = self.app.client_manager.orchestration + self.resource_client = self.app.client_manager.orchestration.resources + + +class TestStackResourceShow(TestResource): + + response = { + 'attributes': {}, + 'creation_time': '2016-02-01T20:20:53', + 'description': 'a resource', + 'links': [ + {'rel': 'stack', + "href": "http://heat.example.com:8004/my_stack/12"} + ], + 'logical_resource_id': 'my_resource', + 'physical_resource_id': '1234', + 'required_by': [], + 'resource_name': 'my_resource', + 'resource_status': 'CREATE_COMPLETE', + 'resource_status_reason': 'state changed', + 'resource_type': 'OS::Heat::None', + 'updated_time': '2016-02-01T20:20:53', + } + + def setUp(self): + super(TestStackResourceShow, self).setUp() + self.cmd = resources.ResourceShow(self.app, None) + self.resource_client.get = mock.MagicMock( + return_value=v1_resources.Resource(None, self.response)) + + def test_resource_show(self): + arglist = ['my_stack', 'my_resource'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.resource_client.get.assert_called_with('my_stack', 'my_resource', + with_attr=None) + for key in self.response: + self.assertIn(key, columns) + self.assertIn(self.response[key], data) + + def test_resource_show_with_attr(self): + arglist = ['my_stack', 'my_resource', + '--with-attr', 'foo', '--with-attr', 'bar'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.resource_client.get.assert_called_with('my_stack', 'my_resource', + with_attr=['foo', 'bar']) + for key in self.response: + self.assertIn(key, columns) + self.assertIn(self.response[key], data) + + def test_resource_show_not_found(self): + arglist = ['my_stack', 'bad_resource'] + self.resource_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 or resource not found: my_stack bad_resource', + str(error)) + + +class TestStackResourceList(TestResource): + + response = { + 'attributes': {}, + 'creation_time': '2016-02-01T20:20:53', + 'description': 'a resource', + 'links': [ + {'rel': 'stack', + "href": "http://heat.example.com:8004/my_stack/12"} + ], + 'logical_resource_id': '1234', + 'physical_resource_id': '1234', + 'required_by': [], + 'resource_name': 'my_resource', + 'resource_status': 'CREATE_COMPLETE', + 'resource_status_reason': 'state changed', + 'resource_type': 'OS::Heat::None', + 'updated_time': '2016-02-01T20:20:53', + } + + columns = ['resource_name', 'physical_resource_id', 'resource_type', + 'resource_status', 'updated_time'] + + data = ['my_resource', '1234', 'OS::Heat::None', + 'CREATE_COMPLETE', '2016-02-01T20:20:53'] + + def setUp(self): + super(TestStackResourceList, self).setUp() + self.cmd = resources.ResourceList(self.app, None) + self.resource_client.list = mock.MagicMock( + return_value=[v1_resources.Resource(None, self.response)]) + + def test_resource_list(self): + arglist = ['my_stack'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.resource_client.list.assert_called_with( + 'my_stack', with_detail=False, nested_depth=None) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), list(data)[0]) + + def test_resource_list_not_found(self): + arglist = ['bad_stack'] + self.resource_client.list.side_effect = heat_exc.HTTPNotFound + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + def test_resource_list_with_detail(self): + arglist = ['my_stack', '--long'] + cols = copy.deepcopy(self.columns) + cols.append('stack_name') + out = copy.deepcopy(self.data) + out.append('my_stack') + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.resource_client.list.assert_called_with( + 'my_stack', with_detail=True, nested_depth=None) + self.assertEqual(cols, columns) + self.assertEqual(tuple(out), list(data)[0]) + + def test_resource_list_nested_depth(self): + arglist = ['my_stack', '--nested-depth', '3'] + cols = copy.deepcopy(self.columns) + cols.append('stack_name') + out = copy.deepcopy(self.data) + out.append('my_stack') + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.resource_client.list.assert_called_with( + 'my_stack', with_detail=False, nested_depth=3) + self.assertEqual(cols, columns) + self.assertEqual(tuple(out), list(data)[0]) + + def test_resource_list_no_resource_name(self): + arglist = ['my_stack'] + resp = copy.deepcopy(self.response) + del resp['resource_name'] + cols = copy.deepcopy(self.columns) + cols[0] = 'logical_resource_id' + out = copy.deepcopy(self.data) + out[1] = '1234' + self.resource_client.list.return_value = [ + v1_resources.Resource(None, resp)] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.resource_client.list.assert_called_with( + 'my_stack', with_detail=False, nested_depth=None) + self.assertEqual(cols, columns) class TestResourceMetadata(TestResource): @@ -31,14 +194,13 @@ class TestResourceMetadata(TestResource): def setUp(self): super(TestResourceMetadata, self).setUp() self.cmd = resources.ResourceMetadata(self.app, None) - self.mock_client.resources.metadata = mock.Mock( - return_value={}) + self.resource_client.metadata = mock.Mock(return_value={}) def test_resource_metadata(self): arglist = ['my_stack', 'my_resource'] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) - self.mock_client.resources.metadata.assert_called_with(**{ + self.resource_client.metadata.assert_called_with(**{ 'stack_id': 'my_stack', 'resource_name': 'my_resource' }) @@ -47,7 +209,7 @@ def test_resource_metadata_yaml(self): arglist = ['my_stack', 'my_resource', '--format', 'yaml'] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) - self.mock_client.resources.metadata.assert_called_with(**{ + self.resource_client.metadata.assert_called_with(**{ 'stack_id': 'my_stack', 'resource_name': 'my_resource' }) @@ -55,6 +217,6 @@ def test_resource_metadata_yaml(self): def test_resource_metadata_error(self): arglist = ['my_stack', 'my_resource'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.resources.metadata = mock.Mock( + self.resource_client.metadata = mock.Mock( side_effect=heat_exc.HTTPNotFound) self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) diff --git a/setup.cfg b/setup.cfg index 1ae897fc..31c14926 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,7 +49,9 @@ openstack.orchestration.v1 = stack_list = heatclient.osc.v1.stack:ListStack stack_output_list = heatclient.osc.v1.stack:OutputListStack stack_output_show = heatclient.osc.v1.stack:OutputShowStack + stack_resource_list = heatclient.osc.v1.resources:ResourceList stack_resource_metadata = heatclient.osc.v1.resources:ResourceMetadata + stack_resource_show = heatclient.osc.v1.resources:ResourceShow stack_resume = heatclient.osc.v1.stack:ResumeStack stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot From 0306a41fa0e5bf81b14466b5cd8618b4dd589070 Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Fri, 11 Dec 2015 13:47:28 -0600 Subject: [PATCH 144/553] Add openstack client stack resource signal Add the openstack stack resource signal command. Based from the existing heat commands: heat resource-signal Change-Id: I3b3628d86b71d448feea197f6c92d3d3d19726b5 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/resources.py | 76 +++++++++++++++++- .../tests/unit/osc/v1/test_resources.py | 78 ++++++++++++++++++- setup.cfg | 1 + 3 files changed, 152 insertions(+), 3 deletions(-) diff --git a/heatclient/osc/v1/resources.py b/heatclient/osc/v1/resources.py index 685629cc..dae8470c 100644 --- a/heatclient/osc/v1/resources.py +++ b/heatclient/osc/v1/resources.py @@ -13,16 +13,20 @@ """Orchestration v1 Stack action implementations""" +from cliff import command import logging import six +from six.moves.urllib import request from cliff import lister from cliff import show from openstackclient.common import exceptions as exc from openstackclient.common import utils from openstackclient.i18n import _ +from oslo_serialization import jsonutils from heatclient.common import format_utils +from heatclient.common import utils as heat_utils from heatclient import exc as heat_exc @@ -161,10 +165,78 @@ def _resource_metadata(heat_client, args): try: metadata = heat_client.resources.metadata(**fields) except heat_exc.HTTPNotFound: - raise exc.CommandError(_('Stack or resource not found: ' - '%(stack)s %(resource)s') % + raise exc.CommandError(_('Stack %(stack)s or resource %(resource)s ' + 'not found.') % {'stack': args.stack, 'resource': args.resource}) + data = list(six.itervalues(metadata)) columns = list(six.iterkeys(metadata)) return columns, data + + +class ResourceSignal(command.Command): + """Signal a resource with optional data.""" + + log = logging.getLogger(__name__ + ".ResourceSignal") + + def get_parser(self, prog_name): + parser = super(ResourceSignal, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack the resource belongs to'), + ) + parser.add_argument( + 'resource', + metavar='', + help=_('Name of the resoure to signal'), + ) + parser.add_argument( + '--data', + metavar='', + help=_('JSON Data to send to the signal handler') + ) + parser.add_argument( + '--data-file', + metavar='', + help=_('File containing JSON data to send to the signal handler') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + return _resource_signal(heat_client, parsed_args) + + +def _resource_signal(heat_client, args): + fields = {'stack_id': args.stack, + 'resource_name': args.resource} + data = args.data + data_file = args.data_file + if data and data_file: + raise exc.CommandError(_('Should only specify one of data or ' + 'data-file')) + + if data_file: + data_url = heat_utils.normalise_file_path_to_url(data_file) + data = request.urlopen(data_url).read() + + if data: + try: + data = jsonutils.loads(data) + except ValueError as ex: + raise exc.CommandError(_('Data should be in JSON format: %s') % ex) + if not isinstance(data, dict): + raise exc.CommandError(_('Data should be a JSON dict')) + + fields['data'] = data + try: + heat_client.resources.signal(**fields) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Stack %(stack)s or resource %(resource)s ' + 'not found.') % + {'stack': args.stack, + 'resource': args.resource}) diff --git a/heatclient/tests/unit/osc/v1/test_resources.py b/heatclient/tests/unit/osc/v1/test_resources.py index 498b8b9b..367e1fd3 100644 --- a/heatclient/tests/unit/osc/v1/test_resources.py +++ b/heatclient/tests/unit/osc/v1/test_resources.py @@ -219,4 +219,80 @@ def test_resource_metadata_error(self): parsed_args = self.check_parser(self.cmd, arglist, []) self.resource_client.metadata = mock.Mock( side_effect=heat_exc.HTTPNotFound) - self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertEqual('Stack my_stack or resource my_resource not found.', + str(error)) + + +class TestResourceSignal(TestResource): + + def setUp(self): + super(TestResourceSignal, self).setUp() + self.cmd = resources.ResourceSignal(self.app, None) + self.resource_client.signal = mock.Mock() + + def test_resource_signal(self): + arglist = ['my_stack', 'my_resource'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.resource_client.signal.assert_called_with(**{ + 'stack_id': 'my_stack', + 'resource_name': 'my_resource' + }) + + def test_resource_signal_error(self): + arglist = ['my_stack', 'my_resource'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.resource_client.signal = mock.Mock( + side_effect=heat_exc.HTTPNotFound) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertEqual('Stack my_stack or resource my_resource not found.', + str(error)) + + def test_resource_signal_data(self): + arglist = ['my_stack', 'my_resource', + '--data', '{"message":"Content"}'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.resource_client.signal.assert_called_with(**{ + 'data': {u'message': u'Content'}, + 'stack_id': 'my_stack', + 'resource_name': 'my_resource' + }) + + def test_resource_signal_data_not_json(self): + arglist = ['my_stack', 'my_resource', '--data', '{'] + parsed_args = self.check_parser(self.cmd, arglist, []) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn('Data should be in JSON format', str(error)) + + def test_resource_signal_data_and_file_error(self): + arglist = ['my_stack', 'my_resource', + '--data', '{}', '--data-file', 'file'] + parsed_args = self.check_parser(self.cmd, arglist, []) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertEqual('Should only specify one of data or data-file', + str(error)) + + @mock.patch('six.moves.urllib.request.urlopen') + def test_resource_signal_file(self, urlopen): + data = mock.Mock() + data.read.side_effect = ['{"message":"Content"}'] + urlopen.return_value = data + + arglist = ['my_stack', 'my_resource', '--data-file', 'test_file'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.resource_client.signal.assert_called_with(**{ + 'data': {u'message': u'Content'}, + 'stack_id': 'my_stack', + 'resource_name': 'my_resource' + }) diff --git a/setup.cfg b/setup.cfg index d721e5e9..4dc93c92 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,7 @@ openstack.orchestration.v1 = stack_resource_list = heatclient.osc.v1.resources:ResourceList stack_resource_metadata = heatclient.osc.v1.resources:ResourceMetadata stack_resource_show = heatclient.osc.v1.resources:ResourceShow + stack_resource_signal = heatclient.osc.v1.resources:ResourceSignal stack_resume = heatclient.osc.v1.stack:ResumeStack stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot From 06be732705a19ba1f9da3bb96722d6cfd062575d Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Wed, 13 Jan 2016 16:31:41 -0600 Subject: [PATCH 145/553] Add openstack client stack snapshot restore Based from the existing heat command: heat stack-restore Change-Id: I863631b393586d1ecd20c94988e16ef85fb8ad5a Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/snapshot.py | 37 +++++++++++++++++++ heatclient/tests/unit/osc/v1/test_snapshot.py | 25 +++++++++++++ setup.cfg | 1 + 3 files changed, 63 insertions(+) diff --git a/heatclient/osc/v1/snapshot.py b/heatclient/osc/v1/snapshot.py index 5e891810..52f9e016 100644 --- a/heatclient/osc/v1/snapshot.py +++ b/heatclient/osc/v1/snapshot.py @@ -16,6 +16,7 @@ import logging import six +from cliff import command from cliff import lister from openstackclient.common import exceptions as exc from openstackclient.common import utils @@ -97,3 +98,39 @@ def _show_snapshot(self, heat_client, stack_id, snapshot_id): rows = list(six.itervalues(data)) columns = list(six.iterkeys(data)) return columns, rows + + +class RestoreSnapshot(command.Command): + """Restore stack snapshot""" + + log = logging.getLogger(__name__ + ".RestoreSnapshot") + + def get_parser(self, prog_name): + parser = super(RestoreSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack containing the snapshot') + ) + parser.add_argument( + 'snapshot', + metavar='', + help=_('ID of the snapshot to restore') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + heat_client = self.app.client_manager.orchestration + return self._restore_snapshot(heat_client, parsed_args) + + def _restore_snapshot(self, heat_client, parsed_args): + fields = {'stack_id': parsed_args.stack, + 'snapshot_id': parsed_args.snapshot} + try: + heat_client.stacks.restore(**fields) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Stack %(stack)s or ' + 'snapshot %(snapshot)s not found.') % + {'stack': parsed_args.stack, + 'snapshot': parsed_args.snapshot}) diff --git a/heatclient/tests/unit/osc/v1/test_snapshot.py b/heatclient/tests/unit/osc/v1/test_snapshot.py index 88513c78..ae23299c 100644 --- a/heatclient/tests/unit/osc/v1/test_snapshot.py +++ b/heatclient/tests/unit/osc/v1/test_snapshot.py @@ -77,3 +77,28 @@ def test_snapshot_not_found(self): exc.CommandError, self.cmd.take_action, parsed_args) + + +class TestRestoreSnapshot(TestStack): + def setUp(self): + super(TestRestoreSnapshot, self).setUp() + self.cmd = snapshot.RestoreSnapshot(self.app, None) + self.stack_client.restore = mock.Mock() + + def test_snapshot_restore(self): + arglist = ['my_stack', 'my_snapshot'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.stack_client.restore.assert_called_with( + snapshot_id='my_snapshot', stack_id='my_stack') + + def test_snapshot_restore_error(self): + self.stack_client.restore.side_effect = heat_exc.HTTPNotFound() + arglist = ['my_stack', 'my_snapshot'] + parsed_args = self.check_parser(self.cmd, arglist, []) + error = self.assertRaises( + exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertEqual('Stack my_stack or snapshot my_snapshot not found.', + str(error)) diff --git a/setup.cfg b/setup.cfg index d721e5e9..294ae1b1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,7 @@ openstack.orchestration.v1 = stack_resume = heatclient.osc.v1.stack:ResumeStack stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot + stack_snapshot_restore = heatclient.osc.v1.snapshot:RestoreSnapshot stack_snapshot_show = heatclient.osc.v1.snapshot:ShowSnapshot stack_suspend = heatclient.osc.v1.stack:SuspendStack stack_template_show = heatclient.osc.v1.stack:TemplateShowStack From c6221b0975e758d406fa2e9316c1aedfada7edfd Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Tue, 24 Nov 2015 02:34:20 -0600 Subject: [PATCH 146/553] Add OpenstackClient plugin for software deployment show This change implements the "openstack software deployment show" command Based on the existing heat command: heat deployment-show Change-Id: Id0772d456d01f9eb5c67ad92584290844f9eeeb3 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/software_deployment.py | 46 ++++++++++++++ .../unit/osc/v1/test_software_deployment.py | 60 +++++++++++++++++++ setup.cfg | 1 + 3 files changed, 107 insertions(+) diff --git a/heatclient/osc/v1/software_deployment.py b/heatclient/osc/v1/software_deployment.py index a0259bae..67d1c401 100644 --- a/heatclient/osc/v1/software_deployment.py +++ b/heatclient/osc/v1/software_deployment.py @@ -17,6 +17,7 @@ from cliff import command from cliff import lister +from cliff import show from openstackclient.common import exceptions as exc from openstackclient.common import utils @@ -112,3 +113,48 @@ def _list_deployment(heat_client, args=None): columns, (utils.get_item_properties(s, columns) for s in deployments) ) + + +class ShowDeployment(show.ShowOne): + """Show SoftwareDeployment Details.""" + + log = logging.getLogger(__name__ + ".ShowSoftwareDeployment") + + def get_parser(self, prog_name): + parser = super(ShowDeployment, self).get_parser(prog_name) + parser.add_argument( + 'id', + metavar='', + help=_('ID of the deployment') + ) + parser.add_argument( + '--long', + action='store_true', + help=_('Show more fields in output') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + try: + data = heat_client.software_deployments.get( + deployment_id=parsed_args.id) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Software Deployment not found: %s') % id) + else: + columns = [ + 'id', + 'server_id', + 'config_id', + 'creation_time', + 'updated_time', + 'status', + 'status_reason', + 'input_values', + 'action', + ] + if parsed_args.long: + columns.append('output_values') + return columns, utils.get_item_properties(data, columns) diff --git a/heatclient/tests/unit/osc/v1/test_software_deployment.py b/heatclient/tests/unit/osc/v1/test_software_deployment.py index 36408b40..7157cf06 100644 --- a/heatclient/tests/unit/osc/v1/test_software_deployment.py +++ b/heatclient/tests/unit/osc/v1/test_software_deployment.py @@ -18,6 +18,7 @@ from heatclient import exc as heat_exc from heatclient.osc.v1 import software_deployment from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes +from heatclient.v1 import software_deployments class TestDeployment(orchestration_fakes.TestOrchestrationv1): @@ -136,3 +137,62 @@ def test_deployment_list_long(self): self.mock_client.list.assert_called_with(**kwargs) self.assertEqual(cols, columns) + + +class TestDeploymentShow(TestDeployment): + get_response = {"software_deployment": { + "status": "IN_PROGRESS", + "server_id": "ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5", + "config_id": "3d5ec2a8-7004-43b6-a7f6-542bdbe9d434", + "output_values": 'null', + "input_values": 'null', + "action": "CREATE", + "status_reason": "Deploy data available", + "id": "06e87bcc-33a2-4bce-aebd-533e698282d3", + "creation_time": "2015-01-31T15:12:36Z", + "updated_time": "2015-01-31T15:18:21Z" + }} + + def setUp(self): + super(TestDeploymentShow, self).setUp() + self.cmd = software_deployment.ShowDeployment(self.app, None) + + def test_deployment_show(self): + arglist = ['my_deployment'] + cols = ['id', 'server_id', 'config_id', 'creation_time', + 'updated_time', 'status', 'status_reason', + 'input_values', 'action'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.get = mock.Mock( + return_value=software_deployments.SoftwareDeployment( + None, self.get_response)) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.get.assert_called_with(**{ + 'deployment_id': 'my_deployment', + }) + self.assertEqual(cols, columns) + + def test_deployment_show_long(self): + arglist = ['my_deployment', '--long'] + cols = ['id', 'server_id', 'config_id', 'creation_time', + 'updated_time', 'status', 'status_reason', + 'input_values', 'action', 'output_values'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.get = mock.Mock( + return_value=software_deployments.SoftwareDeployment( + None, self.get_response)) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.get.assert_called_once_with(**{ + 'deployment_id': 'my_deployment', + }) + self.assertEqual(cols, columns) + + def test_deployment_not_found(self): + arglist = ['my_deployment'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.get = mock.Mock() + self.mock_client.get.side_effect = heat_exc.HTTPNotFound() + self.assertRaises( + exc.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index d721e5e9..047b8f4e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ openstack.orchestration.v1 = software_config_show = heatclient.osc.v1.software_config:ShowConfig software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment software_deployment_list = heatclient.osc.v1.software_deployment:ListDeployment + software_deployment_show = heatclient.osc.v1.software_deployment:ShowDeployment stack_abandon = heatclient.osc.v1.stack:AbandonStack stack_adopt = heatclient.osc.v1.stack:AdoptStack stack_check = heatclient.osc.v1.stack:CheckStack From 236ba5fa6565fe340a1e098bcbe9b216fb5b8931 Mon Sep 17 00:00:00 2001 From: Bo Wang Date: Tue, 16 Feb 2016 20:44:57 +0800 Subject: [PATCH 147/553] Use oslo.utils.reflection to extract class name The oslo.utils.reflection.get_class_name() handles more variations of where a class name may come from (on) python 2 and python 3. Its usage allows getting more accurate class names so we'd better use it. Change-Id: I97cc7f1e818161c2fe265da1ed1b52add1951c90 --- heatclient/exc.py | 7 ++++--- heatclient/openstack/common/apiclient/base.py | 4 +++- heatclient/tests/unit/osc/fakes.py | 4 +++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/heatclient/exc.py b/heatclient/exc.py index ea7d5758..7d4968ce 100644 --- a/heatclient/exc.py +++ b/heatclient/exc.py @@ -13,6 +13,7 @@ import sys from oslo_serialization import jsonutils +from oslo_utils import reflection from heatclient.openstack.common._i18n import _ @@ -78,9 +79,9 @@ def __str__(self): "available.") return (_("%(name)s (HTTP %(code)s) %(details)s") % { - 'name': self.__class__.__name__, - 'code': self.code, - 'details': self.details}) + 'name': reflection.get_class_name(self, fully_qualified=False), + 'code': self.code, + 'details': self.details}) class BadRequest(HTTPException): diff --git a/heatclient/openstack/common/apiclient/base.py b/heatclient/openstack/common/apiclient/base.py index 13faeb67..8251e4fe 100644 --- a/heatclient/openstack/common/apiclient/base.py +++ b/heatclient/openstack/common/apiclient/base.py @@ -41,6 +41,7 @@ import copy import logging +from oslo_utils import reflection from oslo_utils import strutils import six from six.moves.urllib import parse @@ -466,7 +467,8 @@ def __repr__(self): for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) + class_name = reflection.get_class_name(self, fully_qualified=False) + return "<%s %s>" % (class_name, info) @property def human_id(self): diff --git a/heatclient/tests/unit/osc/fakes.py b/heatclient/tests/unit/osc/fakes.py index 1fcd6e59..8ce67c74 100644 --- a/heatclient/tests/unit/osc/fakes.py +++ b/heatclient/tests/unit/osc/fakes.py @@ -16,6 +16,7 @@ import json import sys +from oslo_utils import reflection import requests import six @@ -222,7 +223,8 @@ def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) + class_name = reflection.get_class_name(self, fully_qualified=False) + return "<%s %s>" % (class_name, info) class FakeResponse(requests.Response): From 1a14d1f1538a0180759a0db2598651c69a130018 Mon Sep 17 00:00:00 2001 From: Bo Wang Date: Tue, 16 Feb 2016 21:36:36 +0800 Subject: [PATCH 148/553] Use yaml.safe_load() instead of yaml.load() yaml.load() provides the ability to construct an arbitrary python object that may be dangerous. yaml.safe_load() limits this ability to simple python objects like integers or lists. ref: https://en.wikipedia.org/wiki/YAML#Security Change-Id: Ic8313b4042aad1e077ba1fbb68979f8745adfe3d --- heatclient/tests/unit/test_template_utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/heatclient/tests/unit/test_template_utils.py b/heatclient/tests/unit/test_template_utils.py index 1e1eee94..0274fc12 100644 --- a/heatclient/tests/unit/test_template_utils.py +++ b/heatclient/tests/unit/test_template_utils.py @@ -825,10 +825,10 @@ def test_hot_template(self): files, tmpl_parsed = template_utils.get_template_contents( template_file=tmpl_file) - self.assertEqual(yaml.load(self.foo_template.decode('utf-8')), + self.assertEqual(yaml.safe_load(self.foo_template.decode('utf-8')), json.loads(files.get('file:///home/my/dir/foo.yaml'))) self.assertEqual( - yaml.load(self.egg_template.decode('utf-8')), + yaml.safe_load(self.egg_template.decode('utf-8')), json.loads(files.get('file:///home/my/dir/spam/egg.yaml'))) self.assertEqual({ @@ -907,7 +907,7 @@ def test_hot_template(self): files, tmpl_parsed = template_utils.get_template_contents( template_file=tmpl_file) - self.assertEqual(yaml.load(self.bar_template.decode('utf-8')), + self.assertEqual(yaml.safe_load(self.bar_template.decode('utf-8')), json.loads(files.get('file:///home/my/dir/bar.yaml'))) self.assertEqual({ @@ -1061,7 +1061,7 @@ def test_env_nested_includes(self): } }, json.loads(files.get(template_url))) - self.assertEqual(yaml.load(self.foo_template.decode('utf-8')), + self.assertEqual(yaml.safe_load(self.foo_template.decode('utf-8')), json.loads(files.get(foo_url))) self.assertEqual({ u'heat_template_version': u'2013-05-23', @@ -1081,9 +1081,9 @@ def test_env_nested_includes(self): }, json.loads(files.get(egg_url))) self.assertEqual(b'ham contents', files.get(ham_url)) - self.assertEqual(yaml.load(self.foo_template.decode('utf-8')), + self.assertEqual(yaml.safe_load(self.foo_template.decode('utf-8')), json.loads(files.get(one_url))) - self.assertEqual(yaml.load(self.foo_template.decode('utf-8')), + self.assertEqual(yaml.safe_load(self.foo_template.decode('utf-8')), json.loads(files.get(two_url))) self.assertEqual(b'three contents', files.get(three_url)) From 89deaa3c3cb8335bc9036cd944cc6482363047b8 Mon Sep 17 00:00:00 2001 From: Amey Bhide Date: Sun, 8 Nov 2015 21:45:22 -0800 Subject: [PATCH 149/553] OSC plugin for stack snapshot create This change implements "openstack stack snapshot create" command Based from the existing heat commands: heat stack-snapshot Blueprint: heat-support-python-openstackclient Change-Id: I0a4dc70d9f0dd82129d5839acecf5c1822fc147c --- heatclient/osc/v1/snapshot.py | 44 ++++++++++++++++++- heatclient/tests/unit/osc/v1/test_snapshot.py | 43 ++++++++++++++++++ setup.cfg | 1 + 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/heatclient/osc/v1/snapshot.py b/heatclient/osc/v1/snapshot.py index 52f9e016..bdefdb07 100644 --- a/heatclient/osc/v1/snapshot.py +++ b/heatclient/osc/v1/snapshot.py @@ -18,6 +18,7 @@ from cliff import command from cliff import lister +from cliff import show from openstackclient.common import exceptions as exc from openstackclient.common import utils @@ -27,7 +28,7 @@ class ListSnapshot(lister.Lister): - """List stack snapshots""" + """List stack snapshots.""" log = logging.getLogger(__name__ + ".ListSnapshot") @@ -134,3 +135,44 @@ def _restore_snapshot(self, heat_client, parsed_args): 'snapshot %(snapshot)s not found.') % {'stack': parsed_args.stack, 'snapshot': parsed_args.snapshot}) + + +class CreateSnapshot(show.ShowOne): + """Create stack snapshot.""" + + log = logging.getLogger(__name__ + ".CreateSnapshot") + + def get_parser(self, prog_name): + parser = super(CreateSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack') + ) + parser.add_argument( + '--name', + metavar='', + help=_('Name of snapshot') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + heat_client = self.app.client_manager.orchestration + + try: + data = heat_client.stacks.snapshot(parsed_args.stack, + parsed_args.name) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Stack not found: %s') + % parsed_args.stack) + + columns = [ + 'ID', + 'name', + 'status', + 'status_reason', + 'data', + 'creation_time' + ] + return (columns, utils.get_dict_properties(data, columns)) diff --git a/heatclient/tests/unit/osc/v1/test_snapshot.py b/heatclient/tests/unit/osc/v1/test_snapshot.py index ae23299c..838148e5 100644 --- a/heatclient/tests/unit/osc/v1/test_snapshot.py +++ b/heatclient/tests/unit/osc/v1/test_snapshot.py @@ -102,3 +102,46 @@ def test_snapshot_restore_error(self): parsed_args) self.assertEqual('Stack my_stack or snapshot my_snapshot not found.', str(error)) + + +class TestSnapshotCreate(TestStack): + get_response = { + "status": "IN_PROGRESS", + "name": "test_snapshot", + "status_reason": None, + "creation_time": "2015-11-09T04:35:38.534130", + "data": None, + "id": "108604fe-6d13-41b7-aa3a-79b6cf60c4ff" + } + + def setUp(self): + super(TestSnapshotCreate, self).setUp() + self.cmd = snapshot.CreateSnapshot(self.app, None) + + def test_snapshot_create(self): + arglist = ['my_stack', '--name', 'test_snapshot'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.stack_client.snapshot = mock.Mock( + return_value=self.get_response) + self.cmd.take_action(parsed_args) + self.stack_client.snapshot.assert_called_with( + 'my_stack', 'test_snapshot') + + def test_snapshot_create_no_name(self): + arglist = ['my_stack'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.stack_client.snapshot = mock.Mock( + return_value=self.get_response) + self.cmd.take_action(parsed_args) + self.stack_client.snapshot.assert_called_with( + 'my_stack', None) + + def test_snapshot_create_error(self): + arglist = ['my_stack', '--name', 'test_snapshot'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.stack_client.snapshot = mock.Mock( + side_effect=heat_exc.HTTPNotFound) + self.assertRaises( + exc.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index 9d7dd2ad..26b865b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,6 +57,7 @@ openstack.orchestration.v1 = stack_resource_signal = heatclient.osc.v1.resources:ResourceSignal stack_resume = heatclient.osc.v1.stack:ResumeStack stack_show = heatclient.osc.v1.stack:ShowStack + stack_snapshot_create = heatclient.osc.v1.snapshot:CreateSnapshot stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot stack_snapshot_restore = heatclient.osc.v1.snapshot:RestoreSnapshot stack_snapshot_show = heatclient.osc.v1.snapshot:ShowSnapshot From 7e8b0c825eaa42e549c6141123c3c75c306cb211 Mon Sep 17 00:00:00 2001 From: Amey Bhide Date: Sun, 8 Nov 2015 22:12:27 -0800 Subject: [PATCH 150/553] OSC plugin for stack snapshot delete This change implements "openstack stack snapshot delete" command Based from the existing heat commands: heat snapshot-delete Change-Id: Iaf701d2e58fe05dfa80b26a809dc21aeff8c1784 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/snapshot.py | 31 +++++++++++++++++++ heatclient/tests/unit/osc/v1/test_snapshot.py | 24 ++++++++++++++ setup.cfg | 1 + 3 files changed, 56 insertions(+) diff --git a/heatclient/osc/v1/snapshot.py b/heatclient/osc/v1/snapshot.py index bdefdb07..a46d31ad 100644 --- a/heatclient/osc/v1/snapshot.py +++ b/heatclient/osc/v1/snapshot.py @@ -176,3 +176,34 @@ def take_action(self, parsed_args): 'creation_time' ] return (columns, utils.get_dict_properties(data, columns)) + + +class DeleteSnapshot(command.Command): + """Delete stack snapshot.""" + log = logging.getLogger(__name__ + ".DeleteSnapshot") + + def get_parser(self, prog_name): + parser = super(DeleteSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack') + ) + parser.add_argument( + 'snapshot', + metavar='', + help=_('ID of stack snapshot') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + heat_client = self.app.client_manager.orchestration + try: + heat_client.stacks.snapshot_delete(parsed_args.stack, + parsed_args.snapshot) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Snapshot ID <%(snapshot_id)s> not found ' + 'for stack <%(stack_id)s>') + % {'snapshot_id': parsed_args.snapshot, + 'stack_id': parsed_args.stack}) diff --git a/heatclient/tests/unit/osc/v1/test_snapshot.py b/heatclient/tests/unit/osc/v1/test_snapshot.py index 838148e5..f4e05503 100644 --- a/heatclient/tests/unit/osc/v1/test_snapshot.py +++ b/heatclient/tests/unit/osc/v1/test_snapshot.py @@ -145,3 +145,27 @@ def test_snapshot_create_error(self): exc.CommandError, self.cmd.take_action, parsed_args) + + +class TestSnapshotDelete(TestStack): + def setUp(self): + super(TestSnapshotDelete, self).setUp() + self.cmd = snapshot.DeleteSnapshot(self.app, None) + + def test_snapshot_delete(self): + arglist = ['my_stack', 'snapshot_id'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.stack_client.snapshot_delete = mock.Mock() + self.cmd.take_action(parsed_args) + self.stack_client.snapshot_delete.assert_called_with( + 'my_stack', 'snapshot_id') + + def test_snapshot_delete_not_found(self): + arglist = ['my_stack', 'snapshot_id'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.stack_client.snapshot_delete = mock.Mock( + side_effect=heat_exc.HTTPNotFound()) + self.assertRaises( + exc.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index 26b865b3..b419b5ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,6 +58,7 @@ openstack.orchestration.v1 = stack_resume = heatclient.osc.v1.stack:ResumeStack stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_create = heatclient.osc.v1.snapshot:CreateSnapshot + stack_snapshot_delete = heatclient.osc.v1.snapshot:DeleteSnapshot stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot stack_snapshot_restore = heatclient.osc.v1.snapshot:RestoreSnapshot stack_snapshot_show = heatclient.osc.v1.snapshot:ShowSnapshot From 7627e1bae53747e1471caf632dfc1b6edf2e26cf Mon Sep 17 00:00:00 2001 From: Jay Dobies Date: Mon, 26 Oct 2015 14:21:26 -0400 Subject: [PATCH 151/553] Changes to support server-side environment resolution Adds support in template_utils to optionally produce an ordered list of environment file names and include the environment file contents in the files dict. This will be passed to the server to trigger the server-side handling of resolving overrides for multiple environments. The original behavior of merging the environments client-side is left in for backward compatibility purposes. Both are sent to the server. For mitaka and newer, the client-submitted environment is ignored and the merging is done server-side. For liberty and older, the environment_files list is ignored entirely and the passed in environment is used. Note that the merging, regardless if it is done client or server-side, is handled by the same code; it has not been copied or duplicated in the server. Change-Id: If187c8ca3d80008c21e8f6066d335ab0c30315e6 Implements: blueprint multi-environments Depends-On: Ibe46fd35de7988920c101a83259c06c8f8a3ed0b --- heatclient/common/template_utils.py | 85 +++++++++++++++++--- heatclient/tests/unit/test_template_utils.py | 36 +++++++++ heatclient/v1/shell.py | 30 +++++-- 3 files changed, 137 insertions(+), 14 deletions(-) diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index a14f35cc..fad243d7 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -189,17 +189,51 @@ def deep_update(old, new): def process_multiple_environments_and_files(env_paths=None, template=None, template_url=None, env_path_is_object=None, - object_request=None): - + object_request=None, + env_list_tracker=None): + """Reads one or more environment files. + + Reads in each specified environment file and returns a dictionary + of the filenames->contents (suitable for the files dict) + and the consolidated environment (after having applied the correct + overrides based on order). + + If a list is provided in the env_list_tracker parameter, the behavior + is altered to take advantage of server-side environment resolution. + Specifically, this means: + + * Populating env_list_tracker with an ordered list of environment file + URLs to be passed to the server + * Including the contents of each environment file in the returned + files dict, keyed by one of the URLs in env_list_tracker + + :param env_paths: list of paths to the environment files to load; if + None, empty results will be returned + :type env_paths: list or None + :param template: unused; only included for API compatibility + :param template_url: unused; only included for API compatibility + :param env_list_tracker: if specified, environment filenames will be + stored within + :type env_list_tracker: list or None + :return: tuple of files dict and a dict of the consolidated environment + :rtype: tuple + """ merged_files = {} merged_env = {} + # If we're keeping a list of environment files separately, include the + # contents of the files in the files dict + include_env_in_files = env_list_tracker is not None + if env_paths: for env_path in env_paths: - files, env = process_environment_and_files(env_path, template, - template_url, - env_path_is_object, - object_request) + files, env = process_environment_and_files( + env_path=env_path, + template=template, + template_url=template_url, + env_path_is_object=env_path_is_object, + object_request=object_request, + include_env_in_files=include_env_in_files) # 'files' looks like {"filename1": contents, "filename2": contents} # so a simple update is enough for merging @@ -209,12 +243,32 @@ def process_multiple_environments_and_files(env_paths=None, template=None, # not enough merged_env = deep_update(merged_env, env) + if env_list_tracker is not None: + env_url = utils.normalise_file_path_to_url(env_path) + env_list_tracker.append(env_url) + return merged_files, merged_env -def process_environment_and_files(env_path=None, template=None, - template_url=None, env_path_is_object=None, - object_request=None): +def process_environment_and_files(env_path=None, + template=None, + template_url=None, + env_path_is_object=None, + object_request=None, + include_env_in_files=False): + """Loads a single environment file. + + Returns an entry suitable for the files dict which maps the environment + filename to its contents. + + :param env_path: full path to the file to load + :type env_path: str or None + :param include_env_in_files: if specified, the raw environment file itself + will be included in the returned files dict + :type include_env_in_files: bool + :return: tuple of files dict and the loaded environment as a dict + :rtype: (dict, dict) + """ files = {} env = {} @@ -234,6 +288,10 @@ def process_environment_and_files(env_path=None, template=None, env_url = utils.normalise_file_path_to_url(env_path) 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( @@ -246,6 +304,15 @@ def process_environment_and_files(env_path=None, template=None, def resolve_environment_urls(resource_registry, files, env_base_url, is_object=False, object_request=None): + """Handles any resource URLs specified in an environment. + + :param resource_registry: mapping of type name to template filename + :type resource_registry: dict + :param files: dict to store loaded file contents into + :type files: dict + :param env_base_url: base URL to look in when loading files + :type env_base_url: str or None + """ if resource_registry is None: return diff --git a/heatclient/tests/unit/test_template_utils.py b/heatclient/tests/unit/test_template_utils.py index 1e1eee94..fe71e465 100644 --- a/heatclient/tests/unit/test_template_utils.py +++ b/heatclient/tests/unit/test_template_utils.py @@ -352,6 +352,42 @@ def object_request(method, object_url): self.assertEqual(self.template_a.decode('utf-8'), files['http://no.where/path/to/b/a.yaml']) + def test_process_multiple_environments_and_files_tracker(self): + # Setup + self.m.StubOutWithMock(request, 'urlopen') + env_file1 = '/home/my/dir/env1.yaml' + + env1 = b''' + parameters: + "param1": "value1" + resource_registry: + "OS::Thingy1": "file:///home/b/a.yaml" + ''' + request.urlopen('file://%s' % env_file1).AndReturn( + six.BytesIO(env1)) + request.urlopen('file:///home/b/a.yaml').AndReturn( + six.BytesIO(self.template_a)) + request.urlopen('file:///home/b/a.yaml').AndReturn( + six.BytesIO(self.template_a)) + self.m.ReplayAll() + + # Test + env_file_list = [] + files, env = template_utils.process_multiple_environments_and_files( + [env_file1], env_list_tracker=env_file_list) + + # Verify + expected_env = {'parameters': {'param1': 'value1'}, + 'resource_registry': + {'OS::Thingy1': 'file:///home/b/a.yaml'} + } + self.assertEqual(expected_env, env) + + self.assertEqual(self.template_a.decode('utf-8'), + files['file:///home/b/a.yaml']) + + self.assertEqual(['file:///home/my/dir/env1.yaml'], env_file_list) + def test_global_files(self): url = 'file:///home/b/a.yaml' env = ''' diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 0a119b4f..b2dda6a3 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -105,8 +105,9 @@ def do_stack_create(hc, args): args.template_url, args.template_object, _authenticated_fetcher(hc)) + env_files_list = [] env_files, env = template_utils.process_multiple_environments_and_files( - env_paths=args.environment_file) + env_paths=args.environment_file, env_list_tracker=env_files_list) if args.create_timeout: logger.warning(_LW('%(arg1)s is deprecated, ' @@ -130,6 +131,10 @@ def do_stack_create(hc, args): 'environment': env } + # If one or more environments is found, pass the listing to the server + if env_files_list: + fields['environment_files'] = env_files_list + if args.tags: fields['tags'] = args.tags timeout = args.timeout or args.create_timeout @@ -242,8 +247,9 @@ def do_stack_preview(hc, args): args.template_url, args.template_object, _authenticated_fetcher(hc)) + env_files_list = [] env_files, env = template_utils.process_multiple_environments_and_files( - env_paths=args.environment_file) + env_paths=args.environment_file, env_list_tracker=env_files_list) fields = { 'stack_name': args.name, @@ -258,6 +264,10 @@ def do_stack_preview(hc, args): 'environment': env } + # If one or more environments is found, pass the listing to the server + if env_files_list: + fields['environment_files'] = env_files_list + if args.tags: fields['tags'] = args.tags @@ -454,9 +464,9 @@ def do_stack_update(hc, args): args.template_object, _authenticated_fetcher(hc), existing=args.existing) - + env_files_list = [] env_files, env = template_utils.process_multiple_environments_and_files( - env_paths=args.environment_file) + env_paths=args.environment_file, env_list_tracker=env_files_list) if args.pre_update: template_utils.hooks_to_env(env, args.pre_update, 'pre-update') @@ -473,6 +483,10 @@ def do_stack_update(hc, args): 'environment': env } + # If one or more environments is found, pass the listing to the server + if env_files_list: + fields['environment_files'] = env_files_list + if args.tags: fields['tags'] = args.tags if args.timeout: @@ -820,8 +834,10 @@ def do_template_validate(hc, args): args.template_object, _authenticated_fetcher(hc)) + env_files_list = [] env_files, env = template_utils.process_multiple_environments_and_files( - env_paths=args.environment_file) + env_paths=args.environment_file, env_list_tracker=env_files_list) + fields = { 'template': template, 'parameters': utils.format_parameters(args.parameters), @@ -832,6 +848,10 @@ def do_template_validate(hc, args): if args.ignore_errors: fields['ignore_errors'] = args.ignore_errors + # If one or more environments is found, pass the listing to the server + if env_files_list: + fields['environment_files'] = env_files_list + if args.show_nested: fields['show_nested'] = args.show_nested From 1e9b86b9d3766063d98b794ed2a753db006d5b21 Mon Sep 17 00:00:00 2001 From: Thomas Herve Date: Wed, 17 Feb 2016 15:23:06 +0100 Subject: [PATCH 152/553] Remove the only-value flag of output-show We recently introduced a new flag to output-show, while changing the default behavior to display the key and the description. This removes this recently added flag, and add a new flag used to do the new display. Change-Id: Iafcfaad969356e5029e6a3ac51b52d7592146528 Closes-Bug: #1539783 --- heatclient/tests/unit/test_shell.py | 36 +++++++++++++++-------------- heatclient/v1/shell.py | 24 +++++++++++-------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 9457f808..3a62b6b8 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -2856,7 +2856,7 @@ def test_output_show_all(self): resp_dict1)) self.m.ReplayAll() - list_text = self.shell('output-show teststack/1 key --all') + list_text = self.shell('output-show --with-detail teststack/1 --all') required = [ 'output_key', 'output_value', @@ -2892,7 +2892,7 @@ def test_output_show(self): '/stacks/teststack/1/outputs/key').AndReturn((resp, resp_dict)) self.m.ReplayAll() - resp = self.shell('output-show teststack/1 key') + resp = self.shell('output-show --with-detail teststack/1 key') required = [ 'output_key', 'output_value', @@ -2948,7 +2948,7 @@ def test_output_show_api_400_error(self): jsonutils.dumps(stack_dict)), stack_dict)) self.m.ReplayAll() - resp = self.shell('output-show teststack/1 key') + resp = self.shell('output-show --with-detail teststack/1 key') required = [ 'output_key', 'output_value', @@ -2960,11 +2960,11 @@ def test_output_show_api_400_error(self): for r in required: self.assertRegexpMatches(resp, r) - def test_output_show_output1(self): + def test_output_show_output1_with_detail(self): self.register_keystone_auth_fixture() self._output_fake_response('output1') - list_text = self.shell('output-show teststack/1 output1') + list_text = self.shell('output-show teststack/1 output1 --with-detail') required = [ 'output_key', 'output_value', @@ -2976,26 +2976,27 @@ def test_output_show_output1(self): for r in required: self.assertRegexpMatches(list_text, r) - def test_output_show_output1_only_value(self): + def test_output_show_output1(self): self.register_keystone_auth_fixture() self._output_fake_response('output1') - list_text = self.shell('output-show -v -F raw teststack/1 output1') + list_text = self.shell('output-show -F raw teststack/1 output1') self.assertEqual('value1\n', list_text) - def test_output_show_output2_raw_only_value(self): + def test_output_show_output2_raw(self): self.register_keystone_auth_fixture() self._output_fake_response('output2') - list_text = self.shell('output-show -F raw -v teststack/1 output2') + list_text = self.shell('output-show -F raw teststack/1 output2') self.assertEqual('[\n "output", \n "value", \n "2"\n]\n', list_text) - def test_output_show_output2_raw(self): + def test_output_show_output2_raw_with_detail(self): self.register_keystone_auth_fixture() self._output_fake_response('output2') - list_text = self.shell('output-show -F raw teststack/1 output2') + list_text = self.shell('output-show -F raw --with-detail ' + 'teststack/1 output2') required = [ 'output_key', 'output_value', @@ -3007,19 +3008,20 @@ def test_output_show_output2_raw(self): for r in required: self.assertRegexpMatches(list_text, r) - def test_output_show_output2_json_only_value(self): + def test_output_show_output2_json(self): self.register_keystone_auth_fixture() self._output_fake_response('output2') - list_text = self.shell('output-show -F json -v teststack/1 output2') + list_text = self.shell('output-show -F json teststack/1 output2') self.assertEqual('[\n "output", \n "value", \n "2"\n]\n', list_text) - def test_output_show_output2_json(self): + def test_output_show_output2_json_with_detail(self): self.register_keystone_auth_fixture() self._output_fake_response('output2') - list_text = self.shell('output-show -F json teststack/1 output2') + list_text = self.shell('output-show -F json --with-detail ' + 'teststack/1 output2') required = [ 'output_key', 'output_value', @@ -3031,11 +3033,11 @@ def test_output_show_output2_json(self): for r in required: self.assertRegexpMatches(list_text, r) - def test_output_show_unicode_output_only_value(self): + def test_output_show_unicode_output(self): self.register_keystone_auth_fixture() self._output_fake_response('output_uni') - list_text = self.shell('output-show -v teststack/1 output_uni') + list_text = self.shell('output-show teststack/1 output_uni') self.assertEqual(u'"test\u2665"\n', list_text) def test_output_show_error(self): diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 0a119b4f..a56da63a 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -662,8 +662,9 @@ def do_output_list(hc, args): default='json') @utils.arg('-a', '--all', default=False, action='store_true', help=_('Display all stack outputs.')) -@utils.arg('-v', '--only-value', default=False, action="store_true", - help=_('Returns only output value in specified format.')) +@utils.arg('--with-detail', default=False, action="store_true", + help=_('Enable detail information presented, like ' + 'key and description.')) def do_output_show(hc, args): """Show a specific stack output.""" def resolve_output(output_key): @@ -691,7 +692,14 @@ def show_output(output): if 'output_error' in output['output']: msg = _("Output error: %s") % output['output']['output_error'] raise exc.CommandError(msg) - if args.only_value: + if args.with_detail: + formatters = { + 'output_value': (lambda x: utils.json_formatter(x) + if args.format == 'json' + else x) + } + utils.print_dict(output['output'], formatters=formatters) + else: if (args.format == 'json' or isinstance(output['output']['output_value'], dict) or isinstance(output['output']['output_value'], list)): @@ -699,15 +707,11 @@ def show_output(output): utils.json_formatter(output['output']['output_value'])) else: print(output['output']['output_value']) - else: - formatters = { - 'output_value': (lambda x: utils.json_formatter(x) - if args.format == 'json' - else x) - } - utils.print_dict(output['output'], formatters=formatters) if args.all: + if args.output: + raise exc.CommandError( + _("Can't specify an output name and the --all flag")) try: outputs = hc.stacks.output_list(args.id) resolved = False From d67af77f200e8274c1249531261bc9b64e322a72 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Jan 2016 21:58:26 +0000 Subject: [PATCH 153/553] OpenstackClient plugin for deployment create This change implements the 'openstack software deployment create' command. Blueprint: heat-support-python-openstackclient Change-Id: I757ca2896e1e4a0ad96a5f34c34205e099715801 --- heatclient/osc/v1/software_deployment.py | 105 ++++++++ .../unit/osc/v1/test_software_deployment.py | 235 ++++++++++++++++-- setup.cfg | 1 + 3 files changed, 317 insertions(+), 24 deletions(-) diff --git a/heatclient/osc/v1/software_deployment.py b/heatclient/osc/v1/software_deployment.py index 67d1c401..4451f83b 100644 --- a/heatclient/osc/v1/software_deployment.py +++ b/heatclient/osc/v1/software_deployment.py @@ -22,10 +22,115 @@ from openstackclient.common import exceptions as exc from openstackclient.common import utils +from heatclient.common import deployment_utils +from heatclient.common import format_utils +from heatclient.common import utils as heat_utils from heatclient import exc as heat_exc from heatclient.openstack.common._i18n import _ +class CreateDeployment(format_utils.YamlFormat): + """Create a software deployment.""" + + log = logging.getLogger(__name__ + '.CreateDeployment') + + def get_parser(self, prog_name): + parser = super(CreateDeployment, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the derived config associated with this ' + 'deployment. This is used to apply a sort order to the ' + 'list of configurations currently deployed to the server.') + ) + parser.add_argument( + '--input-value', + metavar='', + action='append', + help=_('Input value to set on the deployment. This can be ' + 'specified multiple times.') + ) + parser.add_argument( + '--action', + metavar='', + default='UPDATE', + help=_('Name of an action for this deployment. This can be a ' + 'custom action, or one of CREATE, UPDATE, DELETE, SUSPEND, ' + 'RESUME. Default is UPDATE') + ) + parser.add_argument( + '--config', + metavar='', + help=_('ID of the configuration to deploy') + ) + parser.add_argument( + '--server', + metavar='', + required=True, + help=_('ID of the server being deployed to') + ) + parser.add_argument( + '--signal-transport', + metavar='', + default='TEMP_URL_SIGNAL', + help=_('How the server should signal to heat with the deployment ' + 'output values. TEMP_URL_SIGNAL will create a Swift ' + 'TempURL to be signaled via HTTP PUT. ZAQAR_SIGNAL will ' + 'create a dedicated zaqar queue to be signaled using the ' + 'provided keystone credentials.NO_SIGNAL will result in ' + 'the resource going to the COMPLETE state without waiting ' + 'for any signal') + ) + parser.add_argument( + '--container', + metavar='', + help=_('Optional name of container to store TEMP_URL_SIGNAL ' + 'objects in. If not specified a container will be created ' + 'with a name derived from the DEPLOY_NAME') + ) + parser.add_argument( + '--timeout', + type=int, + default=60, + help=_('Deployment timeout in minutes') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + config = {} + if parsed_args.config: + try: + config = client.software_configs.get(parsed_args.config) + except heat_exc.HTTPNotFound: + msg = (_('Software configuration not found: %s') % + parsed_args.config) + raise exc.CommandError(msg) + + derived_params = deployment_utils.build_derived_config_params( + parsed_args.action, + config, + parsed_args.name, + heat_utils.format_parameters(parsed_args.input_value, False), + parsed_args.server, + parsed_args.signal_transport, + signal_id=deployment_utils.build_signal_id(client, parsed_args) + ) + derived_config = client.software_configs.create(**derived_params) + + sd = client.software_deployments.create( + config_id=derived_config.id, + server_id=parsed_args.server, + action=parsed_args.action, + status='IN_PROGRESS' + ) + + return zip(*sorted(sd.to_dict().items())) + + class DeleteDeployment(command.Command): """Delete software deployment(s) and correlative config(s).""" diff --git a/heatclient/tests/unit/osc/v1/test_software_deployment.py b/heatclient/tests/unit/osc/v1/test_software_deployment.py index 7157cf06..64273603 100644 --- a/heatclient/tests/unit/osc/v1/test_software_deployment.py +++ b/heatclient/tests/unit/osc/v1/test_software_deployment.py @@ -11,6 +11,7 @@ # under the License. # +import copy import mock from openstackclient.common import exceptions as exc @@ -18,16 +19,202 @@ from heatclient import exc as heat_exc from heatclient.osc.v1 import software_deployment from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes +from heatclient.v1 import software_configs from heatclient.v1 import software_deployments class TestDeployment(orchestration_fakes.TestOrchestrationv1): def setUp(self): super(TestDeployment, self).setUp() - sd_client = self.app.client_manager.orchestration.software_deployments - self.mock_client = sd_client - sc_client = self.app.client_manager.orchestration.software_configs - self.mock_config_client = sc_client + self.mock_client = self.app.client_manager.orchestration + self.config_client = self.mock_client.software_configs + self.sd_client = self.mock_client.software_deployments + + +class TestDeploymentCreate(TestDeployment): + + server_id = '1234' + config_id = '5678' + deploy_id = '910' + + config = { + 'name': 'my_deploy', + 'group': 'strict', + 'config': '#!/bin/bash', + 'inputs': [], + 'outputs': [], + 'options': [], + 'id': config_id, + } + + deployment = { + 'server_id': server_id, + 'input_values': {}, + 'action': 'UPDATE', + 'status': 'IN_PROGRESS', + 'status_reason': None, + 'signal_id': 'signal_id', + 'config_id': config_id, + 'id': deploy_id, + } + + config_defaults = { + 'group': 'Heat::Ungrouped', + 'config': '', + 'options': {}, + 'inputs': [ + { + 'name': 'deploy_server_id', + 'description': 'ID of the server being deployed to', + 'type': 'String', + 'value': server_id, + }, + { + 'name': 'deploy_action', + 'description': 'Name of the current action being deployed', + 'type': 'String', + 'value': 'UPDATE', + }, + { + 'name': 'deploy_signal_transport', + 'description': 'How the server should signal to heat with the ' + 'deployment output values.', + 'type': 'String', + 'value': 'TEMP_URL_SIGNAL', + }, + { + 'name': 'deploy_signal_id', + 'description': 'ID of signal to use for signaling output ' + 'values', + 'type': 'String', + 'value': 'signal_id', + }, + { + 'name': 'deploy_signal_verb', + 'description': 'HTTP verb to use for signaling output values', + 'type': 'String', + 'value': 'PUT', + }, + ], + 'outputs': [], + 'name': 'my_deploy', + } + + deploy_defaults = { + 'config_id': config_id, + 'server_id': server_id, + 'action': 'UPDATE', + 'status': 'IN_PROGRESS', + } + + def setUp(self): + super(TestDeploymentCreate, self).setUp() + self.cmd = software_deployment.CreateDeployment(self.app, None) + self.config_client.create = mock.MagicMock(return_value=( + software_configs.SoftwareConfig(None, self.config))) + self.config_client.get = mock.MagicMock(return_value=( + software_configs.SoftwareConfig(None, self.config))) + self.sd_client.create = mock.MagicMock(return_value=( + software_deployments.SoftwareDeployment(None, self.deployment))) + + @mock.patch('heatclient.common.deployment_utils.build_signal_id', + return_value='signal_id') + def test_deployment_create(self, mock_build): + arglist = ['my_deploy', '--server', self.server_id] + expected_cols = ('action', 'config_id', 'id', 'input_values', + 'server_id', 'signal_id', 'status', 'status_reason') + expected_data = ('UPDATE', self.config_id, self.deploy_id, {}, + self.server_id, 'signal_id', 'IN_PROGRESS', None) + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.config_client.create.assert_called_with(**self.config_defaults) + self.sd_client.create.assert_called_with( + **self.deploy_defaults) + self.assertEqual(expected_cols, columns) + self.assertEqual(expected_data, data) + + @mock.patch('heatclient.common.deployment_utils.build_signal_id', + return_value='signal_id') + def test_deployment_create_with_config(self, mock_build): + arglist = ['my_deploy', '--server', self.server_id, + '--config', self.config_id] + config = copy.deepcopy(self.config_defaults) + config['config'] = '#!/bin/bash' + config['group'] = 'strict' + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.config_client.get.assert_called_with(self.config_id) + self.config_client.create.assert_called_with(**config) + self.sd_client.create.assert_called_with( + **self.deploy_defaults) + + def test_deployment_create_config_not_found(self): + arglist = ['my_deploy', '--server', self.server_id, + '--config', 'bad_id'] + self.config_client.get.side_effect = heat_exc.HTTPNotFound + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + def test_deployment_create_no_signal(self): + arglist = ['my_deploy', '--server', self.server_id, + '--signal-transport', 'NO_SIGNAL'] + config = copy.deepcopy(self.config_defaults) + config['inputs'] = config['inputs'][:-2] + config['inputs'][2]['value'] = 'NO_SIGNAL' + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.config_client.create.assert_called_with(**config) + self.sd_client.create.assert_called_with( + **self.deploy_defaults) + + @mock.patch('heatclient.common.deployment_utils.build_signal_id', + return_value='signal_id') + def test_deployment_create_invalid_signal_transport(self, mock_build): + arglist = ['my_deploy', '--server', self.server_id, + '--signal-transport', 'A'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(heat_exc.CommandError, + self.cmd.take_action, parsed_args) + + @mock.patch('heatclient.common.deployment_utils.build_signal_id', + return_value='signal_id') + def test_deployment_create_input_value(self, mock_build): + arglist = ['my_deploy', '--server', self.server_id, + '--input-value', 'foo=bar'] + config = copy.deepcopy(self.config_defaults) + config['inputs'].insert( + 0, {'name': 'foo', 'type': 'String', 'value': 'bar'}) + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.config_client.create.assert_called_with(**config) + self.sd_client.create.assert_called_with( + **self.deploy_defaults) + + @mock.patch('heatclient.common.deployment_utils.build_signal_id', + return_value='signal_id') + def test_deployment_create_action(self, mock_build): + arglist = ['my_deploy', '--server', self.server_id, + '--action', 'DELETE'] + config = copy.deepcopy(self.config_defaults) + config['inputs'][1]['value'] = 'DELETE' + deploy = copy.deepcopy(self.deploy_defaults) + deploy['action'] = 'DELETE' + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.config_client.create.assert_called_with(**config) + self.sd_client.create.assert_called_with(**deploy) class TestDeploymentDelete(TestDeployment): @@ -39,27 +226,27 @@ def setUp(self): def test_deployment_delete_success(self): arglist = ['test_deployment'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.get = mock.Mock() - self.mock_client.delete = mock.Mock() + self.sd_client.get = mock.Mock() + self.sd_client.delete = mock.Mock() self.cmd.take_action(parsed_args) - self.mock_client.delete.assert_called_with( + self.sd_client.delete.assert_called_with( deployment_id='test_deployment') def test_deployment_delete_multiple(self): arglist = ['test_deployment', 'test_deployment2'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.get = mock.Mock() - self.mock_client.delete = mock.Mock() + self.sd_client.get = mock.Mock() + self.sd_client.delete = mock.Mock() self.cmd.take_action(parsed_args) - self.mock_client.delete.assert_has_calls( + self.sd_client.delete.assert_has_calls( [mock.call(deployment_id='test_deployment'), mock.call(deployment_id='test_deployment2')]) def test_deployment_delete_not_found(self): arglist = ['test_deployment', 'test_deployment2'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.delete = mock.Mock() - self.mock_client.delete.side_effect = heat_exc.HTTPNotFound() + self.sd_client.delete = mock.Mock() + self.sd_client.delete.side_effect = heat_exc.HTTPNotFound() error = self.assertRaises( exc.CommandError, self.cmd.take_action, parsed_args) self.assertIn("Unable to delete 2 of the 2 deployments.", str(error)) @@ -67,8 +254,8 @@ def test_deployment_delete_not_found(self): def test_deployment_config_delete_failed(self): arglist = ['test_deployment'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_config_client.delete = mock.Mock() - self.mock_config_client.delete.side_effect = heat_exc.HTTPNotFound() + self.config_client.delete = mock.Mock() + self.config_client.delete.side_effect = heat_exc.HTTPNotFound() error = self.assertRaises( exc.CommandError, self.cmd.take_action, parsed_args) self.assertEqual("Unable to delete 1 of the 1 deployments.", @@ -108,13 +295,13 @@ class TestDeploymentList(TestDeployment): def setUp(self): super(TestDeploymentList, self).setUp() self.cmd = software_deployment.ListDeployment(self.app, None) - self.mock_client.list = mock.MagicMock(return_value=[self.data]) + self.sd_client.list = mock.MagicMock(return_value=[self.data]) def test_deployment_list(self): arglist = [] parsed_args = self.check_parser(self.cmd, arglist, []) columns, data = self.cmd.take_action(parsed_args) - self.mock_client.list.assert_called_with() + self.sd_client.list.assert_called_with() self.assertEqual(self.columns, columns) def test_deployment_list_server(self): @@ -123,7 +310,7 @@ def test_deployment_list_server(self): arglist = ['--server', 'ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5'] parsed_args = self.check_parser(self.cmd, arglist, []) columns, data = self.cmd.take_action(parsed_args) - self.mock_client.list.assert_called_with(**kwargs) + self.sd_client.list.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) def test_deployment_list_long(self): @@ -135,7 +322,7 @@ def test_deployment_list_long(self): columns, data = self.cmd.take_action(parsed_args) - self.mock_client.list.assert_called_with(**kwargs) + self.sd_client.list.assert_called_with(**kwargs) self.assertEqual(cols, columns) @@ -163,11 +350,11 @@ def test_deployment_show(self): 'updated_time', 'status', 'status_reason', 'input_values', 'action'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.get = mock.Mock( + self.sd_client.get = mock.Mock( return_value=software_deployments.SoftwareDeployment( None, self.get_response)) columns, data = self.cmd.take_action(parsed_args) - self.mock_client.get.assert_called_with(**{ + self.sd_client.get.assert_called_with(**{ 'deployment_id': 'my_deployment', }) self.assertEqual(cols, columns) @@ -178,11 +365,11 @@ def test_deployment_show_long(self): 'updated_time', 'status', 'status_reason', 'input_values', 'action', 'output_values'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.get = mock.Mock( + self.sd_client.get = mock.Mock( return_value=software_deployments.SoftwareDeployment( None, self.get_response)) columns, data = self.cmd.take_action(parsed_args) - self.mock_client.get.assert_called_once_with(**{ + self.sd_client.get.assert_called_once_with(**{ 'deployment_id': 'my_deployment', }) self.assertEqual(cols, columns) @@ -190,8 +377,8 @@ def test_deployment_show_long(self): def test_deployment_not_found(self): arglist = ['my_deployment'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.get = mock.Mock() - self.mock_client.get.side_effect = heat_exc.HTTPNotFound() + self.sd_client.get = mock.Mock() + self.sd_client.get.side_effect = heat_exc.HTTPNotFound() self.assertRaises( exc.CommandError, self.cmd.take_action, diff --git a/setup.cfg b/setup.cfg index b419b5ae..c0024c24 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,7 @@ openstack.orchestration.v1 = software_config_delete = heatclient.osc.v1.software_config:DeleteConfig software_config_list = heatclient.osc.v1.software_config:ListConfig software_config_show = heatclient.osc.v1.software_config:ShowConfig + software_deployment_create = heatclient.osc.v1.software_deployment:CreateDeployment software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment software_deployment_list = heatclient.osc.v1.software_deployment:ListDeployment software_deployment_show = heatclient.osc.v1.software_deployment:ShowDeployment From 319046d2c549c7dc4f8fdb3c7b670814f57a39da Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 18 Feb 2016 00:41:27 +0000 Subject: [PATCH 154/553] Updated from global requirements Change-Id: I98812390a29da86e0b529972fcea1301b51f09ca --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4c3700e8..9af58f7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.4.0 # Apache-2.0 python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 python-swiftclient>=2.2.0 # Apache-2.0 -python-openstackclient>=2.0.0 # Apache-2.0 +python-openstackclient>=2.1.0 # Apache-2.0 PyYAML>=3.1.0 # MIT requests!=2.9.0,>=2.8.1 # Apache-2.0 six>=1.9.0 # MIT From 69f41bceae632253d52cc38c3d4d0adff7ea55e5 Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Wed, 2 Dec 2015 12:07:57 -0600 Subject: [PATCH 155/553] Add stack hook poll and clear to openstack client Refactor existing hook helper fuctions into utilites. based upon heat clis: heat hook-poll hest hook-clear Change-Id: Ib46634cc62369fb5932dcd0967ae492446c79a88 Blueprint: heat-support-python-openstackclient --- heatclient/common/hook_utils.py | 78 ++++++++++++ heatclient/osc/v1/stack.py | 133 ++++++++++++++++++++ heatclient/tests/unit/osc/v1/test_stack.py | 138 +++++++++++++++++++++ heatclient/tests/unit/v1/test_hooks.py | 5 +- heatclient/v1/shell.py | 61 +-------- setup.cfg | 2 + 6 files changed, 360 insertions(+), 57 deletions(-) create mode 100644 heatclient/common/hook_utils.py diff --git a/heatclient/common/hook_utils.py b/heatclient/common/hook_utils.py new file mode 100644 index 00000000..5cef4342 --- /dev/null +++ b/heatclient/common/hook_utils.py @@ -0,0 +1,78 @@ +# 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 fnmatch +import logging + +import heatclient.exc as exc + +from heatclient.openstack.common._i18n import _ +from heatclient.openstack.common._i18n import _LE + +logger = logging.getLogger(__name__) + + +def clear_hook(hc, stack_id, resource_name, hook_type): + try: + hc.resources.signal( + stack_id=stack_id, + resource_name=resource_name, + data={'unset_hook': hook_type}) + except exc.HTTPNotFound: + logger.error( + _LE("Stack %(stack)s or resource %(resource)s" + "not found for hook %(hook_type)"), + {'resource': resource_name, 'stack': stack_id, + 'hook_type': hook_type}) + + +def clear_wildcard_hooks(hc, stack_id, stack_patterns, hook_type, + resource_pattern): + if stack_patterns: + for resource in hc.resources.list(stack_id): + res_name = resource.resource_name + if fnmatch.fnmatchcase(res_name, stack_patterns[0]): + nested_stack = hc.resources.get( + stack_id=stack_id, + resource_name=res_name) + clear_wildcard_hooks( + hc, + nested_stack.physical_resource_id, + stack_patterns[1:], hook_type, resource_pattern) + else: + for resource in hc.resources.list(stack_id): + res_name = resource.resource_name + if fnmatch.fnmatchcase(res_name, resource_pattern): + clear_hook(hc, stack_id, res_name, hook_type) + + +def get_hook_type_via_status(hc, stack_id): + # Figure out if the hook should be pre-create or pre-update based + # on the stack status, also sanity assertions that we're in-progress. + try: + stack = hc.stacks.get(stack_id=stack_id) + except exc.HTTPNotFound: + raise exc.CommandError(_('Stack not found: %s') % stack_id) + else: + if 'IN_PROGRESS' not in stack.stack_status: + raise exc.CommandError(_('Stack status %s not IN_PROGRESS') % + stack.stack_status) + + if 'CREATE' in stack.stack_status: + hook_type = 'pre-create' + elif 'UPDATE' in stack.stack_status: + hook_type = 'pre-update' + else: + raise exc.CommandError(_('Unexpected stack status %s, ' + 'only create/update supported') + % stack.stack_status) + return hook_type diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 98f417e3..cd67ace6 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -26,7 +26,9 @@ import six from six.moves.urllib import request +from heatclient.common import event_utils from heatclient.common import format_utils +from heatclient.common import hook_utils from heatclient.common import http from heatclient.common import template_utils from heatclient.common import utils as heat_utils @@ -1060,3 +1062,134 @@ def take_action(self, parsed_args): ['check_complete'], ['check_failed'] ) + + +class StackHookPoll(lister.Lister): + '''List resources with pending hook for a stack.''' + + log = logging.getLogger(__name__ + '.StackHookPoll') + + def get_parser(self, prog_name): + parser = super(StackHookPoll, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Stack to display (name or ID)') + ) + parser.add_argument( + '--nested-depth', + metavar='', + help=_('Depth of nested stacks from which to display hooks') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + heat_client = self.app.client_manager.orchestration + return _hook_poll( + parsed_args, + heat_client + ) + + +def _hook_poll(args, heat_client): + """List resources with pending hook for a stack.""" + + # 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 + # can be pending + # 2. There is an event for a resource associated with hitting a hook + # 3. There is not an event associated with clearing the hook in step(2) + # + # So, essentially, this ends up being a specially filtered type of event + # listing, because all hook status is exposed via events. In future + # we might consider exposing some more efficient interface via the API + # to reduce the expense of this brute-force polling approach + columns = ['ID', 'Resource Status Reason', 'Resource Status', 'Event Time'] + + if args.nested_depth: + try: + nested_depth = int(args.nested_depth) + except ValueError: + msg = _("--nested-depth invalid value %s") % args.nested_depth + raise exc.CommandError(msg) + columns.append('Stack Name') + else: + nested_depth = 0 + + hook_type = hook_utils.get_hook_type_via_status(heat_client, args.stack) + event_args = {'sort_dir': 'asc'} + hook_events = event_utils.get_hook_events( + heat_client, stack_id=args.stack, event_args=event_args, + nested_depth=nested_depth, hook_type=hook_type) + + if len(hook_events) >= 1: + if hasattr(hook_events[0], 'resource_name'): + columns.insert(0, 'Resource Name') + else: + columns.insert(0, 'Logical Resource ID') + + rows = (utils.get_item_properties(h, columns) for h in hook_events) + return (columns, rows) + + +class StackHookClear(command.Command): + """Clear resource hooks on a given stack.""" + + log = logging.getLogger(__name__ + '.StackHookClear') + + def get_parser(self, prog_name): + parser = super(StackHookClear, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Stack to display (name or ID)') + ) + parser.add_argument( + '--pre-create', + action='store_true', + help=_('Clear the pre-create hooks') + ) + parser.add_argument( + '--pre-update', + action='store_true', + help=_('Clear the pre-update hooks') + ) + parser.add_argument( + 'hook', + metavar='', + nargs='+', + help=_('Resource names with hooks to clear. Resources ' + 'in nested stacks can be set using slash as a separator: ' + 'nested_stack/another/my_resource. You can use wildcards ' + 'to match multiple stacks or resources: ' + 'nested_stack/an*/*_resource') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + heat_client = self.app.client_manager.orchestration + return _hook_clear( + parsed_args, + heat_client + ) + + +def _hook_clear(args, heat_client): + """Clear resource hooks on a given stack.""" + if args.pre_create: + hook_type = 'pre-create' + elif args.pre_update: + hook_type = 'pre-update' + else: + hook_type = hook_utils.get_hook_type_via_status(heat_client, + args.stack) + + for hook_string in args.hook: + hook = [b for b in hook_string.split('/') if b] + resource_pattern = hook[-1] + stack_id = args.stack + + hook_utils.clear_wildcard_hooks(heat_client, stack_id, hook[:-1], + hook_type, resource_pattern) diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index 7a1fd367..318eca3a 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -26,6 +26,8 @@ from heatclient.osc.v1 import stack from heatclient.tests import inline_templates from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes +from heatclient.v1 import events +from heatclient.v1 import resources from heatclient.v1 import stacks load_tests = testscenarios.load_tests_apply_scenarios @@ -1074,3 +1076,139 @@ def test_stack_check_wait_error(self): def test_stack_check_exception(self): self._test_stack_action_exception() + + +class TestStackHookPoll(TestStack): + + stack = stacks.Stack(None, { + "id": '1234', + "stack_name": 'my_stack', + "creation_time": "2013-08-04T20:57:55Z", + "updated_time": "2013-08-04T20:57:55Z", + "stack_status": "CREATE_IN_PROGRESS" + }) + resource = resources.Resource(None, { + 'resource_name': 'resource1', + 'links': [{'href': 'http://heat.example.com:8004/resource1', + 'rel': 'self'}, + {'href': 'http://192.168.27.100:8004/my_stack', + 'rel': 'stack'}], + 'logical_resource_id': 'random_group', + 'creation_time': '2015-12-03T16:50:56', + 'resource_status': 'INIT_COMPLETE', + 'updated_time': '2015-12-03T16:50:56', + 'required_by': [], + 'resource_status_reason': '', + 'physical_resource_id': '', + 'resource_type': 'OS::Heat::ResourceGroup', + 'id': '1111' + }) + columns = ['ID', 'Resource Status Reason', 'Resource Status', + 'Event Time'] + event0 = events.Event(manager=None, info={ + 'resource_name': 'my_stack', + 'event_time': '2015-12-02T16:50:56', + 'logical_resource_id': 'my_stack', + 'resource_status': 'CREATE_IN_PROGRESS', + 'resource_status_reason': 'Stack CREATE started', + 'id': '1234' + }) + event1 = events.Event(manager=None, info={ + 'resource_name': 'resource1', + 'event_time': '2015-12-03T19:59:58', + 'logical_resource_id': 'resource1', + 'resource_status': 'INIT_COMPLETE', + 'resource_status_reason': + 'CREATE paused until Hook pre-create is cleared', + 'id': '1111' + }) + row1 = ('resource1', + '1111', + 'CREATE paused until Hook pre-create is cleared', + 'INIT_COMPLETE', + '2015-12-03T19:59:58' + ) + + def setUp(self): + super(TestStackHookPoll, self).setUp() + self.cmd = stack.StackHookPoll(self.app, None) + self.mock_client.stacks.get = mock.Mock( + return_value=self.stack) + self.mock_client.events.list = mock.Mock( + return_value=[self.event0, self.event1]) + self.mock_client.resources.list = mock.Mock( + return_value=[self.resource]) + + def test_hook_poll(self): + expected_columns = ['Resource Name'] + self.columns + expected_rows = [self.row1] + arglist = ['my_stack'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + self.assertEqual(expected_rows, list(rows)) + self.assertEqual(expected_columns, columns) + + def test_hook_poll_nested(self): + expected_columns = ['Resource Name'] + self.columns + ['Stack Name'] + expected_rows = [self.row1 + ('my_stack',)] + arglist = ['my_stack', '--nested-depth=10'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, rows = self.cmd.take_action(parsed_args) + self.assertEqual(expected_rows, list(rows)) + self.assertEqual(expected_columns, columns) + + def test_hook_poll_nested_invalid(self): + arglist = ['my_stack', '--nested-depth=ugly'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + +class TestStackHookClear(TestStack): + + stack = stacks.Stack(None, { + "id": '1234', + "stack_name": 'my_stack', + "creation_time": "2013-08-04T20:57:55Z", + "updated_time": "2013-08-04T20:57:55Z", + "stack_status": "CREATE_IN_PROGRESS" + }) + resource = resources.Resource(None, { + 'stack_id': 'my_stack', + 'resource_name': 'resource' + }) + + def setUp(self): + super(TestStackHookClear, self).setUp() + self.cmd = stack.StackHookClear(self.app, None) + self.mock_client.stacks.get = mock.Mock( + return_value=self.stack) + self.mock_client.resources.signal = mock.Mock() + self.mock_client.resources.list = mock.Mock( + return_value=[self.resource]) + + def test_hook_clear(self): + arglist = ['my_stack', 'resource'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.resources.signal.assert_called_once_with( + data={'unset_hook': 'pre-create'}, + resource_name='resource', + stack_id='my_stack') + + def test_hook_clear_pre_create(self): + arglist = ['my_stack', 'resource', '--pre-create'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.resources.signal.assert_called_once_with( + data={'unset_hook': 'pre-create'}, + resource_name='resource', + stack_id='my_stack') + + def test_hook_clear_pre_update(self): + arglist = ['my_stack', 'resource', '--pre-update'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.resources.signal.assert_called_once_with( + data={'unset_hook': 'pre-update'}, + resource_name='resource', + stack_id='my_stack') diff --git a/heatclient/tests/unit/v1/test_hooks.py b/heatclient/tests/unit/v1/test_hooks.py index 095af908..d2fd947b 100644 --- a/heatclient/tests/unit/v1/test_hooks.py +++ b/heatclient/tests/unit/v1/test_hooks.py @@ -16,6 +16,8 @@ import heatclient.v1.shell as shell +from heatclient.common import hook_utils + class TestHooks(testtools.TestCase): def setUp(self): @@ -220,7 +222,8 @@ def test_update_hooks_in_env_and_args(self): self.assertEqual(expected_hooks, actual_hooks) def test_clear_all_hooks(self): - shell._get_hook_type_via_status = mock.Mock(return_value='pre-create') + hook_utils.get_hook_type_via_status = mock.Mock( + return_value='pre-create') type(self.args).hook = mock.PropertyMock( return_value=['bp']) type(self.args).pre_create = mock.PropertyMock(return_value=True) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index a56da63a..b18a82f9 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import fnmatch import logging from oslo_serialization import jsonutils @@ -25,13 +24,13 @@ from heatclient.common import deployment_utils from heatclient.common import event_utils +from heatclient.common import hook_utils from heatclient.common import http from heatclient.common import template_format from heatclient.common import template_utils from heatclient.common import utils from heatclient.openstack.common._i18n import _ -from heatclient.openstack.common._i18n import _LE from heatclient.openstack.common._i18n import _LW import heatclient.exc as exc @@ -993,42 +992,15 @@ def do_hook_clear(hc, args): elif args.pre_update: hook_type = 'pre-update' else: - hook_type = _get_hook_type_via_status(hc, args.id) + hook_type = hook_utils.get_hook_type_via_status(hc, args.id) for hook_string in args.hook: hook = [b for b in hook_string.split('/') if b] resource_pattern = hook[-1] stack_id = args.id - def clear_hook(stack_id, resource_name): - try: - hc.resources.signal( - stack_id=stack_id, - resource_name=resource_name, - data={'unset_hook': hook_type}) - except exc.HTTPNotFound: - logger.error( - _LE("Stack %(stack)s or resource %(resource)s not found"), - {'resource': resource_name, 'stack': stack_id}) - - def clear_wildcard_hooks(stack_id, stack_patterns): - if stack_patterns: - for resource in hc.resources.list(stack_id): - res_name = resource.resource_name - if fnmatch.fnmatchcase(res_name, stack_patterns[0]): - nested_stack = hc.resources.get( - stack_id=stack_id, - resource_name=res_name) - clear_wildcard_hooks( - nested_stack.physical_resource_id, - stack_patterns[1:]) - else: - for resource in hc.resources.list(stack_id): - res_name = resource.resource_name - if fnmatch.fnmatchcase(res_name, resource_pattern): - clear_hook(stack_id, res_name) - - clear_wildcard_hooks(stack_id, hook[:-1]) + hook_utils.clear_wildcard_hooks(hc, stack_id, hook[:-1], + hook_type, resource_pattern) @utils.arg('id', metavar='', @@ -1096,29 +1068,6 @@ def do_event_list(hc, args): utils.print_list(events, display_fields, sortby_index=None) -def _get_hook_type_via_status(hc, stack_id): - # Figure out if the hook should be pre-create or pre-update based - # on the stack status, also sanity assertions that we're in-progress. - try: - stack = hc.stacks.get(stack_id=stack_id) - except exc.HTTPNotFound: - raise exc.CommandError(_('Stack not found: %s') % stack_id) - else: - if 'IN_PROGRESS' not in stack.stack_status: - raise exc.CommandError(_('Stack status %s not IN_PROGRESS') % - stack.stack_status) - - if 'CREATE' in stack.stack_status: - hook_type = 'pre-create' - elif 'UPDATE' in stack.stack_status: - hook_type = 'pre-update' - else: - raise exc.CommandError(_('Unexpected stack status %s, ' - 'only create/update supported') - % stack.stack_status) - return hook_type - - @utils.arg('id', metavar='', help=_('Name or ID of stack to show the pending hooks for.')) @utils.arg('-n', '--nested-depth', metavar='', @@ -1148,7 +1097,7 @@ def do_hook_poll(hc, args): else: nested_depth = 0 - hook_type = _get_hook_type_via_status(hc, args.id) + hook_type = hook_utils.get_hook_type_via_status(hc, args.id) event_args = {'sort_dir': 'asc'} hook_events = event_utils.get_hook_events( hc, stack_id=args.id, event_args=event_args, diff --git a/setup.cfg b/setup.cfg index c0024c24..0dc94d07 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,8 @@ openstack.orchestration.v1 = stack_delete = heatclient.osc.v1.stack:DeleteStack stack_event_list = heatclient.osc.v1.event:ListEvent stack_event_show = heatclient.osc.v1.event:ShowEvent + stack_hook_clear = heatclient.osc.v1.stack:StackHookClear + stack_hook_poll = heatclient.osc.v1.stack:StackHookPoll stack_list = heatclient.osc.v1.stack:ListStack stack_output_list = heatclient.osc.v1.stack:OutputListStack stack_output_show = heatclient.osc.v1.stack:OutputShowStack From 4f7085355dc5cd2f5a6be1e13616db5182c4be62 Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Tue, 2 Feb 2016 11:17:02 +0800 Subject: [PATCH 156/553] OpenstackClient plugin for software deployment metadata show This change implements the "openstack software deployment metadata show" command Based on "heat deployment-metadata-show" command Blueprint: heat-support-python-openstackclient Change-Id: I43788230cec346ecdb22073a14ef58187d3d1fab --- heatclient/osc/v1/software_deployment.py | 23 +++++++++++++++++++ .../unit/osc/v1/test_software_deployment.py | 15 ++++++++++++ setup.cfg | 1 + 3 files changed, 39 insertions(+) diff --git a/heatclient/osc/v1/software_deployment.py b/heatclient/osc/v1/software_deployment.py index 4451f83b..545449b5 100644 --- a/heatclient/osc/v1/software_deployment.py +++ b/heatclient/osc/v1/software_deployment.py @@ -14,6 +14,7 @@ """Orchestration v1 Software Deployment action implementations""" import logging +from oslo_serialization import jsonutils from cliff import command from cliff import lister @@ -263,3 +264,25 @@ def take_action(self, parsed_args): if parsed_args.long: columns.append('output_values') return columns, utils.get_item_properties(data, columns) + + +class ShowMetadataDeployment(command.Command): + """Get deployment configuration metadata for the specified server.""" + + log = logging.getLogger(__name__ + '.ShowMetadataDeployment') + + def get_parser(self, prog_name): + parser = super(ShowMetadataDeployment, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('ID of the server to fetch deployments for') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + heat_client = self.app.client_manager.orchestration + md = heat_client.software_deployments.metadata( + server_id=parsed_args.server) + print(jsonutils.dumps(md, indent=2)) diff --git a/heatclient/tests/unit/osc/v1/test_software_deployment.py b/heatclient/tests/unit/osc/v1/test_software_deployment.py index 64273603..3799c2b9 100644 --- a/heatclient/tests/unit/osc/v1/test_software_deployment.py +++ b/heatclient/tests/unit/osc/v1/test_software_deployment.py @@ -383,3 +383,18 @@ def test_deployment_not_found(self): exc.CommandError, self.cmd.take_action, parsed_args) + + +class TestDeploymentMetadataShow(TestDeployment): + + def setUp(self): + super(TestDeploymentMetadataShow, self).setUp() + self.cmd = software_deployment.ShowMetadataDeployment(self.app, None) + self.sd_client.metadata = mock.Mock(return_value={}) + + def test_deployment_show_metadata(self): + arglist = ['ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.sd_client.metadata.assert_called_with( + server_id='ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5') diff --git a/setup.cfg b/setup.cfg index c0024c24..8453c8f7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ openstack.orchestration.v1 = software_deployment_create = heatclient.osc.v1.software_deployment:CreateDeployment software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment software_deployment_list = heatclient.osc.v1.software_deployment:ListDeployment + software_deployment_metadata_show = heatclient.osc.v1.software_deployment:ShowMetadataDeployment software_deployment_show = heatclient.osc.v1.software_deployment:ShowDeployment stack_abandon = heatclient.osc.v1.stack:AbandonStack stack_adopt = heatclient.osc.v1.stack:AdoptStack From 6302614f086b3f930d497fe572f3d1b1c758af0a Mon Sep 17 00:00:00 2001 From: Kanagaraj Manickam Date: Tue, 15 Dec 2015 17:37:42 +0530 Subject: [PATCH 157/553] Stack resource search heat resource-list -f implements blueprint heat-stack-resource-search depends-on: Iaae88f3b32bc2ba7b41a4078ef3aa8ffc07079b7 Change-Id: I2d1f6ee68623e08bb9654dbcba28336364d14d6d --- heatclient/tests/unit/test_resources.py | 10 +++- heatclient/tests/unit/test_shell.py | 63 +++++++++---------------- heatclient/v1/resources.py | 5 ++ heatclient/v1/shell.py | 7 +++ 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/heatclient/tests/unit/test_resources.py b/heatclient/tests/unit/test_resources.py index 120b1c4b..f8863326 100644 --- a/heatclient/tests/unit/test_resources.py +++ b/heatclient/tests/unit/test_resources.py @@ -115,7 +115,15 @@ def test_list_nested(self): self._test_list( fields={'stack_id': 'teststack', 'nested_depth': '99'}, expect='/stacks/teststack/resources?%s' % parse.urlencode({ - 'nested_depth': 99, + 'nested_depth': 99 + }, True) + ) + + def test_list_filtering(self): + self._test_list( + fields={'stack_id': 'teststack', 'filters': {'name': 'rsc_1'}}, + expect='/stacks/teststack/resources?%s' % parse.urlencode({ + 'name': 'rsc_1' }, True) ) diff --git a/heatclient/tests/unit/test_shell.py b/heatclient/tests/unit/test_shell.py index 3a62b6b8..f7dc02df 100644 --- a/heatclient/tests/unit/test_shell.py +++ b/heatclient/tests/unit/test_shell.py @@ -3791,7 +3791,8 @@ def test_resource_list_empty(self): --------------+ ''', resource_list_text) - def test_resource_list_nested(self): + def _test_resource_list_more_args(self, query_args, cmd_args, + response_args): self.register_keystone_auth_fixture() resp_dict = {"resources": [{ "resource_name": "foobar", @@ -3810,55 +3811,35 @@ def test_resource_list_nested(self): jsonutils.dumps(resp_dict)) stack_id = 'teststack/1' http.SessionClient.request( - '/stacks/%s/resources?nested_depth=99' % ( - stack_id), 'GET').AndReturn(resp) + '/stacks/%s/resources?%s' % ( + stack_id, query_args), 'GET').AndReturn(resp) self.m.ReplayAll() - shell_cmd = 'resource-list {0} --nested-depth {1}'.format(stack_id, 99) + shell_cmd = 'resource-list %s %s' % (stack_id, cmd_args) + resource_list_text = self.shell(shell_cmd) - required = [ - 'resource_name', 'foobar', - 'stack_name', 'foo', - ] - for field in required: + for field in response_args: self.assertRegexpMatches(resource_list_text, field) - def test_resource_list_detail(self): - self.register_keystone_auth_fixture() - resp_dict = {"resources": [{ - "resource_name": "foobar", - "links": [{ - "href": "http://heat.example.com:8004/foo/12/resources/foobar", - "rel": "self" - }, { - "href": "http://heat.example.com:8004/foo/12", - "rel": "stack" - }], - }]} - resp = fakes.FakeHTTPResponse( - 200, - 'OK', - {'content-type': 'application/json'}, - jsonutils.dumps(resp_dict)) - stack_id = 'teststack/1' - http.SessionClient.request('/stacks/%s/resources?%s' % ( - stack_id, - parse.urlencode({'with_detail': True}, True) - ), 'GET').AndReturn(resp) - - self.m.ReplayAll() + def test_resource_list_nested(self): + self._test_resource_list_more_args( + query_args='nested_depth=99', + cmd_args='--nested-depth 99', + response_args=['resource_name', 'foobar', 'stack_name', 'foo']) - shell_cmd = 'resource-list {0} --with-detail'.format(stack_id) - resource_list_text = self.shell(shell_cmd) + def test_resource_list_filter(self): + self._test_resource_list_more_args( + query_args='name=foobar', + cmd_args='--filter name=foobar', + response_args=['resource_name', 'foobar']) - required = [ - 'resource_name', 'foobar', - 'stack_name', 'foo', - ] - for field in required: - self.assertRegexpMatches(resource_list_text, field) + def test_resource_list_detail(self): + self._test_resource_list_more_args( + query_args=parse.urlencode({'with_detail': True}, True), + cmd_args='--with-detail', + response_args=['resource_name', 'foobar', 'stack_name', 'foo']) def test_resource_show_with_attrs(self): self.register_keystone_auth_fixture() diff --git a/heatclient/v1/resources.py b/heatclient/v1/resources.py index f063c841..1e908a41 100644 --- a/heatclient/v1/resources.py +++ b/heatclient/v1/resources.py @@ -56,9 +56,14 @@ def list(self, stack_id, **kwargs): """ params = {} + if 'filters' in kwargs: + filters = kwargs.pop('filters') + params.update(filters) + for key, value in six.iteritems(kwargs): if value: params[key] = value + url = '/stacks/%s/resources' % stack_id if params: url += '?%s' % parse.urlencode(params, True) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index a56da63a..894bda21 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -850,12 +850,19 @@ def do_template_validate(hc, args): @utils.arg('--with-detail', default=False, action="store_true", help=_('Enable detail information presented for each resource ' 'in resources list.')) +@utils.arg('-f', '--filter', metavar='', + help=_('Filter parameters to apply on returned resources based on' + ' their name, status, type, action, id and' + ' physcial_resource_id. This can be specified multiple' + ' times.'), + action='append') def do_resource_list(hc, args): '''Show list of resources belonging to a stack.''' fields = { 'stack_id': args.id, 'nested_depth': args.nested_depth, 'with_detail': args.with_detail, + 'filters': utils.format_parameters(args.filter) } try: resources = hc.resources.list(**fields) From b275fd249557662e16a1241b9f73a261739e44fd Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 20 Feb 2016 22:00:12 +0000 Subject: [PATCH 158/553] Updated from global requirements Change-Id: I55228fcddd20bd50c980843eab345f1d7f3a6dfb --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9af58f7a..1534a966 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ iso8601>=0.1.9 # MIT PrettyTable<0.8,>=0.7 # BSD oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.4.0 # Apache-2.0 +oslo.utils>=3.5.0 # Apache-2.0 python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 python-swiftclient>=2.2.0 # Apache-2.0 python-openstackclient>=2.1.0 # Apache-2.0 From 79742a3d67d1e3cc4f2c9a5994181ec51ca31881 Mon Sep 17 00:00:00 2001 From: Kanagaraj Manickam Date: Fri, 19 Feb 2016 11:04:04 +0530 Subject: [PATCH 159/553] Adds --filter to search for resources openstack resource list --filter implements blueprint heat-support-python-openstackclient implements blueprint heat-stack-resource-search Change-Id: I9ca33a0743bfad88190509351e8aca08ce7d2c0f --- .../osc/v1/{resources.py => resource.py} | 13 +++++- .../{test_resources.py => test_resource.py} | 44 +++++++++++++++---- setup.cfg | 8 ++-- 3 files changed, 50 insertions(+), 15 deletions(-) rename heatclient/osc/v1/{resources.py => resource.py} (95%) rename heatclient/tests/unit/osc/v1/{test_resources.py => test_resource.py} (90%) diff --git a/heatclient/osc/v1/resources.py b/heatclient/osc/v1/resource.py similarity index 95% rename from heatclient/osc/v1/resources.py rename to heatclient/osc/v1/resource.py index dae8470c..2a88da52 100644 --- a/heatclient/osc/v1/resources.py +++ b/heatclient/osc/v1/resource.py @@ -97,8 +97,16 @@ def get_parser(self, prog_name): type=int, help=_('Depth of nested stacks from which to display resources') ) - # TODO(jonesbr): - # Add --filter once https://review.openstack.org/#/c/257864/ is merged + + parser.add_argument( + '--filter', + metavar='', + action='append', + help=_('Filter parameters to apply on returned resources based on ' + 'their name, status, type, action, id and ' + 'physcial_resource_id') + ) + return parser def take_action(self, parsed_args): @@ -109,6 +117,7 @@ def take_action(self, parsed_args): fields = { 'nested_depth': parsed_args.nested_depth, 'with_detail': parsed_args.long, + 'filters': heat_utils.format_parameters(parsed_args.filter), } try: diff --git a/heatclient/tests/unit/osc/v1/test_resources.py b/heatclient/tests/unit/osc/v1/test_resource.py similarity index 90% rename from heatclient/tests/unit/osc/v1/test_resources.py rename to heatclient/tests/unit/osc/v1/test_resource.py index 367e1fd3..1807942a 100644 --- a/heatclient/tests/unit/osc/v1/test_resources.py +++ b/heatclient/tests/unit/osc/v1/test_resource.py @@ -17,7 +17,7 @@ from openstackclient.common import exceptions as exc from heatclient import exc as heat_exc -from heatclient.osc.v1 import resources +from heatclient.osc.v1 import resource from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes from heatclient.v1 import resources as v1_resources @@ -50,7 +50,7 @@ class TestStackResourceShow(TestResource): def setUp(self): super(TestStackResourceShow, self).setUp() - self.cmd = resources.ResourceShow(self.app, None) + self.cmd = resource.ResourceShow(self.app, None) self.resource_client.get = mock.MagicMock( return_value=v1_resources.Resource(None, self.response)) @@ -118,7 +118,7 @@ class TestStackResourceList(TestResource): def setUp(self): super(TestStackResourceList, self).setUp() - self.cmd = resources.ResourceList(self.app, None) + self.cmd = resource.ResourceList(self.app, None) self.resource_client.list = mock.MagicMock( return_value=[v1_resources.Resource(None, self.response)]) @@ -129,7 +129,10 @@ def test_resource_list(self): columns, data = self.cmd.take_action(parsed_args) self.resource_client.list.assert_called_with( - 'my_stack', with_detail=False, nested_depth=None) + 'my_stack', + filters={}, + with_detail=False, + nested_depth=None) self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data), list(data)[0]) @@ -151,7 +154,10 @@ def test_resource_list_with_detail(self): columns, data = self.cmd.take_action(parsed_args) self.resource_client.list.assert_called_with( - 'my_stack', with_detail=True, nested_depth=None) + 'my_stack', + filters={}, + with_detail=True, + nested_depth=None) self.assertEqual(cols, columns) self.assertEqual(tuple(out), list(data)[0]) @@ -166,7 +172,10 @@ def test_resource_list_nested_depth(self): columns, data = self.cmd.take_action(parsed_args) self.resource_client.list.assert_called_with( - 'my_stack', with_detail=False, nested_depth=3) + 'my_stack', + filters={}, + with_detail=False, + nested_depth=3) self.assertEqual(cols, columns) self.assertEqual(tuple(out), list(data)[0]) @@ -185,15 +194,32 @@ def test_resource_list_no_resource_name(self): columns, data = self.cmd.take_action(parsed_args) self.resource_client.list.assert_called_with( - 'my_stack', with_detail=False, nested_depth=None) + 'my_stack', + filters={}, + with_detail=False, + nested_depth=None) self.assertEqual(cols, columns) + def test_resource_list_filter(self): + arglist = ['my_stack', '--filter', 'name=my_resource'] + out = copy.deepcopy(self.data) + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.resource_client.list.assert_called_with( + 'my_stack', + filters=dict(name='my_resource'), + with_detail=False, + nested_depth=None) + self.assertEqual(tuple(out), list(data)[0]) + class TestResourceMetadata(TestResource): def setUp(self): super(TestResourceMetadata, self).setUp() - self.cmd = resources.ResourceMetadata(self.app, None) + self.cmd = resource.ResourceMetadata(self.app, None) self.resource_client.metadata = mock.Mock(return_value={}) def test_resource_metadata(self): @@ -230,7 +256,7 @@ class TestResourceSignal(TestResource): def setUp(self): super(TestResourceSignal, self).setUp() - self.cmd = resources.ResourceSignal(self.app, None) + self.cmd = resource.ResourceSignal(self.app, None) self.resource_client.signal = mock.Mock() def test_resource_signal(self): diff --git a/setup.cfg b/setup.cfg index 8453c8f7..7cbead27 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,10 +53,10 @@ openstack.orchestration.v1 = stack_list = heatclient.osc.v1.stack:ListStack stack_output_list = heatclient.osc.v1.stack:OutputListStack stack_output_show = heatclient.osc.v1.stack:OutputShowStack - stack_resource_list = heatclient.osc.v1.resources:ResourceList - stack_resource_metadata = heatclient.osc.v1.resources:ResourceMetadata - stack_resource_show = heatclient.osc.v1.resources:ResourceShow - stack_resource_signal = heatclient.osc.v1.resources:ResourceSignal + stack_resource_list = heatclient.osc.v1.resource:ResourceList + stack_resource_metadata = heatclient.osc.v1.resource:ResourceMetadata + stack_resource_show = heatclient.osc.v1.resource:ResourceShow + stack_resource_signal = heatclient.osc.v1.resource:ResourceSignal stack_resume = heatclient.osc.v1.stack:ResumeStack stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_create = heatclient.osc.v1.snapshot:CreateSnapshot From 302040c759e13ac93c9bf8a6b1acd14c73faaad7 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Mon, 15 Feb 2016 10:35:47 +1300 Subject: [PATCH 160/553] Move poll_for_events to event_utils Currently only used by "heat stack-create --poll", moving this function will allow all new openstack client commands to use for their --wait implementation. This change also adds unit tests for this function. Change-Id: I642797b08cd13c0c50169071d998cf5ba8b397a3 Blueprint: heat-support-python-openstackclient --- heatclient/common/event_utils.py | 30 +++++++++++ heatclient/tests/unit/test_event_utils.py | 65 ++++++++++++++++++++++- heatclient/v1/shell.py | 47 ++++------------ 3 files changed, 104 insertions(+), 38 deletions(-) diff --git a/heatclient/common/event_utils.py b/heatclient/common/event_utils.py index 27685f00..03bb8b2b 100644 --- a/heatclient/common/event_utils.py +++ b/heatclient/common/event_utils.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import time + from heatclient.common import utils import heatclient.exc as exc @@ -126,3 +128,31 @@ def _get_stack_events(hc, stack_id, event_args): for e in events: e.stack_name = stack_id.split("/")[0] return events + + +def poll_for_events(hc, stack_name, action, poll_period): + """Continuously poll events and logs for performed action on stack.""" + + marker = None + stop_status = ('%s_FAILED' % action, '%s_COMPLETE' % action) + while True: + events = get_events(hc, stack_id=stack_name, + event_args={'sort_dir': 'asc', + 'marker': marker}) + + if len(events) >= 1: + # set marker to last event that was received. + marker = getattr(events[-1], 'id', None) + events_log = utils.event_log_formatter(events) + print(events_log) + + for event in events: + # check if stack event was also received + if getattr(event, 'resource_name', '') == stack_name: + stack_status = getattr(event, 'resource_status', '') + msg = _("\n Stack %(name)s %(status)s \n") % dict( + name=stack_name, status=stack_status) + if stack_status in stop_status: + return stack_status, msg + + time.sleep(poll_period) diff --git a/heatclient/tests/unit/test_event_utils.py b/heatclient/tests/unit/test_event_utils.py index bba77b4a..d2350e55 100644 --- a/heatclient/tests/unit/test_event_utils.py +++ b/heatclient/tests/unit/test_event_utils.py @@ -36,11 +36,13 @@ def _mock_resource(resource_id, nested_id=None): return hc_res.Resource(manager=None, info=res_info) @staticmethod - def _mock_event(event_id, resource_id): + def _mock_event(event_id, resource_id, + resource_status='CREATE_COMPLETE'): ev_info = {"links": [{"href": "http://heat/foo", "rel": "self"}], "logical_resource_id": resource_id, "physical_resource_id": resource_id, - "resource_status": "CREATE_COMPLETE", + "resource_name": resource_id, + "resource_status": resource_status, "resource_status_reason": "state changed", "event_time": "2014-12-05T14:14:30Z", "id": event_id} @@ -128,3 +130,62 @@ def event_list_stub(stack_id, argfoo): self.assertEqual('event1', evs[0].id) self.assertEqual('event2', evs[1].id) self.assertEqual('event3', evs[2].id) + + @mock.patch('heatclient.common.event_utils.get_events') + def test_poll_for_events(self, ge): + ge.side_effect = [[ + self._mock_event('1', 'astack', 'CREATE_IN_PROGRESS'), + self._mock_event('2', 'res_child1', 'CREATE_IN_PROGRESS'), + self._mock_event('3', 'res_child2', 'CREATE_IN_PROGRESS'), + self._mock_event('4', 'res_child3', 'CREATE_IN_PROGRESS') + ], [ + self._mock_event('5', 'res_child1', 'CREATE_COMPLETE'), + self._mock_event('6', 'res_child2', 'CREATE_COMPLETE'), + self._mock_event('7', 'res_child3', 'CREATE_COMPLETE'), + self._mock_event('8', 'astack', 'CREATE_COMPLETE') + ]] + + stack_status, msg = event_utils.poll_for_events( + None, 'astack', 'CREATE', 0) + self.assertEqual('CREATE_COMPLETE', stack_status) + self.assertEqual('\n Stack astack CREATE_COMPLETE \n', msg) + ge.assert_has_calls([ + mock.call(None, stack_id='astack', event_args={ + 'sort_dir': 'asc', 'marker': None + }), + mock.call(None, stack_id='astack', event_args={ + 'sort_dir': 'asc', 'marker': '4' + }) + ]) + + @mock.patch('heatclient.common.event_utils.get_events') + def test_poll_for_events_in_progress_resource(self, ge): + ge.side_effect = [[ + self._mock_event('1', 'astack', 'CREATE_IN_PROGRESS'), + self._mock_event('2', 'res_child1', 'CREATE_IN_PROGRESS'), + self._mock_event('3', 'astack', 'CREATE_COMPLETE') + ]] + + stack_status, msg = event_utils.poll_for_events( + None, 'astack', 'CREATE', 0) + self.assertEqual('CREATE_COMPLETE', stack_status) + self.assertEqual('\n Stack astack CREATE_COMPLETE \n', msg) + + @mock.patch('heatclient.common.event_utils.get_events') + def test_poll_for_events_failed(self, ge): + ge.side_effect = [[ + self._mock_event('1', 'astack', 'CREATE_IN_PROGRESS'), + self._mock_event('2', 'res_child1', 'CREATE_IN_PROGRESS'), + self._mock_event('3', 'res_child2', 'CREATE_IN_PROGRESS'), + self._mock_event('4', 'res_child3', 'CREATE_IN_PROGRESS') + ], [ + self._mock_event('5', 'res_child1', 'CREATE_COMPLETE'), + self._mock_event('6', 'res_child2', 'CREATE_FAILED'), + self._mock_event('7', 'res_child3', 'CREATE_COMPLETE'), + self._mock_event('8', 'astack', 'CREATE_FAILED') + ]] + + stack_status, msg = event_utils.poll_for_events( + None, 'astack', 'CREATE', 0) + self.assertEqual('CREATE_FAILED', stack_status) + self.assertEqual('\n Stack astack CREATE_FAILED \n', msg) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index 894bda21..82f4c03f 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -20,7 +20,6 @@ from oslo_utils import strutils import six from six.moves.urllib import request -import time import yaml from heatclient.common import deployment_utils @@ -138,8 +137,17 @@ def do_stack_create(hc, args): hc.stacks.create(**fields) do_stack_list(hc) - if args.poll is not None: - _poll_for_events(hc, args.name, 'CREATE', args.poll) + if not args.poll: + return + + show_fields = {'stack_id': args.name} + _do_stack_show(hc, show_fields) + stack_status, msg = event_utils.poll_for_events( + hc, args.name, action='CREATE', poll_period=args.poll) + _do_stack_show(hc, show_fields) + if stack_status == 'CREATE_FAILED': + raise exc.StackFailure(msg) + print(msg) @utils.arg('-e', '--environment-file', metavar='', @@ -1608,36 +1616,3 @@ def _do_stack_show(hc, fields): 'tags': utils.json_formatter } utils.print_dict(stack.to_dict(), formatters=formatters) - - -def _poll_for_events(hc, stack_name, action, poll_period): - """Continuously poll events and logs for performed action on stack.""" - - fields = {'stack_id': stack_name} - _do_stack_show(hc, fields) - marker = None - while True: - events = event_utils.get_events(hc, stack_id=stack_name, - event_args={'sort_dir': 'asc', - 'marker': marker}) - - if len(events) >= 1: - # set marker to last event that was received. - marker = getattr(events[-1], 'id', None) - events_log = utils.event_log_formatter(events) - print(events_log) - for event in events: - # check if stack event was also received - if getattr(event, 'resource_name', '') == stack_name: - stack_status = getattr(event, 'resource_status', '') - msg = _("\n Stack %(name)s %(status)s \n") % dict( - name=stack_name, status=stack_status) - if stack_status == '%s_COMPLETE' % action: - _do_stack_show(hc, fields) - print(msg) - return - elif stack_status == '%s_FAILED' % action: - _do_stack_show(hc, fields) - raise exc.StackFailure(msg) - - time.sleep(poll_period) From 5f6a88bc5f37dfd153d44c8c6201eed36f33b340 Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Mon, 30 Nov 2015 23:34:13 -0600 Subject: [PATCH 161/553] OpenstackClient plugin for software deployment output show This change implements the "openstack software deployment output show" command Based from the existing heat command: heat deployment-output-show Change-Id: Ide607c925a15071c0c02c8cdb1ba20d9a745f9a9 Blueprint: heat-support-python-openstackclient --- heatclient/common/format_utils.py | 40 ++++++++ heatclient/osc/v1/software_deployment.py | 82 +++++++++++++++-- .../unit/osc/v1/test_software_deployment.py | 52 +++++++++++ heatclient/tests/unit/test_format_utils.py | 91 +++++++++++++++++++ setup.cfg | 1 + 5 files changed, 259 insertions(+), 7 deletions(-) diff --git a/heatclient/common/format_utils.py b/heatclient/common/format_utils.py index 2b02fc93..5cb691ab 100644 --- a/heatclient/common/format_utils.py +++ b/heatclient/common/format_utils.py @@ -13,6 +13,8 @@ # Copyright 2015 IBM Corp. from cliff import show +import six +import sys class RawFormat(show.ShowOne): @@ -51,3 +53,41 @@ class ValueFormat(RawFormat): @property def formatter_default(self): return 'value' + + +def indent_and_truncate(txt, spaces=0, truncate=False, truncate_limit=10, + truncate_prefix=None, truncate_postfix=None): + """Indents supplied multiline text by the specified number of spaces + + """ + if txt is None: + return + lines = six.text_type(txt).splitlines() + if truncate and len(lines) > truncate_limit: + lines = lines[-truncate_limit:] + if truncate_prefix is not None: + lines.insert(0, truncate_prefix) + if truncate_postfix is not None: + lines.append(truncate_postfix) + + if spaces > 0: + lines = [" " * spaces + line for line in lines] + return '\n'.join(lines) + + +def print_software_deployment_output(data, name, out=sys.stdout, long=False): + """Prints details of the software deployment for user consumption + + The format attempts to be valid yaml, but is primarily aimed at showing + useful information to the user in a helpful layout. + """ + if name in ('deploy_stdout', 'deploy_stderr'): + output = indent_and_truncate( + data.get(name), + spaces=4, + truncate=not long, + truncate_prefix='...', + truncate_postfix='(truncated, view all with --long)') + out.write(' %s: |\n%s\n' % (name, output)) + else: + out.write(' %s: %s\n' % (name, data.get(name))) diff --git a/heatclient/osc/v1/software_deployment.py b/heatclient/osc/v1/software_deployment.py index 545449b5..b60f4c1e 100644 --- a/heatclient/osc/v1/software_deployment.py +++ b/heatclient/osc/v1/software_deployment.py @@ -140,8 +140,8 @@ class DeleteDeployment(command.Command): def get_parser(self, prog_name): parser = super(DeleteDeployment, self).get_parser(prog_name) parser.add_argument( - 'id', - metavar='', + 'deployment', + metavar='', nargs='+', help=_('ID of the deployment(s) to delete.') ) @@ -152,7 +152,7 @@ def take_action(self, parsed_args): hc = self.app.client_manager.orchestration failure_count = 0 - for deploy_id in parsed_args.id: + for deploy_id in parsed_args.deployment: try: sd = hc.software_deployments.get(deployment_id=deploy_id) hc.software_deployments.delete( @@ -178,7 +178,7 @@ def take_action(self, parsed_args): raise exc.CommandError(_('Unable to delete %(count)s of the ' '%(total)s deployments.') % {'count': failure_count, - 'total': len(parsed_args.id)}) + 'total': len(parsed_args.deployment)}) class ListDeployment(lister.Lister): @@ -229,8 +229,8 @@ class ShowDeployment(show.ShowOne): def get_parser(self, prog_name): parser = super(ShowDeployment, self).get_parser(prog_name) parser.add_argument( - 'id', - metavar='', + 'deployment', + metavar='', help=_('ID of the deployment') ) parser.add_argument( @@ -246,7 +246,7 @@ def take_action(self, parsed_args): heat_client = self.app.client_manager.orchestration try: data = heat_client.software_deployments.get( - deployment_id=parsed_args.id) + deployment_id=parsed_args.deployment) except heat_exc.HTTPNotFound: raise exc.CommandError(_('Software Deployment not found: %s') % id) else: @@ -286,3 +286,71 @@ def take_action(self, parsed_args): md = heat_client.software_deployments.metadata( server_id=parsed_args.server) print(jsonutils.dumps(md, indent=2)) + + +class ShowOutputDeployment(command.Command): + """Show a specific deployment output.""" + + log = logging.getLogger(__name__ + '.ShowOutputDeployment') + + def get_parser(self, prog_name): + parser = super(ShowOutputDeployment, self).get_parser(prog_name) + parser.add_argument( + 'deployment', + metavar='', + help=_('ID of deployment to show the output for') + ) + parser.add_argument( + 'output', + metavar='', + nargs='?', + default=None, + help=_('Name of an output to display') + ) + parser.add_argument( + '--all', + default=False, + action='store_true', + help=_('Display all deployment outputs') + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='Show full deployment logs in output', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + if (not parsed_args.all and parsed_args.output is None or + parsed_args.all and parsed_args.output is not None): + raise exc.CommandError( + _('Error: either %(output)s or %(all)s argument is needed.') + % {'output': '', 'all': '--all'}) + try: + sd = heat_client.software_deployments.get( + deployment_id=parsed_args.deployment) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Deployment not found: %s') + % parsed_args.deployment) + outputs = sd.output_values + if outputs: + if parsed_args.all: + print('output_values:\n') + for k in outputs.keys(): + format_utils.print_software_deployment_output( + data=outputs, name=k, long=parsed_args.long) + else: + if parsed_args.output not in outputs: + msg = (_('Output %(output)s does not exist in deployment' + ' %(deployment)s') + % {'output': parsed_args.output, + 'deployment': parsed_args.deployment}) + raise exc.CommandError(msg) + else: + print('output_value:\n') + format_utils.print_software_deployment_output( + data=outputs, name=parsed_args.output) diff --git a/heatclient/tests/unit/osc/v1/test_software_deployment.py b/heatclient/tests/unit/osc/v1/test_software_deployment.py index 3799c2b9..e44f9a80 100644 --- a/heatclient/tests/unit/osc/v1/test_software_deployment.py +++ b/heatclient/tests/unit/osc/v1/test_software_deployment.py @@ -398,3 +398,55 @@ def test_deployment_show_metadata(self): self.cmd.take_action(parsed_args) self.sd_client.metadata.assert_called_with( server_id='ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5') + + +class TestDeploymentOutputShow(TestDeployment): + + get_response = { + "status": "IN_PROGRESS", + "server_id": "ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5", + "config_id": "3d5ec2a8-7004-43b6-a7f6-542bdbe9d434", + "output_values": None, + "input_values": None, + "action": "CREATE", + "status_reason": "Deploy data available", + "id": "06e87bcc-33a2-4bce-aebd-533e698282d3", + "creation_time": "2015-01-31T15:12:36Z", + "updated_time": "2015-01-31T15:18:21Z" + } + + def setUp(self): + super(TestDeploymentOutputShow, self).setUp() + self.cmd = software_deployment.ShowOutputDeployment(self.app, None) + + def test_deployment_output_show(self): + arglist = ['85c3a507-351b-4b28-a7d8-531c8d53f4e6', '--all', '--long'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.sd_client.get = mock.Mock( + return_value=software_deployments.SoftwareDeployment( + None, self.get_response)) + self.cmd.take_action(parsed_args) + self.sd_client.get.assert_called_with(**{ + 'deployment_id': '85c3a507-351b-4b28-a7d8-531c8d53f4e6' + }) + + def test_deployment_output_show_invalid(self): + arglist = ['85c3a507-351b-4b28-a7d8-531c8d53f4e6'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.sd_client.get = mock.Mock() + error = self.assertRaises( + exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn('either or --all argument is needed', + str(error)) + + def test_deployment_output_show_not_found(self): + arglist = ['85c3a507-351b-4b28-a7d8-531c8d53f4e6', '--all'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.sd_client.get = mock.Mock() + self.sd_client.get.side_effect = heat_exc.HTTPNotFound() + self.assertRaises( + exc.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/heatclient/tests/unit/test_format_utils.py b/heatclient/tests/unit/test_format_utils.py index 8e948ce2..3962702c 100644 --- a/heatclient/tests/unit/test_format_utils.py +++ b/heatclient/tests/unit/test_format_utils.py @@ -13,6 +13,7 @@ # Copyright 2015 IBM Corp. import json +import six import yaml from heatclient.common import format_utils @@ -89,3 +90,93 @@ def test_value_format(self): self.cmd.run(parsed_args) self.assertEqual(expected, self.app.stdout.make_string()) + + def test_indent_and_truncate(self): + self.assertEqual( + None, + format_utils.indent_and_truncate(None)) + self.assertEqual( + None, + format_utils.indent_and_truncate(None, truncate=True)) + self.assertEqual( + '', + format_utils.indent_and_truncate('')) + self.assertEqual( + 'one', + format_utils.indent_and_truncate('one')) + self.assertEqual( + None, + format_utils.indent_and_truncate(None, spaces=2)) + self.assertEqual( + '', + format_utils.indent_and_truncate('', spaces=2)) + self.assertEqual( + ' one', + format_utils.indent_and_truncate('one', spaces=2)) + self.assertEqual( + 'one\ntwo\nthree\nfour\nfive', + format_utils.indent_and_truncate('one\ntwo\nthree\nfour\nfive')) + self.assertEqual( + 'three\nfour\nfive', + format_utils.indent_and_truncate( + 'one\ntwo\nthree\nfour\nfive', + truncate=True, + truncate_limit=3)) + self.assertEqual( + ' and so on\n three\n four\n five\n truncated', + format_utils.indent_and_truncate( + 'one\ntwo\nthree\nfour\nfive', + spaces=2, + truncate=True, + truncate_limit=3, + truncate_prefix='and so on', + truncate_postfix='truncated')) + + def test_print_software_deployment_output(self): + out = six.StringIO() + format_utils.print_software_deployment_output( + {'deploy_stdout': ''}, out=out, name='deploy_stdout') + self.assertEqual( + ' deploy_stdout: |\n\n', + out.getvalue()) + ov = {'deploy_stdout': '', 'deploy_stderr': '1\n2\n3\n4\n5\n6\n7\n8\n9' + '\n10\n11', + 'deploy_status_code': 0} + out = six.StringIO() + format_utils.print_software_deployment_output(ov, out=out, + name='deploy_stderr') + self.assertEqual( + u'''\ + deploy_stderr: | + ... + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + (truncated, view all with --long) +''', out.getvalue()) + out = six.StringIO() + format_utils.print_software_deployment_output(ov, out=out, + name='deploy_stderr', + long=True) + self.assertEqual( + u'''\ + deploy_stderr: | + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 +''', out.getvalue()) diff --git a/setup.cfg b/setup.cfg index 8453c8f7..03c9ac02 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,7 @@ openstack.orchestration.v1 = software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment software_deployment_list = heatclient.osc.v1.software_deployment:ListDeployment software_deployment_metadata_show = heatclient.osc.v1.software_deployment:ShowMetadataDeployment + software_deployment_output_show = heatclient.osc.v1.software_deployment:ShowOutputDeployment software_deployment_show = heatclient.osc.v1.software_deployment:ShowDeployment stack_abandon = heatclient.osc.v1.stack:AbandonStack stack_adopt = heatclient.osc.v1.stack:AdoptStack From 4733a65eab78afd3188c8facd147c14821f39f4a Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Wed, 17 Feb 2016 20:11:22 +0000 Subject: [PATCH 162/553] Update OSC metavars OpenstackClient has a consistent pattern for the metavars on their arguments. Heatclient's plugin should follow that pattern. Blueprint: heat-support-python-openstackclient Change-Id: Ie2fdb2a985c4c41235916f0745138ea9c812b4ac --- heatclient/osc/v1/event.py | 20 +++---- heatclient/osc/v1/resource.py | 14 ++--- heatclient/osc/v1/resource_type.py | 6 +- heatclient/osc/v1/software_config.py | 26 ++++----- heatclient/osc/v1/software_deployment.py | 31 ++++++----- heatclient/osc/v1/stack.py | 70 ++++++++++++------------ heatclient/osc/v1/template.py | 2 +- 7 files changed, 85 insertions(+), 84 deletions(-) diff --git a/heatclient/osc/v1/event.py b/heatclient/osc/v1/event.py index 930e7e62..664b56c5 100644 --- a/heatclient/osc/v1/event.py +++ b/heatclient/osc/v1/event.py @@ -33,17 +33,17 @@ def get_parser(self, prog_name): parser = super(ShowEvent, self).get_parser(prog_name) parser.add_argument( 'stack', - metavar='', + metavar='', help=_('Name or ID of stack to show events for') ) parser.add_argument( 'resource', - metavar='', + metavar='', help=_('Name of the resource event belongs to') ) parser.add_argument( 'event', - metavar='', + metavar='', help=_('ID of event to display details for') ) @@ -93,42 +93,42 @@ def get_parser(self, prog_name): parser = super(ListEvent, self).get_parser(prog_name) parser.add_argument( 'stack', - metavar='', + metavar='', help=_('Name or ID of stack to show events for') ) parser.add_argument( '--resource', - metavar='', + metavar='', help=_('Name of resource to show events for. Note: this cannot ' 'be specified with --nested-depth') ) parser.add_argument( '--filter', - metavar='', + metavar='', action='append', help=_('Filter parameters to apply on returned events') ) parser.add_argument( '--limit', - metavar='', + metavar='', type=int, help=_('Limit the number of events returned') ) parser.add_argument( '--marker', - metavar='', + metavar='', help=_('Only return events that appear after the given ID') ) parser.add_argument( '--nested-depth', - metavar='', + metavar='', type=int, help=_('Depth of nested stacks from which to display events. ' 'Note: this cannot be specified with --resource') ) parser.add_argument( '--sort', - metavar='[:]', + metavar='[:]', action='append', help=_('Sort output by selected keys and directions (asc or desc) ' '(default: asc). Specify multiple times to sort on ' diff --git a/heatclient/osc/v1/resource.py b/heatclient/osc/v1/resource.py index 2a88da52..7c12c756 100644 --- a/heatclient/osc/v1/resource.py +++ b/heatclient/osc/v1/resource.py @@ -93,7 +93,7 @@ def get_parser(self, prog_name): ) parser.add_argument( '-n', '--nested-depth', - metavar='', + metavar='', type=int, help=_('Depth of nested stacks from which to display resources') ) @@ -152,12 +152,12 @@ def get_parser(self, prog_name): parser = super(ResourceMetadata, self).get_parser(prog_name) parser.add_argument( 'stack', - metavar='', + metavar='', help=_('Stack to display (name or ID)'), ) parser.add_argument( 'resource', - metavar='', + metavar='', help=_('Name of the resource to show the metadata for')) return parser @@ -193,22 +193,22 @@ def get_parser(self, prog_name): parser = super(ResourceSignal, self).get_parser(prog_name) parser.add_argument( 'stack', - metavar='', + metavar='', help=_('Name or ID of stack the resource belongs to'), ) parser.add_argument( 'resource', - metavar='', + metavar='', help=_('Name of the resoure to signal'), ) parser.add_argument( '--data', - metavar='', + metavar='', help=_('JSON Data to send to the signal handler') ) parser.add_argument( '--data-file', - metavar='', + metavar='', help=_('File containing JSON data to send to the signal handler') ) return parser diff --git a/heatclient/osc/v1/resource_type.py b/heatclient/osc/v1/resource_type.py index 1d85345c..28d966e8 100644 --- a/heatclient/osc/v1/resource_type.py +++ b/heatclient/osc/v1/resource_type.py @@ -34,12 +34,12 @@ def get_parser(self, prog_name): self).get_parser(prog_name) parser.add_argument( 'resource_type', - metavar='', + metavar='', help=_('Resource type to show details for'), ) parser.add_argument( '--template-type', - metavar='', + metavar='', help=_('Optional template type to generate, hot or cfn') ) return parser @@ -84,7 +84,7 @@ def get_parser(self, prog_name): parser.add_argument( '--filter', dest='filter', - metavar='', + metavar='', help=_('Filter parameters to apply on returned resource types. ' 'This can be specified multiple times. It can be any of ' 'name, version or support_status'), diff --git a/heatclient/osc/v1/software_config.py b/heatclient/osc/v1/software_config.py index facab0e4..d3d31587 100644 --- a/heatclient/osc/v1/software_config.py +++ b/heatclient/osc/v1/software_config.py @@ -39,8 +39,8 @@ class DeleteConfig(command.Command): def get_parser(self, prog_name): parser = super(DeleteConfig, self).get_parser(prog_name) parser.add_argument( - 'id', - metavar='', + 'config', + metavar='', nargs='+', help=_('IDs of the software configs to delete') ) @@ -56,7 +56,7 @@ def take_action(self, parsed_args): def _delete_config(heat_client, args): failure_count = 0 - for config_id in args.id: + for config_id in args.config: try: heat_client.software_configs.delete( config_id=config_id) @@ -70,7 +70,7 @@ def _delete_config(heat_client, args): raise exc.CommandError(_('Unable to delete %(count)s of the ' '%(total)s software configs.') % {'count': failure_count, - 'total': len(args.id)}) + 'total': len(args.config)}) class ListConfig(lister.Lister): @@ -82,12 +82,12 @@ def get_parser(self, prog_name): parser = super(ListConfig, self).get_parser(prog_name) parser.add_argument( '--limit', - metavar='', + metavar='', help=_('Limit the number of configs returned') ) parser.add_argument( '--marker', - metavar='', + metavar='', help=_('Return configs that appear after the given config ID') ) return parser @@ -119,23 +119,23 @@ def get_parser(self, prog_name): parser = super(CreateConfig, self).get_parser(prog_name) parser.add_argument( 'name', - metavar='', + metavar='', help=_('Name of the software config to create') ) parser.add_argument( '--config-file', - metavar='', + metavar='', help=_('Path to JSON/YAML containing map defining ' ', , and ') ) parser.add_argument( '--definition-file', - metavar='', + metavar='', help=_('Path to software config script/data') ) parser.add_argument( '--group', - metavar='', + metavar='', default='Heat::Ungrouped', help=_('Group name of tool expected by the software config') ) @@ -196,8 +196,8 @@ class ShowConfig(format_utils.YamlFormat): def get_parser(self, prog_name): parser = super(ShowConfig, self).get_parser(prog_name) parser.add_argument( - 'id', - metavar='', + 'config', + metavar='', help=_('ID of the config') ) parser.add_argument( @@ -211,7 +211,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) heat_client = self.app.client_manager.orchestration - return _show_config(heat_client, config_id=parsed_args.id, + return _show_config(heat_client, config_id=parsed_args.config, config_only=parsed_args.config_only) diff --git a/heatclient/osc/v1/software_deployment.py b/heatclient/osc/v1/software_deployment.py index 545449b5..040e1162 100644 --- a/heatclient/osc/v1/software_deployment.py +++ b/heatclient/osc/v1/software_deployment.py @@ -39,21 +39,21 @@ def get_parser(self, prog_name): parser = super(CreateDeployment, self).get_parser(prog_name) parser.add_argument( 'name', - metavar='', + metavar='', help=_('Name of the derived config associated with this ' 'deployment. This is used to apply a sort order to the ' 'list of configurations currently deployed to the server.') ) parser.add_argument( '--input-value', - metavar='', + metavar='', action='append', help=_('Input value to set on the deployment. This can be ' 'specified multiple times.') ) parser.add_argument( '--action', - metavar='', + metavar='', default='UPDATE', help=_('Name of an action for this deployment. This can be a ' 'custom action, or one of CREATE, UPDATE, DELETE, SUSPEND, ' @@ -61,18 +61,18 @@ def get_parser(self, prog_name): ) parser.add_argument( '--config', - metavar='', + metavar='', help=_('ID of the configuration to deploy') ) parser.add_argument( '--server', - metavar='', + metavar='', required=True, help=_('ID of the server being deployed to') ) parser.add_argument( '--signal-transport', - metavar='', + metavar='', default='TEMP_URL_SIGNAL', help=_('How the server should signal to heat with the deployment ' 'output values. TEMP_URL_SIGNAL will create a Swift ' @@ -84,13 +84,14 @@ def get_parser(self, prog_name): ) parser.add_argument( '--container', - metavar='', + metavar='', help=_('Optional name of container to store TEMP_URL_SIGNAL ' 'objects in. If not specified a container will be created ' 'with a name derived from the DEPLOY_NAME') ) parser.add_argument( '--timeout', + metavar='', type=int, default=60, help=_('Deployment timeout in minutes') @@ -140,8 +141,8 @@ class DeleteDeployment(command.Command): def get_parser(self, prog_name): parser = super(DeleteDeployment, self).get_parser(prog_name) parser.add_argument( - 'id', - metavar='', + 'deployment', + metavar='', nargs='+', help=_('ID of the deployment(s) to delete.') ) @@ -152,7 +153,7 @@ def take_action(self, parsed_args): hc = self.app.client_manager.orchestration failure_count = 0 - for deploy_id in parsed_args.id: + for deploy_id in parsed_args.deployment: try: sd = hc.software_deployments.get(deployment_id=deploy_id) hc.software_deployments.delete( @@ -178,7 +179,7 @@ def take_action(self, parsed_args): raise exc.CommandError(_('Unable to delete %(count)s of the ' '%(total)s deployments.') % {'count': failure_count, - 'total': len(parsed_args.id)}) + 'total': len(parsed_args.deployment)}) class ListDeployment(lister.Lister): @@ -190,7 +191,7 @@ def get_parser(self, prog_name): parser = super(ListDeployment, self).get_parser(prog_name) parser.add_argument( '--server', - metavar='', + metavar='', help=_('ID of the server to fetch deployments for') ) parser.add_argument( @@ -229,8 +230,8 @@ class ShowDeployment(show.ShowOne): def get_parser(self, prog_name): parser = super(ShowDeployment, self).get_parser(prog_name) parser.add_argument( - 'id', - metavar='', + 'deployment', + metavar='', help=_('ID of the deployment') ) parser.add_argument( @@ -246,7 +247,7 @@ def take_action(self, parsed_args): heat_client = self.app.client_manager.orchestration try: data = heat_client.software_deployments.get( - deployment_id=parsed_args.id) + deployment_id=parsed_args.deployment) except heat_exc.HTTPNotFound: raise exc.CommandError(_('Software Deployment not found: %s') % id) else: diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 98f417e3..d88c9d1f 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -55,25 +55,25 @@ def get_parser(self, prog_name): parser = super(CreateStack, self).get_parser(prog_name) parser.add_argument( '-t', '--template', - metavar='', + metavar='