Merge "Add 'explain' command to v2 that describes schemas"
This commit is contained in:
@@ -22,6 +22,11 @@ class EndpointNotFound(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaNotFound(KeyError):
|
||||||
|
"""Could not find schema"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ClientException(Exception):
|
class ClientException(Exception):
|
||||||
"""
|
"""
|
||||||
The base exception class for all exceptions this library raises.
|
The base exception class for all exceptions this library raises.
|
||||||
|
@@ -17,6 +17,7 @@ import logging
|
|||||||
|
|
||||||
from glanceclient.common import http
|
from glanceclient.common import http
|
||||||
from glanceclient.v2 import images
|
from glanceclient.v2 import images
|
||||||
|
from glanceclient.v2 import schemas
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -32,7 +33,8 @@ class Client(object):
|
|||||||
http requests. (optional)
|
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(
|
self.http_client = http.HTTPClient(
|
||||||
endpoint, token=token, timeout=timeout)
|
endpoint, token=token, timeout=timeout)
|
||||||
self.images = images.Controller(self.http_client)
|
self.images = images.Controller(self.http_client)
|
||||||
|
self.schemas = schemas.Controller(self.http_client)
|
||||||
|
@@ -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)
|
||||||
|
@@ -21,3 +21,11 @@ def do_image_list(gc, args):
|
|||||||
images = gc.images.list()
|
images = gc.images.list()
|
||||||
columns = ['ID', 'Name']
|
columns = ['ID', 'Name']
|
||||||
utils.print_list(images, columns)
|
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
33
tests/utils.py
Normal 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)
|
@@ -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
|
import unittest
|
||||||
|
|
||||||
from tests.v1 import utils
|
|
||||||
|
|
||||||
import glanceclient.v1.images
|
import glanceclient.v1.images
|
||||||
import glanceclient.v1.image_members
|
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):
|
class ImageMemberManagerTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.api = utils.FakeAPI()
|
self.api = utils.FakeAPI(fixtures)
|
||||||
self.mgr = glanceclient.v1.image_members.ImageMemberManager(self.api)
|
self.mgr = glanceclient.v1.image_members.ImageMemberManager(self.api)
|
||||||
self.image = glanceclient.v1.images.Image(self.api, {'id': '1'}, True)
|
self.image = glanceclient.v1.images.Image(self.api, {'id': '1'}, True)
|
||||||
|
|
||||||
|
@@ -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 StringIO
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from tests.v1 import utils
|
|
||||||
|
|
||||||
import glanceclient.v1.images
|
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):
|
class ImageManagerTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.api = utils.FakeAPI()
|
self.api = utils.FakeAPI(fixtures)
|
||||||
self.mgr = glanceclient.v1.images.ImageManager(self.api)
|
self.mgr = glanceclient.v1.images.ImageManager(self.api)
|
||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
@@ -135,7 +212,7 @@ class ImageManagerTest(unittest.TestCase):
|
|||||||
|
|
||||||
class ImageTest(unittest.TestCase):
|
class ImageTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.api = utils.FakeAPI()
|
self.api = utils.FakeAPI(fixtures)
|
||||||
self.mgr = glanceclient.v1.images.ImageManager(self.api)
|
self.mgr = glanceclient.v1.images.ImageManager(self.api)
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
|
@@ -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
0
tests/v2/__init__.py
Normal file
80
tests/v2/test_schemas.py
Normal file
80
tests/v2/test_schemas.py
Normal 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'])
|
Reference in New Issue
Block a user