From f0445a1b443182cf77e27038cc5f90c424e00c62 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 13 Jul 2012 22:03:22 +0000 Subject: [PATCH 1/5] Rewrite link parsing for finding v2 schemas What we called 'links' are no longer returned in a container of objects, they are top-level entity attribtues. This fixes the parsing of the entities to look in the correct place when trying to locate a specific schema. Add a helper for printing to stderr and exiting with a non-zero exit code. Map 'name' to 'Attribute' when explaining a schema. Related to bp glance-client-v2 Change-Id: Ib98e912a7af0bb570b4fd738733edd9b837d1a05 --- glanceclient/common/utils.py | 9 ++++++++- glanceclient/v2/schemas.py | 8 ++++---- glanceclient/v2/shell.py | 14 ++++++++++---- tests/v2/test_schemas.py | 8 ++++---- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/glanceclient/common/utils.py b/glanceclient/common/utils.py index 8bbb7ac..07e77a1 100644 --- a/glanceclient/common/utils.py +++ b/glanceclient/common/utils.py @@ -14,6 +14,7 @@ # under the License. import os +import sys import uuid import prettytable @@ -47,7 +48,7 @@ def print_list(objs, fields, formatters={}): row.append(formatters[field](o)) else: field_name = field.lower().replace(' ', '_') - data = getattr(o, field_name, '') + data = getattr(o, field_name, None) or '' row.append(data) pt.add_row(row) @@ -123,3 +124,9 @@ def import_versioned_module(version, submodule=None): if submodule: module = '.'.join((module, submodule)) return importutils.import_module(module) + + +def exit(msg=''): + if msg: + print >> sys.stderr, msg + sys.exit(1) diff --git a/glanceclient/v2/schemas.py b/glanceclient/v2/schemas.py index 8757da1..b4ed61c 100644 --- a/glanceclient/v2/schemas.py +++ b/glanceclient/v2/schemas.py @@ -52,7 +52,7 @@ class Controller(object): 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 exc.SchemaNotFound(schema_name) + try: + return schema_index[schema_name] + except KeyError: + raise exc.SchemaNotFound(schema_name) diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py index 90c2ea8..32a8487 100644 --- a/glanceclient/v2/shell.py +++ b/glanceclient/v2/shell.py @@ -14,6 +14,7 @@ # under the License. from glanceclient.common import utils +from glanceclient import exc def do_image_list(gc, args): @@ -23,9 +24,14 @@ def do_image_list(gc, args): utils.print_list(images, columns) -@utils.arg('name', metavar='', help='Name of model to describe.') +@utils.arg('model', 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) + try: + schema = gc.schemas.get(args.model) + except exc.SchemaNotFound: + utils.exit('Unable to find requested model \'%s\'' % args.model) + else: + formatters = {'Attribute': lambda m: m.name} + columns = ['Attribute', 'Description'] + utils.print_list(schema.properties, columns, formatters) diff --git a/tests/v2/test_schemas.py b/tests/v2/test_schemas.py index b67b976..6e64c29 100644 --- a/tests/v2/test_schemas.py +++ b/tests/v2/test_schemas.py @@ -23,10 +23,10 @@ fixtures = { '/v2/schemas': { 'GET': ( {}, - {'links': [ - {'rel': 'image', 'href': '/v2/schemas/image'}, - {'rel': 'access', 'href': '/v2/schemas/image/access'}, - ]}, + { + 'image': '/v2/schemas/image', + 'access': '/v2/schemas/image/access', + }, ), }, '/v2/schemas/image': { From b6cef9d145f870dd717843751f0c5d68867e07d5 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 13 Jul 2012 23:05:38 +0000 Subject: [PATCH 2/5] Add support for viewing a single image through v2 * Add image-create command * Add tests for Image model, Controller.get, and Controller.list * Related to bp glance-client-v2 Change-Id: Ib98e912a7af0bb570b4fd738733edd9b837d1a06 --- glanceclient/v2/images.py | 8 ++++ glanceclient/v2/shell.py | 7 ++++ tests/v2/test_images.py | 79 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 tests/v2/test_images.py diff --git a/glanceclient/v2/images.py b/glanceclient/v2/images.py index 6199086..660e259 100644 --- a/glanceclient/v2/images.py +++ b/glanceclient/v2/images.py @@ -19,6 +19,9 @@ class Image(object): self.id = id self.name = name + def iteritems(self): + return {'id': self.id, 'name': self.name}.iteritems() + class Controller(object): def __init__(self, http_client): @@ -27,3 +30,8 @@ class Controller(object): def list(self): resp, body = self.http_client.json_request('GET', '/v2/images') return [Image(i['id'], i['name']) for i in body['images']] + + def get(self, image_id): + url = '/v2/images/%s' % image_id + resp, body = self.http_client.json_request('GET', url) + return Image(body['image']['id'], body['image']['name']) diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py index 32a8487..7a100a7 100644 --- a/glanceclient/v2/shell.py +++ b/glanceclient/v2/shell.py @@ -24,6 +24,13 @@ def do_image_list(gc, args): utils.print_list(images, columns) +@utils.arg('id', metavar='', help='ID of image to describe.') +def do_image_show(gc, args): + """Describe a specific image.""" + image = gc.images.get(args.id) + utils.print_dict(image) + + @utils.arg('model', metavar='', help='Name of model to describe.') def do_explain(gc, args): """Describe a specific model.""" diff --git a/tests/v2/test_images.py b/tests/v2/test_images.py new file mode 100644 index 0000000..f6f1292 --- /dev/null +++ b/tests/v2/test_images.py @@ -0,0 +1,79 @@ +# 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 images +from tests import utils + + +fixtures = { + '/v2/images': { + 'GET': ( + {}, + {'images': [ + { + 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', + 'name': 'image-1', + }, + { + 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', + 'name': 'image-2', + }, + ]}, + ), + }, + '/v2/images/3a4560a1-e585-443e-9b39-553b46ec92d1': { + 'GET': ( + {}, + { + 'image': { + 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', + 'name': 'image-1', + }, + }, + ), + }, +} + + +class TestImage(unittest.TestCase): + def test_image_minimum(self): + raw_image = { + 'id': '8a5b2424-9751-498b-925f-66f62747c501', + 'name': 'image-7', + } + image = images.Image(**raw_image) + self.assertEqual(image.id, '8a5b2424-9751-498b-925f-66f62747c501') + self.assertEqual(image.name, 'image-7') + + +class TestController(unittest.TestCase): + def setUp(self): + super(TestController, self).setUp() + self.api = utils.FakeAPI(fixtures) + self.controller = images.Controller(self.api) + + def test_list_images(self): + images = self.controller.list() + self.assertEqual(images[0].id, '3a4560a1-e585-443e-9b39-553b46ec92d1') + self.assertEqual(images[0].name, 'image-1') + self.assertEqual(images[1].id, '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810') + self.assertEqual(images[1].name, 'image-2') + + def test_get_image(self): + image = self.controller.get('3a4560a1-e585-443e-9b39-553b46ec92d1') + self.assertEqual(image.id, '3a4560a1-e585-443e-9b39-553b46ec92d1') + self.assertEqual(image.name, 'image-1') From c398af18b0b8fb5fb075be22563812e179290b2a Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Sat, 14 Jul 2012 01:11:22 +0000 Subject: [PATCH 3/5] Replace static v2 Image model with warlock model * Add warlock v0.1.0 as a dependency * Generate a pythonic, self-validating Image model using warlock * Add raw method to Schema model * Related to bp glance-client-v2 Change-Id: Ib98e912a7af0bb570b4fd738733edd9b837d1a04 --- glanceclient/v2/client.py | 9 ++++++++- glanceclient/v2/images.py | 25 +++++++++++++------------ glanceclient/v2/schemas.py | 5 +++++ tests/v2/test_images.py | 15 +++++---------- tests/v2/test_schemas.py | 5 +++++ tools/pip-requires | 1 + 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/glanceclient/v2/client.py b/glanceclient/v2/client.py index 0435f64..cc0c383 100644 --- a/glanceclient/v2/client.py +++ b/glanceclient/v2/client.py @@ -15,6 +15,8 @@ import logging +import warlock + from glanceclient.common import http from glanceclient.v2 import images from glanceclient.v2 import schemas @@ -36,5 +38,10 @@ class Client(object): 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) + self.images = images.Controller(self.http_client, + self._get_image_model()) + + def _get_image_model(self): + schema = self.schemas.get('image') + return warlock.model_factory(schema.raw()) diff --git a/glanceclient/v2/images.py b/glanceclient/v2/images.py index 660e259..e5de052 100644 --- a/glanceclient/v2/images.py +++ b/glanceclient/v2/images.py @@ -14,24 +14,25 @@ # under the License. -class Image(object): - def __init__(self, id, name): - self.id = id - self.name = name - - def iteritems(self): - return {'id': self.id, 'name': self.name}.iteritems() - - class Controller(object): - def __init__(self, http_client): + def __init__(self, http_client, model): self.http_client = http_client + self.model = model def list(self): resp, body = self.http_client.json_request('GET', '/v2/images') - return [Image(i['id'], i['name']) for i in body['images']] + images = [] + for image in body['images']: + #NOTE(bcwaldon): remove 'self' for now until we have an elegant + # way to pass it into the model constructor without conflict + image.pop('self', None) + images.append(self.model(**image)) + return images def get(self, image_id): url = '/v2/images/%s' % image_id resp, body = self.http_client.json_request('GET', url) - return Image(body['image']['id'], body['image']['name']) + #NOTE(bcwaldon): remove 'self' for now until we have an elegant + # way to pass it into the model constructor without conflict + body['image'].pop('self', None) + return self.model(**body['image']) diff --git a/glanceclient/v2/schemas.py b/glanceclient/v2/schemas.py index b4ed61c..6e178d2 100644 --- a/glanceclient/v2/schemas.py +++ b/glanceclient/v2/schemas.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + from glanceclient import exc @@ -40,6 +42,9 @@ class Schema(object): raw_properties = raw_schema['properties'] self.properties = translate_schema_properties(raw_properties) + def raw(self): + return copy.deepcopy(self._raw_schema) + class Controller(object): def __init__(self, http_client): diff --git a/tests/v2/test_images.py b/tests/v2/test_images.py index f6f1292..471c9ee 100644 --- a/tests/v2/test_images.py +++ b/tests/v2/test_images.py @@ -15,6 +15,8 @@ import unittest +import warlock + from glanceclient.v2 import images from tests import utils @@ -49,22 +51,15 @@ fixtures = { } -class TestImage(unittest.TestCase): - def test_image_minimum(self): - raw_image = { - 'id': '8a5b2424-9751-498b-925f-66f62747c501', - 'name': 'image-7', - } - image = images.Image(**raw_image) - self.assertEqual(image.id, '8a5b2424-9751-498b-925f-66f62747c501') - self.assertEqual(image.name, 'image-7') +fake_schema = {'name': 'image', 'properties': {'id': {}, 'name': {}}} +FakeModel = warlock.model_factory(fake_schema) class TestController(unittest.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(fixtures) - self.controller = images.Controller(self.api) + self.controller = images.Controller(self.api, FakeModel) def test_list_images(self): images = self.controller.list() diff --git a/tests/v2/test_schemas.py b/tests/v2/test_schemas.py index 6e64c29..b11b480 100644 --- a/tests/v2/test_schemas.py +++ b/tests/v2/test_schemas.py @@ -67,6 +67,11 @@ class TestSchema(unittest.TestCase): self.assertEqual(schema.name, 'Country') self.assertEqual([p.name for p in schema.properties], ['size']) + def test_raw(self): + raw_schema = {'name': 'Country', 'properties': {}} + schema = schemas.Schema(raw_schema) + self.assertEqual(schema.raw(), raw_schema) + class TestController(unittest.TestCase): def setUp(self): diff --git a/tools/pip-requires b/tools/pip-requires index f517a02..eec261e 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -2,3 +2,4 @@ argparse httplib2 prettytable==0.6 python-keystoneclient>=0.1,<0.2 +warlock==0.1.0 From e5f038b62a9edf3f19390f2437c8a10616426903 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Sat, 14 Jul 2012 01:16:31 +0000 Subject: [PATCH 4/5] Convert v2 images list method to generator We will want this to be a generator as soon as we implement pagination. Let's establish the interface now. Related to bp glance-client-v2 Change-Id: Ib98e912a7af0bb570b4fd738733edd9b837d1a07 --- glanceclient/v2/images.py | 8 +++++--- tests/v2/test_images.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/glanceclient/v2/images.py b/glanceclient/v2/images.py index e5de052..0da8ceb 100644 --- a/glanceclient/v2/images.py +++ b/glanceclient/v2/images.py @@ -20,14 +20,16 @@ class Controller(object): self.model = model def list(self): + """Retrieve a listing of Image objects + + :returns generator over list of Images + """ resp, body = self.http_client.json_request('GET', '/v2/images') - images = [] for image in body['images']: #NOTE(bcwaldon): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict image.pop('self', None) - images.append(self.model(**image)) - return images + yield self.model(**image) def get(self, image_id): url = '/v2/images/%s' % image_id diff --git a/tests/v2/test_images.py b/tests/v2/test_images.py index 471c9ee..298d5c1 100644 --- a/tests/v2/test_images.py +++ b/tests/v2/test_images.py @@ -62,7 +62,8 @@ class TestController(unittest.TestCase): self.controller = images.Controller(self.api, FakeModel) def test_list_images(self): - images = self.controller.list() + #NOTE(bcwaldon): cast to list since the controller returns a generator + images = list(self.controller.list()) self.assertEqual(images[0].id, '3a4560a1-e585-443e-9b39-553b46ec92d1') self.assertEqual(images[0].name, 'image-1') self.assertEqual(images[1].id, '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810') From 8bf9e112447f97b57d744465cef99b823f544c03 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Sat, 14 Jul 2012 01:54:29 +0000 Subject: [PATCH 5/5] Align print_dict to the left Change-Id: Ib98e912a7af0bb570b4fd738733edd9b837d1a10 --- glanceclient/common/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glanceclient/common/utils.py b/glanceclient/common/utils.py index 07e77a1..957f713 100644 --- a/glanceclient/common/utils.py +++ b/glanceclient/common/utils.py @@ -57,7 +57,7 @@ def print_list(objs, fields, formatters={}): def print_dict(d): pt = prettytable.PrettyTable(['Property', 'Value'], caching=False) - pt.aligns = ['l', 'l'] + pt.align = 'l' [pt.add_row(list(r)) for r in d.iteritems()] print pt.get_string(sortby='Property')