diff options
| -rw-r--r-- | doc/source/ext/gen_ref.py | 3 | ||||
| -rw-r--r-- | heatclient/common/environment_format.py | 5 | ||||
| -rw-r--r-- | heatclient/common/http.py | 14 | ||||
| -rw-r--r-- | heatclient/common/template_utils.py | 3 | ||||
| -rw-r--r-- | heatclient/tests/test_common_http.py | 81 | ||||
| -rw-r--r-- | heatclient/tests/test_environment_format.py | 12 | ||||
| -rw-r--r-- | heatclient/tests/test_software_configs.py | 93 | ||||
| -rw-r--r-- | heatclient/tests/test_software_deployments.py | 161 | ||||
| -rw-r--r-- | heatclient/v1/client.py | 7 | ||||
| -rw-r--r-- | heatclient/v1/shell.py | 2 | ||||
| -rw-r--r-- | heatclient/v1/software_configs.py | 53 | ||||
| -rw-r--r-- | heatclient/v1/software_deployments.py | 76 |
12 files changed, 499 insertions, 11 deletions
diff --git a/doc/source/ext/gen_ref.py b/doc/source/ext/gen_ref.py index 97b9bfd..15ff818 100644 --- a/doc/source/ext/gen_ref.py +++ b/doc/source/ext/gen_ref.py @@ -55,4 +55,5 @@ def gen_ref(ver, title, names): gen_ref("", "Client Reference", ["client", "exc"]) gen_ref("v1", "Version 1 API Reference", - ["stacks", "resources", "events", "actions"]) + ["stacks", "resources", "events", "actions", + "software_configs", "software_deployments"]) diff --git a/heatclient/common/environment_format.py b/heatclient/common/environment_format.py index 136c0ae..d3c002e 100644 --- a/heatclient/common/environment_format.py +++ b/heatclient/common/environment_format.py @@ -26,7 +26,7 @@ def parse(env_str): '''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. + YAML format. ''' try: env = yaml.load(env_str, Loader=yaml_loader) @@ -35,6 +35,9 @@ def parse(env_str): else: if env is None: env = {} + elif not isinstance(env, dict): + raise ValueError('The environment is not a valid ' + 'YAML mapping data type.') for param in env: if param not in SECTIONS: diff --git a/heatclient/common/http.py b/heatclient/common/http.py index fd1cf29..3282e2a 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -23,6 +23,7 @@ import requests from heatclient import exc from heatclient.openstack.common import jsonutils from heatclient.openstack.common.py3kcompat import urlutils +from heatclient.openstack.common import strutils LOG = logging.getLogger(__name__) if not LOG.handlers: @@ -82,7 +83,8 @@ class HTTPClient(object): curl = ['curl -i -X %s' % method] for (key, value) in kwargs['headers'].items(): - header = '-H \'%s: %s\'' % (key, value) + header = '-H \'%s: %s\'' % (strutils.safe_decode(key), + strutils.safe_decode(value)) curl.append(header) conn_params_fmt = [ @@ -142,16 +144,16 @@ class HTTPClient(object): if self.verify_cert is not None: kwargs['verify'] = self.verify_cert - # We are not using requests builtin redirection on DELETE since it does - # not follow the RFC having to resend the same method on a - # redirect. For example if we do a DELETE on a URL and we get - # a 302 RFC says that we should follow that URL with the same + # We are not using requests builtin redirection on DELETE/POST/PUT + # since it does not follow the RFC having to resend the same method on + # a redirect. For example if we do a DELETE/POST/PUT on a URL and we + # get a 302 RFC says that we should follow that URL with the same # method as before, requests doesn't follow that and send a # GET instead for the method. See issue: # https://github.com/kennethreitz/requests/issues/1704 # hopefully this could be fixed as they say in a comment in a # future point version i.e: 3.x - if method == 'DELETE': + if method != 'GET': allow_redirects = False else: allow_redirects = True diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index ccf5c37..0986f59 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -15,7 +15,6 @@ import os import six -import urllib from heatclient.common import environment_format from heatclient.common import template_format @@ -113,7 +112,7 @@ def normalise_file_path_to_url(path): if urlutils.urlparse(path).scheme: return path path = os.path.abspath(path) - return urlutils.urljoin('file:', urllib.pathname2url(path)) + return urlutils.urljoin('file:', urlutils.pathname2url(path)) def process_environment_and_files(env_path=None, template=None, diff --git a/heatclient/tests/test_common_http.py b/heatclient/tests/test_common_http.py index 3388c94..7f0af13 100644 --- a/heatclient/tests/test_common_http.py +++ b/heatclient/tests/test_common_http.py @@ -1,3 +1,4 @@ +#-*- 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 @@ -320,6 +321,70 @@ class HttpClientTest(testtools.TestCase): self.assertEqual(200, resp.status_code) self.m.VerifyAll() + def test_http_manual_redirect_post(self): + mock_conn = http.requests.request( + 'POST', 'http://example.com:8004/foo', + allow_redirects=False, + headers={'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'python-heatclient'}) + mock_conn.AndReturn( + fakes.FakeHTTPResponse( + 302, 'Found', + {'location': 'http://example.com:8004/foo/bar'}, + '')) + mock_conn = http.requests.request( + 'POST', 'http://example.com:8004/foo/bar', + allow_redirects=False, + headers={'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'python-heatclient'}) + mock_conn.AndReturn( + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + '{}')) + + self.m.ReplayAll() + + client = http.HTTPClient('http://example.com:8004/foo') + resp, body = client.json_request('POST', '') + + self.assertEqual(200, resp.status_code) + self.m.VerifyAll() + + def test_http_manual_redirect_put(self): + mock_conn = http.requests.request( + 'PUT', 'http://example.com:8004/foo', + allow_redirects=False, + headers={'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'python-heatclient'}) + mock_conn.AndReturn( + fakes.FakeHTTPResponse( + 302, 'Found', + {'location': 'http://example.com:8004/foo/bar'}, + '')) + mock_conn = http.requests.request( + 'PUT', 'http://example.com:8004/foo/bar', + allow_redirects=False, + headers={'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'python-heatclient'}) + mock_conn.AndReturn( + fakes.FakeHTTPResponse( + 200, 'OK', + {'content-type': 'application/json'}, + '{}')) + + self.m.ReplayAll() + + client = http.HTTPClient('http://example.com:8004/foo') + resp, body = client.json_request('PUT', '') + + self.assertEqual(200, resp.status_code) + self.m.VerifyAll() + def test_http_manual_redirect_prohibited(self): mock_conn = http.requests.request( 'DELETE', 'http://example.com:8004/foo', @@ -504,3 +569,19 @@ class HttpClientTest(testtools.TestCase): self.m.ReplayAll() client = http.HTTPClient('https://foo') self.assertEqual(client.verify_cert, "SOMEWHERE") + + def test_curl_log_i18n_headers(self): + self.m.StubOutWithMock(logging.Logger, 'debug') + kwargs = {'headers': {'Key': 'foo\xe3\x8a\x8e'}} + + mock_logging_debug = logging.Logger.debug( + u"curl -i -X GET -H 'Key: foo㊎' http://somewhere" + ) + mock_logging_debug.AndReturn(None) + + self.m.ReplayAll() + + client = http.HTTPClient('http://somewhere') + client.log_curl_request("GET", '', kwargs=kwargs) + + self.m.VerifyAll() diff --git a/heatclient/tests/test_environment_format.py b/heatclient/tests/test_environment_format.py index 8a1e6bf..b972eea 100644 --- a/heatclient/tests/test_environment_format.py +++ b/heatclient/tests/test_environment_format.py @@ -49,6 +49,18 @@ parameters: } ''' self.assertRaises(ValueError, environment_format.parse, env) + def test_parse_string_environment(self): + env = 'just string' + expect = 'The environment is not a valid YAML mapping data type.' + msg = self.assertRaises(ValueError, environment_format.parse, env) + self.assertIn(expect, msg) + + def test_parse_document(self): + env = '["foo" , "bar"]' + expect = 'The environment is not a valid YAML mapping data type.' + msg = self.assertRaises(ValueError, environment_format.parse, env) + self.assertIn(expect, msg) + class YamlParseExceptions(testtools.TestCase): diff --git a/heatclient/tests/test_software_configs.py b/heatclient/tests/test_software_configs.py new file mode 100644 index 0000000..4313e7b --- /dev/null +++ b/heatclient/tests/test_software_configs.py @@ -0,0 +1,93 @@ +# 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 testtools + +from heatclient.v1.software_configs import SoftwareConfig +from heatclient.v1.software_configs import SoftwareConfigManager + + +class SoftwareConfigTest(testtools.TestCase): + + def setUp(self): + super(SoftwareConfigTest, self).setUp() + config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' + self.config = SoftwareConfig(mock.MagicMock(), info={'id': config_id}) + self.config_id = config_id + + def test_delete(self): + self.config.manager.delete.return_value = None + self.assertIsNone(self.config.delete()) + kwargs = self.config.manager.delete.call_args[1] + self.assertEqual(self.config_id, kwargs['config_id']) + + def test_data(self): + self.assertEqual( + "<SoftwareConfig {'id': '%s'}>" % self.config_id, str(self.config)) + self.config.manager.data.return_value = None + self.config.data(name='config_mysql') + kwargs = self.config.manager.data.call_args[1] + self.assertEqual('config_mysql', kwargs['name']) + + +class SoftwareConfigManagerTest(testtools.TestCase): + + def setUp(self): + super(SoftwareConfigManagerTest, self).setUp() + self.manager = SoftwareConfigManager(mock.MagicMock()) + + def test_get(self): + config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' + data = { + 'id': config_id, + 'name': 'config_mysql', + 'group': 'Heat::Shell', + 'config': '#!/bin/bash', + 'inputs': [], + 'ouputs': [], + 'options': []} + + self.manager.client.json_request.return_value = ( + {}, {'software_config': data}) + result = self.manager.get(config_id=config_id) + self.assertEqual(SoftwareConfig(self.manager, data), result) + call_args = self.manager.client.json_request.call_args + self.assertEqual( + ('GET', '/software_configs/%s' % config_id), *call_args) + + def test_create(self): + config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' + body = { + 'name': 'config_mysql', + 'group': 'Heat::Shell', + 'config': '#!/bin/bash', + 'inputs': [], + 'ouputs': [], + 'options': []} + data = body.copy() + data['id'] = config_id + self.manager.client.json_request.return_value = ( + {}, {'software_config': data}) + result = self.manager.create(**body) + self.assertEqual(SoftwareConfig(self.manager, data), result) + args, kargs = self.manager.client.json_request.call_args + self.assertEqual('POST', args[0]) + self.assertEqual('/software_configs', args[1]) + self.assertEqual({'data': body}, kargs) + + def test_delete(self): + config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' + self.manager.delete(config_id) + call_args = self.manager.client.delete.call_args + self.assertEqual( + ('/software_configs/%s' % config_id,), *call_args) diff --git a/heatclient/tests/test_software_deployments.py b/heatclient/tests/test_software_deployments.py new file mode 100644 index 0000000..c80b292 --- /dev/null +++ b/heatclient/tests/test_software_deployments.py @@ -0,0 +1,161 @@ +# 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 testtools + +from heatclient.v1.software_deployments import SoftwareDeployment +from heatclient.v1.software_deployments import SoftwareDeploymentManager + + +class SoftwareDeploymentTest(testtools.TestCase): + + def setUp(self): + super(SoftwareDeploymentTest, self).setUp() + deployment_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' + self.deployment = SoftwareDeployment( + mock.MagicMock(), info={'id': deployment_id}) + self.deployment_id = deployment_id + + def test_delete(self): + self.deployment.manager.delete.return_value = None + self.assertIsNone(self.deployment.delete()) + kwargs = self.deployment.manager.delete.call_args[1] + self.assertEqual(self.deployment_id, kwargs['deployment_id']) + + def test_update(self): + self.assertEqual( + "<SoftwareDeployment {'id': '%s'}>" % self.deployment_id, + str(self.deployment)) + self.deployment.manager.update.return_value = None + config_id = 'd00ba4aa-db33-42e1-92f4-2a6469260107' + self.assertIsNone(self.deployment.update(config_id=config_id)) + kwargs = self.deployment.manager.update.call_args[1] + self.assertEqual(self.deployment_id, kwargs['deployment_id']) + self.assertEqual(config_id, kwargs['config_id']) + + +class SoftwareDeploymentManagerTest(testtools.TestCase): + + def setUp(self): + super(SoftwareDeploymentManagerTest, self).setUp() + self.manager = SoftwareDeploymentManager(mock.MagicMock()) + + def test_list(self): + server_id = 'fc01f89f-e151-4dc5-9c28-543c0d20ed6a' + self.manager.client.json_request.return_value = ( + {}, + {'software_deployments': []}) + result = self.manager.list(server_id=server_id) + self.assertEqual([], result) + call_args = self.manager.client.get.call_args + self.assertEqual( + ('/software_deployments?server_id=%s' % server_id,), + *call_args) + + def test_metadata(self): + server_id = 'fc01f89f-e151-4dc5-9c28-543c0d20ed6a' + metadata = { + 'group1': [{'foo': 'bar'}], + 'group2': [{'foo': 'bar'}, {'bar': 'baz'}], + } + self.manager.client.json_request.return_value = ( + {}, + {'metadata': metadata}) + result = self.manager.metadata(server_id=server_id) + self.assertEqual(metadata, result) + call_args = self.manager.client.json_request.call_args + self.assertEqual( + '/software_deployments/metadata/%s' % server_id, + call_args[0][1]) + + def test_get(self): + deployment_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' + config_id = 'd00ba4aa-db33-42e1-92f4-2a6469260107' + server_id = 'fb322564-7927-473d-8aad-68ae7fbf2abf' + data = { + 'id': deployment_id, + 'server_id': server_id, + 'input_values': {}, + 'output_values': {}, + 'action': 'INIT', + 'status': 'COMPLETE', + 'status_reason': None, + 'signal_id': None, + 'config_id': config_id, + 'config': '#!/bin/bash', + 'name': 'config_mysql', + 'group': 'Heat::Shell', + 'inputs': [], + 'outputs': [], + 'options': []} + + self.manager.client.json_request.return_value = ( + {}, {'software_deployment': data}) + result = self.manager.get(deployment_id=deployment_id) + self.assertEqual(SoftwareDeployment(self.manager, data), result) + call_args = self.manager.client.json_request.call_args + self.assertEqual( + ('GET', '/software_deployments/%s' % deployment_id), *call_args) + + def test_create(self): + deployment_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' + config_id = 'd00ba4aa-db33-42e1-92f4-2a6469260107' + server_id = 'fb322564-7927-473d-8aad-68ae7fbf2abf' + body = { + 'server_id': server_id, + 'input_values': {}, + 'action': 'INIT', + 'status': 'COMPLETE', + 'status_reason': None, + 'signal_id': None, + 'config_id': config_id} + data = body.copy() + data['id'] = deployment_id + self.manager.client.json_request.return_value = ( + {}, {'software_deployment': data}) + result = self.manager.create(**body) + self.assertEqual(SoftwareDeployment(self.manager, data), result) + args, kwargs = self.manager.client.json_request.call_args + self.assertEqual('POST', args[0]) + self.assertEqual('/software_deployments', args[1]) + self.assertEqual({'data': body}, kwargs) + + def test_delete(self): + deployment_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' + self.manager.delete(deployment_id) + call_args = self.manager.client.delete.call_args + self.assertEqual( + ('/software_deployments/%s' % deployment_id,), *call_args) + + def test_update(self): + deployment_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57' + config_id = 'd00ba4aa-db33-42e1-92f4-2a6469260107' + server_id = 'fb322564-7927-473d-8aad-68ae7fbf2abf' + body = { + 'server_id': server_id, + 'input_values': {}, + 'action': 'DEPLOYED', + 'status': 'COMPLETE', + 'status_reason': None, + 'signal_id': None, + 'config_id': config_id} + data = body.copy() + data['id'] = deployment_id + self.manager.client.json_request.return_value = ( + {}, {'software_deployment': data}) + result = self.manager.update(deployment_id, **body) + self.assertEqual(SoftwareDeployment(self.manager, data), result) + args, kwargs = self.manager.client.json_request.call_args + self.assertEqual('PUT', args[0]) + self.assertEqual('/software_deployments/%s' % deployment_id, args[1]) + self.assertEqual({'data': body}, kwargs) diff --git a/heatclient/v1/client.py b/heatclient/v1/client.py index 827a4fc..2ce7302 100644 --- a/heatclient/v1/client.py +++ b/heatclient/v1/client.py @@ -19,6 +19,8 @@ from heatclient.v1 import build_info from heatclient.v1 import events from heatclient.v1 import resource_types from heatclient.v1 import resources +from heatclient.v1 import software_configs +from heatclient.v1 import software_deployments from heatclient.v1 import stacks @@ -42,3 +44,8 @@ class Client(object): 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 = \ + software_deployments.SoftwareDeploymentManager( + self.http_client) + self.software_configs = software_configs.SoftwareConfigManager( + self.http_client) diff --git a/heatclient/v1/shell.py b/heatclient/v1/shell.py index bf2bf2b..7c9b32a 100644 --- a/heatclient/v1/shell.py +++ b/heatclient/v1/shell.py @@ -242,7 +242,7 @@ def do_stack_update(hc, args): do_stack_list(hc) -def do_list(hc): +def do_list(hc, args=None): '''DEPRECATED! Use stack-list instead.''' do_stack_list(hc) diff --git a/heatclient/v1/software_configs.py b/heatclient/v1/software_configs.py new file mode 100644 index 0000000..9fa77d3 --- /dev/null +++ b/heatclient/v1/software_configs.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. +import copy + +from heatclient.openstack.common.apiclient import base + + +class SoftwareConfig(base.Resource): + def __repr__(self): + return "<SoftwareConfig %s>" % self._info + + def delete(self): + return self.manager.delete(config_id=self.id) + + def data(self, **kwargs): + return self.manager.data(self, **kwargs) + + def to_dict(self): + return copy.deepcopy(self._info) + + +class SoftwareConfigManager(base.BaseManager): + resource_class = SoftwareConfig + + def get(self, config_id): + """Get the details for a specific software config. + + :param config_id: ID of the software config + """ + resp, body = self.client.json_request( + 'GET', '/software_configs/%s' % config_id) + + return SoftwareConfig(self, body['software_config']) + + def create(self, **kwargs): + """Create a software config.""" + resp, body = self.client.json_request('POST', '/software_configs', + data=kwargs) + + return SoftwareConfig(self, body['software_config']) + + def delete(self, config_id): + """Delete a software config.""" + self._delete("/software_configs/%s" % config_id) diff --git a/heatclient/v1/software_deployments.py b/heatclient/v1/software_deployments.py new file mode 100644 index 0000000..2a17868 --- /dev/null +++ b/heatclient/v1/software_deployments.py @@ -0,0 +1,76 @@ +# 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 copy + +from heatclient.openstack.common.apiclient import base +from heatclient.openstack.common.py3kcompat import urlutils + + +class SoftwareDeployment(base.Resource): + def __repr__(self): + return "<SoftwareDeployment %s>" % self._info + + def update(self, **fields): + self.manager.update(deployment_id=self.id, **fields) + + def delete(self): + return self.manager.delete(deployment_id=self.id) + + def to_dict(self): + return copy.deepcopy(self._info) + + +class SoftwareDeploymentManager(base.BaseManager): + resource_class = SoftwareDeployment + + def list(self, **kwargs): + """Get a list of software deployments. + :rtype: list of :class:`SoftwareDeployment` + """ + url = '/software_deployments?%s' % urlutils.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. + :rtype: list of :class:`SoftwareDeployment` + """ + url = '/software_deployments/metadata/%s' % urlutils.quote( + server_id, '') + resp, body = self.client.json_request('GET', url) + return body['metadata'] + + def get(self, deployment_id): + """Get the details for a specific software deployment. + + :param deployment_id: ID of the software deployment + """ + resp, body = self.client.json_request( + 'GET', '/software_deployments/%s' % deployment_id) + + return SoftwareDeployment(self, body['software_deployment']) + + def create(self, **kwargs): + """Create a software deployment.""" + resp, body = self.client.json_request( + 'POST', '/software_deployments', data=kwargs) + return SoftwareDeployment(self, body['software_deployment']) + + def update(self, deployment_id, **kwargs): + """Update a software deployment.""" + resp, body = self.client.json_request( + 'PUT', '/software_deployments/%s' % deployment_id, data=kwargs) + return SoftwareDeployment(self, body['software_deployment']) + + def delete(self, deployment_id): + """Delete a software deployment.""" + self._delete("/software_deployments/%s" % deployment_id) |
