summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/source/ext/gen_ref.py3
-rw-r--r--heatclient/common/environment_format.py5
-rw-r--r--heatclient/common/http.py14
-rw-r--r--heatclient/common/template_utils.py3
-rw-r--r--heatclient/tests/test_common_http.py81
-rw-r--r--heatclient/tests/test_environment_format.py12
-rw-r--r--heatclient/tests/test_software_configs.py93
-rw-r--r--heatclient/tests/test_software_deployments.py161
-rw-r--r--heatclient/v1/client.py7
-rw-r--r--heatclient/v1/shell.py2
-rw-r--r--heatclient/v1/software_configs.py53
-rw-r--r--heatclient/v1/software_deployments.py76
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)