Add 'explain' command to v2 that describes schemas

At its core, this is adding the ability to finx a schema through
published links and convert it to a usable object.

Related to bp glance-client-v2

Change-Id: I7b38ad091c6b0ad80197eb789503cf73989893e5
This commit is contained in:
Brian Waldon 2012-06-07 14:01:50 -07:00
parent 2822748bc1
commit 7f48506781
10 changed files with 316 additions and 133 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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='<NAME>', 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)

33
tests/utils.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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)

0
tests/v2/__init__.py Normal file
View File

80
tests/v2/test_schemas.py Normal file
View File

@ -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'])