diff options
author | Mike Fedosin <mfedosin@mirantis.com> | 2015-06-16 18:36:14 +0300 |
---|---|---|
committer | Mike Fedosin <mfedosin@mirantis.com> | 2015-08-26 20:02:24 +0300 |
commit | c71a87241e15d819b08dd182b7cc295bf6b3ab62 (patch) | |
tree | 8e4ff4a08737437f3f00365be47f8046af1af412 | |
parent | 5d847b92cdf62596dc0448adf85a797f82b51d0d (diff) | |
download | python-glanceclient-c71a87241e15d819b08dd182b7cc295bf6b3ab62.tar.gz |
Glance v3 client basic methods
This commit adds standard lifecycle methods and related tests
to the client.
Currently implemented methods: 'create', 'update', 'get', 'delete'
FastTrack
Implements-blueprint: artifact-repository
Change-Id: I03f8be8917cf1de527989c441dde24160854cf59
-rw-r--r-- | glanceclient/tests/unit/v3/test_artifacts.py | 276 | ||||
-rw-r--r-- | glanceclient/v3/__init__.py | 2 | ||||
-rw-r--r-- | glanceclient/v3/artifacts.py | 87 |
3 files changed, 360 insertions, 5 deletions
diff --git a/glanceclient/tests/unit/v3/test_artifacts.py b/glanceclient/tests/unit/v3/test_artifacts.py index 0d09ec9..91597e0 100644 --- a/glanceclient/tests/unit/v3/test_artifacts.py +++ b/glanceclient/tests/unit/v3/test_artifacts.py @@ -14,11 +14,117 @@ import testtools +from glanceclient import exc +from glanceclient.tests.unit.v3 import get_artifact_fixture from glanceclient.tests import utils from glanceclient.v3 import artifacts -data_fixtures = {} +type_fixture = {'type_name': 'adventure_time', 'type_version': '1.0.2'} + +data_fixtures = { + '/v3/artifacts/ice_kingdom/v1.0.1/drafts': { + 'POST': ( + {}, + get_artifact_fixture(id='3a4560a1-e585-443e-9b39-553b46ec92d1', + name='LSP', + version='0.0.1') + ), + }, + '/v3/artifacts/ice_kingdom/v1.0.1/87b634c1-f893-33c9-28a9-e5673c99239a': { + 'DELETE': ( + {}, + { + 'id': '87b634c1-f893-33c9-28a9-e5673c99239a', + }, + ), + 'GET': ( + {}, + get_artifact_fixture(id='87b634c1-f893-33c9-28a9-e5673c99239a'), + ), + }, + '/v3/artifacts/ice_kingdom/v1.0.1/' + '87b634c1-f893-33c9-28a9-e5673c99239a?show_level=none': { + 'GET': ( + {}, + get_artifact_fixture(id='87b634c1-f893-33c9-28a9-e5673c99239a', + version='3123.0'), + ), + }, + '/v3/artifacts/adventure_time/v1.0.2/' + '3a4560a1-e585-443e-9b39-553b46ec92d1': { + 'GET': ( + {}, + get_artifact_fixture(id='3a4560a1-e585-443e-9b39-553b46ec92d1', + finn=None, **type_fixture), + ), + 'PATCH': ( + {}, + get_artifact_fixture(id='3a4560a1-e585-443e-9b39-553b46ec92d1', + finn='human', **type_fixture) + ), + }, + '/v3/artifacts/adventure_time/v1.0.2/' + '4cd6da69-1f08-45f0-af22-a06f3106588f': { + 'GET': ( + {}, + get_artifact_fixture(id='4cd6da69-1f08-45f0-af22-a06f3106588f', + finn='dog', **type_fixture), + ), + 'PATCH': ( + {}, + get_artifact_fixture(id='4cd6da69-1f08-45f0-af22-a06f3106588f', + finn='human', **type_fixture) + ), + }, + '/v3/artifacts/adventure_time/v1.0.2/' + '55f2bcf0-f34d-4c06-bb67-fa43b439ab20': { + 'GET': ( + {}, + get_artifact_fixture(id='55f2bcf0-f34d-4c06-bb67-fa43b439ab20', + finn='human', **type_fixture), + ), + 'PATCH': ( + {}, + get_artifact_fixture(id='55f2bcf0-f34d-4c06-bb67-fa43b439ab20', + **type_fixture) + ), + }, + '/v3/artifacts/adventure_time/v1.0.2/' + '73a0ebdd-6b32-4536-a529-c8301f2af2c6': { + 'GET': ( + {}, + get_artifact_fixture(id='73a0ebdd-6b32-4536-a529-c8301f2af2c6', + **type_fixture), + ), + 'PATCH': ( + {}, + get_artifact_fixture(id='73a0ebdd-6b32-4536-a529-c8301f2af2c6', + name='Marceline', **type_fixture) + ), + }, + '/v3/artifacts/adventure_time/v1.0.2/' + 'c859cae3-d924-45b7-a0da-fcc30ad9c6ab': { + 'GET': ( + {}, + get_artifact_fixture(id='c859cae3-d924-45b7-a0da-fcc30ad9c6ab', + **type_fixture) + ), + }, + '/v3/artifacts/adventure_time/v1.0.2/' + '9dcbafea-ebea-40a8-8f4b-5c54d6d58e1f': { + 'GET': ( + {}, + get_artifact_fixture(id='9dcbafea-ebea-40a8-8f4b-5c54d6d58e1f', + finn='dog', **type_fixture), + ), + 'PATCH': ( + {}, + get_artifact_fixture(id='9dcbafea-ebea-40a8-8f4b-5c54d6d58e1f', + finn='human', **type_fixture) + ), + }, +} class TestController(testtools.TestCase): @@ -26,3 +132,171 @@ class TestController(testtools.TestCase): super(TestController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.controller = artifacts.Controller(self.api) + + def test_create_artifact(self): + artifact_fixture = get_artifact_fixture(name='LSP', + version='0.0.1') + artifact = self.controller.create(**artifact_fixture) + self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', + artifact.id) + self.assertEqual('LSP', artifact.name) + self.assertEqual('0.0.1', artifact.version) + + def test_create_artifact_no_type_version(self): + artifact_fixture = get_artifact_fixture(name='artifact-1', + version='0.0.1') + del artifact_fixture['type_version'] + self.assertRaises(exc.HTTPBadRequest, self.controller.create, + **artifact_fixture) + + def test_create_artifact_no_type_name(self): + artifact_fixture = get_artifact_fixture(name='artifact-1', + version='0.0.1') + del artifact_fixture['type_name'] + self.assertRaises(exc.HTTPBadRequest, self.controller.create, + **artifact_fixture) + + def test_delete_artifact(self): + artifact_id = '87b634c1-f893-33c9-28a9-e5673c99239a' + + self.controller.delete(artifact_id, 'ice_kingdom', '1.0.1') + expect = [ + ('DELETE', + '/v3/artifacts/ice_kingdom/v1.0.1/%s' % artifact_id, + {}, + None)] + self.assertEqual(expect, self.api.calls) + + def test_get_artifact(self): + artifact_id = '87b634c1-f893-33c9-28a9-e5673c99239a' + + artifact = self.controller.get(artifact_id, 'ice_kingdom', '1.0.1') + self.assertEqual('Gunter The Penguin', artifact.name) + self.assertEqual('11.2', artifact.version) + + def test_get_artifact_show_level(self): + artifact_id = '87b634c1-f893-33c9-28a9-e5673c99239a' + show_level = 'none' + artifact = self.controller.get(artifact_id, 'ice_kingdom', '1.0.1', + show_level) + self.assertEqual('Gunter The Penguin', artifact.name) + self.assertEqual('3123.0', artifact.version) + + def test_get_artifact_invalid_show_level(self): + artifact_id = '87b634c1-f893-33c9-28a9-e5673c99239a' + show_level = 'invalid' + self.assertRaises(exc.HTTPBadRequest, self.controller.get, + artifact_id, 'ice_kingdom', '1.0.1', show_level) + + def test_update_add_custom_prop(self): + artifact_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' + + artifact = self.controller.update(artifact_id, finn='human', + **type_fixture) + expect_hdrs = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch', + } + expect_body = [{'op': 'add', 'path': '/finn', 'value': 'human'}] + expect = [ + ('GET', '/v3/artifacts/adventure_time/v1.0.2/' + '%s' % artifact_id, {}, None), + ('PATCH', '/v3/artifacts/adventure_time/v1.0.2/' + '%s' % artifact_id, expect_hdrs, expect_body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(artifact_id, artifact.id) + self.assertEqual('Gunter The Penguin', artifact.name) + self.assertEqual('human', artifact.type_specific_properties['finn']) + + def test_update_replace_custom_prop(self): + artifact_id = '4cd6da69-1f08-45f0-af22-a06f3106588f' + + artifact = self.controller.update(artifact_id, finn='human', + **type_fixture) + expect_hdrs = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch', + } + expect_body = [{'op': 'replace', 'path': '/finn', 'value': 'human'}] + expect = [ + ('GET', '/v3/artifacts/adventure_time/v1.0.2/' + '%s' % artifact_id, {}, None), + ('PATCH', '/v3/artifacts/adventure_time/v1.0.2/' + '%s' % artifact_id, expect_hdrs, expect_body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(artifact_id, artifact.id) + self.assertEqual('Gunter The Penguin', artifact.name) + self.assertEqual('human', artifact.type_specific_properties['finn']) + + def test_update_delete_custom_prop(self): + artifact_id = '55f2bcf0-f34d-4c06-bb67-fa43b439ab20' + + artifact = self.controller.update(artifact_id, remove_props=['finn'], + **type_fixture) + expect_hdrs = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch', + } + expect_body = [{'op': 'remove', 'path': '/finn'}] + expect = [ + ('GET', '/v3/artifacts/adventure_time/v1.0.2/' + '%s' % artifact_id, {}, None), + ('PATCH', '/v3/artifacts/adventure_time/v1.0.2/' + '%s' % artifact_id, expect_hdrs, expect_body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(artifact_id, artifact.id) + self.assertEqual('Gunter The Penguin', artifact.name) + self.assertNotIn('finn', artifact.type_specific_properties) + + def test_update_replace_base_prop(self): + artifact_id = '73a0ebdd-6b32-4536-a529-c8301f2af2c6' + + artifact = self.controller.update(artifact_id, name='Marceline', + **type_fixture) + expect_hdrs = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch', + } + expect_body = [{'op': 'replace', 'path': '/name', + 'value': 'Marceline'}] + expect = [ + ('GET', '/v3/artifacts/adventure_time/v1.0.2/' + '%s' % artifact_id, {}, None), + ('PATCH', '/v3/artifacts/adventure_time/v1.0.2/' + '%s' % artifact_id, expect_hdrs, expect_body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(artifact_id, artifact.id) + self.assertEqual('Marceline', artifact.name) + + def test_update_remove_base_prop(self): + artifact_id = '73a0ebdd-6b32-4536-a529-c8301f2af2c6' + + self.assertRaises(exc.HTTPBadRequest, self.controller.update, + artifact_id, remove_props=['name'], **type_fixture) + + def test_update_add_nonexiting_property(self): + artifact_id = 'c859cae3-d924-45b7-a0da-fcc30ad9c6ab' + + self.assertRaises(exc.HTTPBadRequest, self.controller.update, + artifact_id, finn='human', **type_fixture) + + def test_remove_and_replace_same_property(self): + artifact_id = '9dcbafea-ebea-40a8-8f4b-5c54d6d58e1f' + + artifact = self.controller.update(artifact_id, finn='human', + remove_props=['finn'], + **type_fixture) + expect_hdrs = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch', + } + expect_body = [{'op': 'replace', 'path': '/finn', 'value': 'human'}] + expect = [ + ('GET', '/v3/artifacts/adventure_time/v1.0.2/' + '%s' % artifact_id, {}, None), + ('PATCH', '/v3/artifacts/adventure_time/v1.0.2/' + '%s' % artifact_id, expect_hdrs, expect_body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(artifact_id, artifact.id) + self.assertEqual('Gunter The Penguin', artifact.name) + self.assertEqual('human', artifact.type_specific_properties['finn']) diff --git a/glanceclient/v3/__init__.py b/glanceclient/v3/__init__.py index 27d99b5..937309b 100644 --- a/glanceclient/v3/__init__.py +++ b/glanceclient/v3/__init__.py @@ -22,6 +22,8 @@ class ArtifactType(object): 'version', 'visibility', 'description', 'tags', 'published_at', 'deleted_at') + supported_show_levels = ('none', 'basic', 'direct', 'transitive') + def __init__(self, **kwargs): try: for prop in self.generic_properties: diff --git a/glanceclient/v3/artifacts.py b/glanceclient/v3/artifacts.py index 0ddd308..2440a36 100644 --- a/glanceclient/v3/artifacts.py +++ b/glanceclient/v3/artifacts.py @@ -11,7 +11,9 @@ # 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 glanceclient import exc +from glanceclient.v3 import ArtifactType class Controller(object): @@ -37,15 +39,84 @@ class Controller(object): def create(self, name, version, type_name=None, type_version=None, **kwargs): - raise NotImplementedError() + """Create an artifact of given type and version. + + :param name: name of creating artifact. + :param version: semver string describing an artifact version + """ + type_name, type_version = self._check_type_params(type_name, + type_version) + kwargs.update({'name': name, 'version': version}) + url = '/v3/artifacts/%s/v%s/drafts' % (type_name, type_version) + resp, body = self.http_client.post(url, data=kwargs) + return ArtifactType(**body) def update(self, artifact_id, type_name=None, type_version=None, remove_props=None, **kwargs): - raise NotImplementedError() + """Update attributes of an artifact. + + :param artifact_id: ID of the artifact to modify. + :param remove_props: List of property names to remove + :param \*\*kwargs: Artifact attribute names and their new values. + """ + type_name, type_version = self._check_type_params(type_name, + type_version) + url = '/v3/artifacts/%s/v%s/%s' % (type_name, type_version, + artifact_id) + hdrs = { + 'Content-Type': 'application/openstack-images-v2.1-json-patch'} + + artifact_obj = self.get(artifact_id, type_name, type_version) + + changes = [] + if remove_props: + for prop in remove_props: + if prop in ArtifactType.generic_properties: + msg = "Generic properties cannot be removed" + raise exc.HTTPBadRequest(msg) + if prop not in kwargs: + changes.append({'op': 'remove', + 'path': '/' + prop}) + + for prop in kwargs: + if prop in artifact_obj.generic_properties: + op = 'add' if getattr(artifact_obj, + prop) is None else 'replace' + elif prop in artifact_obj.type_specific_properties: + if artifact_obj.type_specific_properties[prop] is None: + op = 'add' + else: + op = 'replace' + else: + msg = ("Property '%s' doesn't exist in type '%s' with version" + " '%s'" % (prop, type_name, type_version)) + raise exc.HTTPBadRequest(msg) + changes.append({'op': op, 'path': '/' + prop, + 'value': kwargs[prop]}) + + resp, body = self.http_client.patch(url, headers=hdrs, data=changes) + return ArtifactType(**body) def get(self, artifact_id, type_name=None, type_version=None, show_level=None): - raise NotImplementedError() + """Get information about an artifact. + + :param artifact_id: ID of the artifact to get. + :param show_level: value of datalization. Possible values: + "none", "basic", "direct", "transitive" + """ + type_name, type_version = self._check_type_params(type_name, + type_version) + + url = '/v3/artifacts/%s/v%s/%s' % (type_name, type_version, + artifact_id) + if show_level: + if show_level not in ArtifactType.supported_show_levels: + msg = "Invalid show level: %s" % show_level + raise exc.HTTPBadRequest(msg) + url += '?show_level=%s' % show_level + resp, body = self.http_client.get(url) + return ArtifactType(**body) def list(self, type_name=None, type_version=None, **kwargs): raise NotImplementedError() @@ -57,7 +128,15 @@ class Controller(object): raise NotImplementedError() def delete(self, artifact_id, type_name=None, type_version=None): - raise NotImplementedError() + """Delete an artifact and all its data. + + :param artifact_id: ID of the artifact to delete. + """ + type_name, type_version = self._check_type_params(type_name, + type_version) + url = '/v3/artifacts/%s/v%s/%s' % (type_name, type_version, + artifact_id) + self.http_client.delete(url) def upload_blob(self, artifact_id, blob_property, data, position=None, type_name=None, type_version=None): |