diff --git a/glanceclient/common/exceptions.py b/glanceclient/common/exceptions.py index e2b71cbf..fd8ed74d 100644 --- a/glanceclient/common/exceptions.py +++ b/glanceclient/common/exceptions.py @@ -22,6 +22,11 @@ class EndpointNotFound(Exception): pass +class SchemaNotFound(KeyError): + """Could not find schema""" + pass + + class ClientException(Exception): """ The base exception class for all exceptions this library raises. diff --git a/glanceclient/v2/client.py b/glanceclient/v2/client.py index 99b17933..0435f64a 100644 --- a/glanceclient/v2/client.py +++ b/glanceclient/v2/client.py @@ -17,6 +17,7 @@ import logging from glanceclient.common import http from glanceclient.v2 import images +from glanceclient.v2 import schemas logger = logging.getLogger(__name__) @@ -32,7 +33,8 @@ class Client(object): http requests. (optional) """ - def __init__(self, endpoint, token=None, timeout=600): + def __init__(self, endpoint, token=None, timeout=600, **kwargs): self.http_client = http.HTTPClient( endpoint, token=token, timeout=timeout) self.images = images.Controller(self.http_client) + self.schemas = schemas.Controller(self.http_client) diff --git a/glanceclient/v2/schemas.py b/glanceclient/v2/schemas.py index e69de29b..41e94e80 100644 --- a/glanceclient/v2/schemas.py +++ b/glanceclient/v2/schemas.py @@ -0,0 +1,58 @@ +# Copyright 2012 OpenStack LLC. +# 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. + +from glanceclient.common import exceptions + + +class SchemaProperty(object): + def __init__(self, name, **kwargs): + self.name = name + self.description = kwargs.get('description') + + +def translate_schema_properties(schema_properties): + """Parse the properties dictionary of a schema document + + :returns list of SchemaProperty objects + """ + properties = [] + for (name, prop) in schema_properties.items(): + properties.append(SchemaProperty(name, **prop)) + return properties + + +class Schema(object): + def __init__(self, raw_schema): + self._raw_schema = raw_schema + self.name = raw_schema['name'] + raw_properties = raw_schema['properties'] + self.properties = translate_schema_properties(raw_properties) + + +class Controller(object): + def __init__(self, http_client): + self.http_client = http_client + + def get(self, schema_name): + uri = self._find_schema_uri(schema_name) + _, raw_schema = self.http_client.json_request('GET', uri) + return Schema(raw_schema) + + def _find_schema_uri(self, schema_name): + _, schema_index = self.http_client.json_request('GET', '/v2/schemas') + for link in schema_index['links']: + if link['rel'] == schema_name: + return link['href'] + raise exceptions.SchemaNotFound(schema_name) diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py index b12b1b90..90c2ea8a 100644 --- a/glanceclient/v2/shell.py +++ b/glanceclient/v2/shell.py @@ -21,3 +21,11 @@ def do_image_list(gc, args): images = gc.images.list() columns = ['ID', 'Name'] utils.print_list(images, columns) + + +@utils.arg('name', metavar='', help='Name of model to describe.') +def do_explain(gc, args): + """Describe a specific model.""" + schema = gc.schemas.get(args.name) + columns = ['Name', 'Description'] + utils.print_list(schema.properties, columns) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..c3563918 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,33 @@ +# Copyright 2012 OpenStack LLC. +# 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. + + +class FakeAPI(object): + def __init__(self, fixtures): + self.fixtures = fixtures + self.calls = [] + + def _request(self, method, url, headers=None, body=None): + call = (method, url, headers or {}, body) + self.calls.append(call) + # drop any query params + url = url.split('?', 1)[0] + return self.fixtures[url][method] + + def raw_request(self, *args, **kwargs): + return self._request(*args, **kwargs) + + def json_request(self, *args, **kwargs): + return self._request(*args, **kwargs) diff --git a/tests/v1/test_image_members.py b/tests/v1/test_image_members.py index 2a0ed38b..1013ad88 100644 --- a/tests/v1/test_image_members.py +++ b/tests/v1/test_image_members.py @@ -1,16 +1,61 @@ +# Copyright 2012 OpenStack LLC. +# 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 unittest -from tests.v1 import utils - import glanceclient.v1.images import glanceclient.v1.image_members +from tests import utils + + +fixtures = { + '/v1/images/1/members': { + 'GET': ( + {}, + {'members': [ + {'member_id': '1', 'can_share': False}, + ]}, + ), + 'PUT': ({}, None), + }, + '/v1/images/1/members/1': { + 'GET': ( + {}, + {'member': { + 'member_id': '1', + 'can_share': False, + }}, + ), + 'PUT': ({}, None), + 'DELETE': ({}, None), + }, + '/v1/shared-images/1': { + 'GET': ( + {}, + {'shared_images': [ + {'image_id': '1', 'can_share': False}, + ]}, + ), + }, +} class ImageMemberManagerTest(unittest.TestCase): def setUp(self): - self.api = utils.FakeAPI() + self.api = utils.FakeAPI(fixtures) self.mgr = glanceclient.v1.image_members.ImageMemberManager(self.api) self.image = glanceclient.v1.images.Image(self.api, {'id': '1'}, True) diff --git a/tests/v1/test_images.py b/tests/v1/test_images.py index 4e9436e4..8a183605 100644 --- a/tests/v1/test_images.py +++ b/tests/v1/test_images.py @@ -1,16 +1,93 @@ +# Copyright 2012 OpenStack LLC. +# 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 json import StringIO import unittest -from tests.v1 import utils - import glanceclient.v1.images +from tests import utils + + +fixtures = { + '/v1/images': { + 'POST': ( + { + 'location': '/v1/images/1', + }, + json.dumps( + {'image': { + 'id': '1', + 'name': 'image-1', + 'container_format': 'ovf', + 'disk_format': 'vhd', + 'owner': 'asdf', + 'size': '1024', + 'min_ram': '512', + 'min_disk': '10', + 'properties': {'a': 'b', 'c': 'd'}, + }}, + ), + ), + }, + '/v1/images/detail': { + 'GET': ( + {}, + {'images': [ + { + 'id': '1', + 'name': 'image-1', + 'properties': {'arch': 'x86_64'}, + }, + ]}, + ), + }, + '/v1/images/1': { + 'HEAD': ( + { + 'x-image-meta-id': '1', + 'x-image-meta-name': 'image-1', + 'x-image-meta-property-arch': 'x86_64', + }, + None), + 'PUT': ( + {}, + json.dumps( + {'image': { + 'id': '1', + 'name': 'image-2', + 'container_format': 'ovf', + 'disk_format': 'vhd', + 'owner': 'asdf', + 'size': '1024', + 'min_ram': '512', + 'min_disk': '10', + 'properties': {'a': 'b', 'c': 'd'}, + }}, + ), + ), + 'DELETE': ({}, None), + }, + +} class ImageManagerTest(unittest.TestCase): def setUp(self): - self.api = utils.FakeAPI() + self.api = utils.FakeAPI(fixtures) self.mgr = glanceclient.v1.images.ImageManager(self.api) def test_list(self): @@ -135,7 +212,7 @@ class ImageManagerTest(unittest.TestCase): class ImageTest(unittest.TestCase): def setUp(self): - self.api = utils.FakeAPI() + self.api = utils.FakeAPI(fixtures) self.mgr = glanceclient.v1.images.ImageManager(self.api) def test_delete(self): diff --git a/tests/v1/utils.py b/tests/v1/utils.py deleted file mode 100644 index c38519b4..00000000 --- a/tests/v1/utils.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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 json - - -fixtures = { - '/v1/images': { - 'POST': ( - { - 'location': '/v1/images/1', - }, - json.dumps( - {'image': { - 'id': '1', - 'name': 'image-1', - 'container_format': 'ovf', - 'disk_format': 'vhd', - 'owner': 'asdf', - 'size': '1024', - 'min_ram': '512', - 'min_disk': '10', - 'properties': {'a': 'b', 'c': 'd'}, - }}, - ), - ), - }, - '/v1/images/detail': { - 'GET': ( - {}, - {'images': [ - { - 'id': '1', - 'name': 'image-1', - 'properties': {'arch': 'x86_64'}, - }, - ]}, - ), - }, - '/v1/images/1': { - 'HEAD': ( - { - 'x-image-meta-id': '1', - 'x-image-meta-name': 'image-1', - 'x-image-meta-property-arch': 'x86_64', - }, - None), - 'PUT': ( - {}, - json.dumps( - {'image': { - 'id': '1', - 'name': 'image-2', - 'container_format': 'ovf', - 'disk_format': 'vhd', - 'owner': 'asdf', - 'size': '1024', - 'min_ram': '512', - 'min_disk': '10', - 'properties': {'a': 'b', 'c': 'd'}, - }}, - ), - ), - 'DELETE': ({}, None), - }, - '/v1/images/1/members': { - 'GET': ( - {}, - {'members': [ - {'member_id': '1', 'can_share': False}, - ]}, - ), - 'PUT': ({}, None), - }, - '/v1/images/1/members/1': { - 'GET': ( - {}, - {'member': { - 'member_id': '1', - 'can_share': False, - }}, - ), - 'PUT': ({}, None), - 'DELETE': ({}, None), - }, - '/v1/shared-images/1': { - 'GET': ( - {}, - {'shared_images': [ - {'image_id': '1', 'can_share': False}, - ]}, - ), - }, -} - - -class FakeAPI(object): - - def __init__(self): - self.calls = [] - - def _request(self, method, url, headers=None, body=None): - call = (method, url, headers or {}, body) - self.calls.append(call) - # drop any query params - url = url.split('?', 1)[0] - return fixtures[url][method] - - def raw_request(self, *args, **kwargs): - return self._request(*args, **kwargs) - - def json_request(self, *args, **kwargs): - return self._request(*args, **kwargs) diff --git a/tests/v2/__init__.py b/tests/v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/v2/test_schemas.py b/tests/v2/test_schemas.py new file mode 100644 index 00000000..b67b976b --- /dev/null +++ b/tests/v2/test_schemas.py @@ -0,0 +1,80 @@ +# Copyright 2012 OpenStack LLC. +# 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 unittest + +from glanceclient.v2 import schemas +from tests import utils + + +fixtures = { + '/v2/schemas': { + 'GET': ( + {}, + {'links': [ + {'rel': 'image', 'href': '/v2/schemas/image'}, + {'rel': 'access', 'href': '/v2/schemas/image/access'}, + ]}, + ), + }, + '/v2/schemas/image': { + 'GET': ( + {}, + { + 'name': 'image', + 'properties': { + 'name': {'type': 'string', 'description': 'Name of image'}, + }, + }, + ), + }, +} + + +class TestSchemaProperty(unittest.TestCase): + def test_property_minimum(self): + prop = schemas.SchemaProperty('size') + self.assertEqual(prop.name, 'size') + + def test_property_description(self): + prop = schemas.SchemaProperty('size', description='some quantity') + self.assertEqual(prop.name, 'size') + self.assertEqual(prop.description, 'some quantity') + + +class TestSchema(unittest.TestCase): + def test_schema_minimum(self): + raw_schema = {'name': 'Country', 'properties': {}} + schema = schemas.Schema(raw_schema) + self.assertEqual(schema.name, 'Country') + self.assertEqual(schema.properties, []) + + def test_schema_with_property(self): + raw_schema = {'name': 'Country', 'properties': {'size': {}}} + schema = schemas.Schema(raw_schema) + self.assertEqual(schema.name, 'Country') + self.assertEqual([p.name for p in schema.properties], ['size']) + + +class TestController(unittest.TestCase): + def setUp(self): + super(TestController, self).setUp() + self.api = utils.FakeAPI(fixtures) + self.controller = schemas.Controller(self.api) + + def test_get_schema(self): + schema = self.controller.get('image') + self.assertEqual(schema.name, 'image') + self.assertEqual([p.name for p in schema.properties], ['name'])