From c7ccbd7a16a546cbd0717427772691ce7d8b4da6 Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:46 -0700 Subject: support volume and network in the direct api --- bin/nova-direct-api | 9 +++++++-- nova/api/ec2/cloud.py | 20 +++++++++++--------- nova/compute/api.py | 9 +++++---- nova/tests/test_direct.py | 14 ++++++++++++-- nova/volume/api.py | 3 ++- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/bin/nova-direct-api b/bin/nova-direct-api index a2c9f1557..1a78fb0c0 100755 --- a/bin/nova-direct-api +++ b/bin/nova-direct-api @@ -34,12 +34,14 @@ if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): gettext.install('nova', unicode=1) +from nova import compute from nova import flags from nova import log as logging +from nova import network from nova import utils +from nova import volume from nova import wsgi from nova.api import direct -from nova.compute import api as compute_api FLAGS = flags.FLAGS @@ -50,12 +52,15 @@ flags.DEFINE_flag(flags.HelpshortFlag()) flags.DEFINE_flag(flags.HelpXMLFlag()) + if __name__ == '__main__': utils.default_flagfile() FLAGS(sys.argv) logging.setup() - direct.register_service('compute', compute_api.API()) + direct.register_service('compute', compute.API()) + direct.register_service('volume', volume.API()) + direct.register_service('network', network.API()) direct.register_service('reflect', direct.Reflection()) router = direct.Router() with_json = direct.JsonParamsMiddleware(router) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 2afcea77c..5d31d71d3 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -541,7 +541,7 @@ class CloudController(object): volumes = [] for ec2_id in volume_id: internal_id = ec2utils.ec2_id_to_id(ec2_id) - volume = self.volume_api.get(context, internal_id) + volume = self.volume_api.get(context, volume_id=internal_id) volumes.append(volume) else: volumes = self.volume_api.get_all(context) @@ -585,9 +585,11 @@ class CloudController(object): def create_volume(self, context, size, **kwargs): LOG.audit(_("Create volume of %s GB"), size, context=context) - volume = self.volume_api.create(context, size, - kwargs.get('display_name'), - kwargs.get('display_description')) + volume = self.volume_api.create( + context, + size=size, + name=kwargs.get('display_name'), + description=kwargs.get('display_description')) # TODO(vish): Instance should be None at db layer instead of # trying to lazy load, but for now we turn it into # a dict to avoid an error. @@ -606,7 +608,7 @@ class CloudController(object): if field in kwargs: changes[field] = kwargs[field] if changes: - self.volume_api.update(context, volume_id, kwargs) + self.volume_api.update(context, volume_id=volume_id, fields=changes) return True def attach_volume(self, context, volume_id, instance_id, device, **kwargs): @@ -619,7 +621,7 @@ class CloudController(object): instance_id=instance_id, volume_id=volume_id, device=device) - volume = self.volume_api.get(context, volume_id) + volume = self.volume_api.get(context, volume_id=volume_id) return {'attachTime': volume['attach_time'], 'device': volume['mountpoint'], 'instanceId': ec2utils.id_to_ec2_id(instance_id), @@ -630,7 +632,7 @@ class CloudController(object): def detach_volume(self, context, volume_id, **kwargs): volume_id = ec2utils.ec2_id_to_id(volume_id) LOG.audit(_("Detach volume %s"), volume_id, context=context) - volume = self.volume_api.get(context, volume_id) + volume = self.volume_api.get(context, volume_id=volume_id) instance = self.compute_api.detach_volume(context, volume_id=volume_id) return {'attachTime': volume['attach_time'], 'device': volume['mountpoint'], @@ -768,7 +770,7 @@ class CloudController(object): def release_address(self, context, public_ip, **kwargs): LOG.audit(_("Release address %s"), public_ip, context=context) - self.network_api.release_floating_ip(context, public_ip) + self.network_api.release_floating_ip(context, address=public_ip) return {'releaseResponse': ["Address released."]} def associate_address(self, context, instance_id, public_ip, **kwargs): @@ -782,7 +784,7 @@ class CloudController(object): def disassociate_address(self, context, public_ip, **kwargs): LOG.audit(_("Disassociate address %s"), public_ip, context=context) - self.network_api.disassociate_floating_ip(context, public_ip) + self.network_api.disassociate_floating_ip(context, address=public_ip) return {'disassociateResponse': ["Address disassociated."]} def run_instances(self, context, **kwargs): diff --git a/nova/compute/api.py b/nova/compute/api.py index 309847156..f4aab97de 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -636,7 +636,7 @@ class API(base.Base): if not re.match("^/dev/[a-z]d[a-z]+$", device): raise exception.ApiError(_("Invalid device specified: %s. " "Example device: /dev/vdb") % device) - self.volume_api.check_attach(context, volume_id) + self.volume_api.check_attach(context, volume_id=volume_id) instance = self.get(context, instance_id) host = instance['host'] rpc.cast(context, @@ -650,7 +650,7 @@ class API(base.Base): instance = self.db.volume_get_instance(context.elevated(), volume_id) if not instance: raise exception.ApiError(_("Volume isn't attached to anything!")) - self.volume_api.check_detach(context, volume_id) + self.volume_api.check_detach(context, volume_id=volume_id) host = instance['host'] rpc.cast(context, self.db.queue_get_for(context, FLAGS.compute_topic, host), @@ -661,5 +661,6 @@ class API(base.Base): def associate_floating_ip(self, context, instance_id, address): instance = self.get(context, instance_id) - self.network_api.associate_floating_ip(context, address, - instance['fixed_ip']) + self.network_api.associate_floating_ip(context, + floating_ip=address, + fixed_ip=instance['fixed_ip']) diff --git a/nova/tests/test_direct.py b/nova/tests/test_direct.py index 80e4d2e1f..001246fc4 100644 --- a/nova/tests/test_direct.py +++ b/nova/tests/test_direct.py @@ -25,7 +25,9 @@ import webob from nova import compute from nova import context from nova import exception +from nova import network from nova import test +from nova import volume from nova import utils from nova.api import direct from nova.tests import test_cloud @@ -93,12 +95,20 @@ class DirectTestCase(test.TestCase): class DirectCloudTestCase(test_cloud.CloudTestCase): def setUp(self): super(DirectCloudTestCase, self).setUp() - compute_handle = compute.API(network_api=self.cloud.network_api, - volume_api=self.cloud.volume_api) + compute_handle = compute.API(image_service=self.cloud.image_service) + volume_handle = volume.API() + network_handle = network.API() direct.register_service('compute', compute_handle) + direct.register_service('volume', volume_handle) + direct.register_service('network', network_handle) + self.router = direct.JsonParamsMiddleware(direct.Router()) proxy = direct.Proxy(self.router) self.cloud.compute_api = proxy.compute + self.cloud.volume_api = proxy.volume + self.cloud.network_api = proxy.network + compute_handle.volume_api = proxy.volume + compute_handle.network_api = proxy.network def tearDown(self): super(DirectCloudTestCase, self).tearDown() diff --git a/nova/volume/api.py b/nova/volume/api.py index 2f4494845..4b4bb9dc5 100644 --- a/nova/volume/api.py +++ b/nova/volume/api.py @@ -82,7 +82,8 @@ class API(base.Base): self.db.volume_update(context, volume_id, fields) def get(self, context, volume_id): - return self.db.volume_get(context, volume_id) + rv = self.db.volume_get(context, volume_id) + return dict(rv.iteritems()) def get_all(self, context): if context.is_admin: -- cgit From ac44b8a9c5ed6a761793e1fa997768bd00a6c2da Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:46 -0700 Subject: improve the formatting of the stack tool --- bin/stack | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/bin/stack b/bin/stack index 25caca06f..d84a82e27 100755 --- a/bin/stack +++ b/bin/stack @@ -59,11 +59,21 @@ USAGE = """usage: stack [options] [arg1=value arg2=value] def format_help(d): """Format help text, keys are labels and values are descriptions.""" + MAX_INDENT = 30 indent = max([len(k) for k in d]) + if indent > MAX_INDENT: + indent = MAX_INDENT - 6 + out = [] for k, v in d.iteritems(): - t = textwrap.TextWrapper(initial_indent=' %s ' % k.ljust(indent), - subsequent_indent=' ' * (indent + 6)) + if (len(k) + 6) > MAX_INDENT: + out.extend([' %s' % k]) + initial_indent = ' ' * (indent + 6) + else: + initial_indent = ' %s ' % k.ljust(indent) + subsequent_indent = ' ' * (indent + 6) + t = textwrap.TextWrapper(initial_indent=initial_indent, + subsequent_indent=subsequent_indent) out.extend(t.wrap(v)) return out -- cgit From ef5c9e11595a00de468783adbb60cfbc2cbbf13d Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:46 -0700 Subject: add Limited, an API limiting/versioning wrapper --- bin/nova-direct-api | 7 +++++++ nova/api/direct.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/bin/nova-direct-api b/bin/nova-direct-api index 1a78fb0c0..ac0b5b51c 100755 --- a/bin/nova-direct-api +++ b/bin/nova-direct-api @@ -53,12 +53,19 @@ flags.DEFINE_flag(flags.HelpXMLFlag()) +class ReadOnlyCompute(direct.Limited): + """Read-only Compute API.""" + + _allowed = ['get', 'get_all', 'get_console_output'] + + if __name__ == '__main__': utils.default_flagfile() FLAGS(sys.argv) logging.setup() direct.register_service('compute', compute.API()) + direct.register_service('compute-readonly', ReadOnlyCompute(compute.API())) direct.register_service('volume', volume.API()) direct.register_service('network', network.API()) direct.register_service('reflect', direct.Reflection()) diff --git a/nova/api/direct.py b/nova/api/direct.py index dfca250e0..1011091a6 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -211,6 +211,42 @@ class ServiceWrapper(wsgi.Controller): return result +class Limited(object): + __notdoc = """Limit the available methods on a given object. + + (Not a docstring so that the docstring can be conditionally overriden.) + + Useful when defining a public API that only exposes a subset of an + internal API. + + Expected usage of this class is to define a subclass that lists the allowed + methods in the 'allowed' variable. + + Additionally where appropriate methods can be added or overwritten, for + example to provide backwards compatibility. + + """ + + _allowed = None + + def __init__(self, proxy): + self._proxy = proxy + if not self.__doc__: + self.__doc__ = proxy.__doc__ + if not self._allowed: + self._allowed = [] + + def __getattr__(self, key): + """Only return methods that are named in self._allowed.""" + if key not in self._allowed: + raise AttributeError() + return getattr(self._proxy, key) + + def __dir__(self): + """Only return methods that are named in self._allowed.""" + return [x for x in dir(self._proxy) if x in self._allowed] + + class Proxy(object): """Pretend a Direct API endpoint is an object.""" def __init__(self, app, prefix=None): -- cgit From 5c03ade2ee82350d845c8306d5aab9eda3073137 Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:47 -0700 Subject: add some more docs to direct.py --- nova/api/direct.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nova/api/direct.py b/nova/api/direct.py index 1011091a6..2e158e89e 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -225,6 +225,10 @@ class Limited(object): Additionally where appropriate methods can be added or overwritten, for example to provide backwards compatibility. + The wrapping approach has been chosen so that the wrapped API can maintain + its own internal consistency, for example if it calls "self.create" it + should get its own create method rather than anything we do here. + """ _allowed = None -- cgit From a7863c026819a9369cecaa42778a10ab54e798ba Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:47 -0700 Subject: add an example of a versioned api --- bin/nova-direct-api | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bin/nova-direct-api b/bin/nova-direct-api index ac0b5b51c..0f7589871 100755 --- a/bin/nova-direct-api +++ b/bin/nova-direct-api @@ -53,11 +53,19 @@ flags.DEFINE_flag(flags.HelpXMLFlag()) +# An example of an API that only exposes read-only methods. class ReadOnlyCompute(direct.Limited): """Read-only Compute API.""" _allowed = ['get', 'get_all', 'get_console_output'] +# An example of an API that provides a backwards compatibility layer. +class VolumeVersionOne(direct.Limited): + _allowed = ['create', 'delete', 'update', 'get'] + + def create(self, context, size, name): + self.proxy.create(context, size, name, description=None) + if __name__ == '__main__': utils.default_flagfile() @@ -65,10 +73,11 @@ if __name__ == '__main__': logging.setup() direct.register_service('compute', compute.API()) - direct.register_service('compute-readonly', ReadOnlyCompute(compute.API())) direct.register_service('volume', volume.API()) direct.register_service('network', network.API()) direct.register_service('reflect', direct.Reflection()) + direct.register_service('compute-readonly', ReadOnlyCompute(compute.API())) + direct.register_service('volume-v1', VolumeVersionOne(volume.API())) router = direct.Router() with_json = direct.JsonParamsMiddleware(router) with_req = direct.PostParamsMiddleware(with_json) -- cgit From a1bde64e91a8b76fd0e69c3bdfc51e4e85adf6f0 Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:47 -0700 Subject: add some more docs and make it more obvious which parts are examples --- bin/nova-direct-api | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/bin/nova-direct-api b/bin/nova-direct-api index 0f7589871..bb3aa8ae7 100755 --- a/bin/nova-direct-api +++ b/bin/nova-direct-api @@ -54,12 +54,19 @@ flags.DEFINE_flag(flags.HelpXMLFlag()) # An example of an API that only exposes read-only methods. +# In this case we're just limiting which methods are exposed. class ReadOnlyCompute(direct.Limited): """Read-only Compute API.""" _allowed = ['get', 'get_all', 'get_console_output'] + # An example of an API that provides a backwards compatibility layer. +# In this case we're overwriting the implementation to ensure +# compatibility with an older version. In reality we would want the +# "description=None" to be part of the actual API so that code +# like this isn't even necessary, but this example shows what one can +# do if that isn't the situation. class VolumeVersionOne(direct.Limited): _allowed = ['create', 'delete', 'update', 'get'] @@ -76,8 +83,12 @@ if __name__ == '__main__': direct.register_service('volume', volume.API()) direct.register_service('network', network.API()) direct.register_service('reflect', direct.Reflection()) - direct.register_service('compute-readonly', ReadOnlyCompute(compute.API())) - direct.register_service('volume-v1', VolumeVersionOne(volume.API())) + + # Here is how we could expose the code in the examples above. + #direct.register_service('compute-readonly', + # ReadOnlyCompute(compute.API())) + #direct.register_service('volume-v1', VolumeVersionOne(volume.API())) + router = direct.Router() with_json = direct.JsonParamsMiddleware(router) with_req = direct.PostParamsMiddleware(with_json) -- cgit From 4a6db815b01c71076bae96c155396e5adbe8af90 Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:47 -0700 Subject: better error handling and serialization --- nova/api/direct.py | 9 ++++++--- nova/tests/test_direct.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/nova/api/direct.py b/nova/api/direct.py index 2e158e89e..bb2ace1c9 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -38,6 +38,7 @@ import routes import webob from nova import context +from nova import exception from nova import flags from nova import utils from nova import wsgi @@ -205,10 +206,12 @@ class ServiceWrapper(wsgi.Controller): # NOTE(vish): make sure we have no unicode keys for py2.6. params = dict([(str(k), v) for (k, v) in params.iteritems()]) result = method(context, **params) - if type(result) is dict or type(result) is list: - return self._serialize(result, req.best_match_content_type()) - else: + if result is None or type(result) is str or type(result) is unicode: return result + try: + return self._serialize(result, req.best_match_content_type()) + except: + raise exception.Error("returned non-serializable type: %s" % result) class Limited(object): diff --git a/nova/tests/test_direct.py b/nova/tests/test_direct.py index 001246fc4..383840234 100644 --- a/nova/tests/test_direct.py +++ b/nova/tests/test_direct.py @@ -33,6 +33,9 @@ from nova.api import direct from nova.tests import test_cloud +class ArbitraryObject(object): + pass + class FakeService(object): def echo(self, context, data): return {'data': data} @@ -41,6 +44,9 @@ class FakeService(object): return {'user': context.user_id, 'project': context.project_id} + def invalid_return(self, context): + return ArbitraryObject() + class DirectTestCase(test.TestCase): def setUp(self): @@ -86,6 +92,12 @@ class DirectTestCase(test.TestCase): resp_parsed = json.loads(resp.body) self.assertEqual(resp_parsed['data'], 'foo') + def test_invalid(self): + req = webob.Request.blank('/fake/invalid_return') + req.environ['openstack.context'] = self.context + req.method = 'POST' + self.assertRaises(exception.Error, req.get_response, self.router) + def test_proxy(self): proxy = direct.Proxy(self.router) rv = proxy.fake.echo(self.context, data='baz') -- cgit From c5cbec20d2785d3060d57b55a264fbf936709500 Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 13:20:15 -0700 Subject: pep8 cleanups --- bin/nova-direct-api | 1 - nova/api/direct.py | 3 ++- nova/api/ec2/cloud.py | 4 +++- nova/tests/test_direct.py | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bin/nova-direct-api b/bin/nova-direct-api index bb3aa8ae7..83ec72722 100755 --- a/bin/nova-direct-api +++ b/bin/nova-direct-api @@ -52,7 +52,6 @@ flags.DEFINE_flag(flags.HelpshortFlag()) flags.DEFINE_flag(flags.HelpXMLFlag()) - # An example of an API that only exposes read-only methods. # In this case we're just limiting which methods are exposed. class ReadOnlyCompute(direct.Limited): diff --git a/nova/api/direct.py b/nova/api/direct.py index bb2ace1c9..e5f33cee4 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -211,7 +211,8 @@ class ServiceWrapper(wsgi.Controller): try: return self._serialize(result, req.best_match_content_type()) except: - raise exception.Error("returned non-serializable type: %s" % result) + raise exception.Error("returned non-serializable type: %s" + % result) class Limited(object): diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 5d31d71d3..0da642318 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -608,7 +608,9 @@ class CloudController(object): if field in kwargs: changes[field] = kwargs[field] if changes: - self.volume_api.update(context, volume_id=volume_id, fields=changes) + self.volume_api.update(context, + volume_id=volume_id, + fields=changes) return True def attach_volume(self, context, volume_id, instance_id, device, **kwargs): diff --git a/nova/tests/test_direct.py b/nova/tests/test_direct.py index 383840234..588a24b35 100644 --- a/nova/tests/test_direct.py +++ b/nova/tests/test_direct.py @@ -36,6 +36,7 @@ from nova.tests import test_cloud class ArbitraryObject(object): pass + class FakeService(object): def echo(self, context, data): return {'data': data} -- cgit