summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndy Smith <code@term.ie>2010-12-27 15:15:24 -0800
committerAndy Smith <code@term.ie>2010-12-27 15:15:24 -0800
commit8e1b74aa1c5a2f9113473eedc8e35b38b41445ea (patch)
tree44311f33afa023cfd354889cb2d7b8e4dcbd028d
parenta1b5220879632d093f450413f96668a8f77c0613 (diff)
downloadnova-8e1b74aa1c5a2f9113473eedc8e35b38b41445ea.tar.gz
nova-8e1b74aa1c5a2f9113473eedc8e35b38b41445ea.tar.xz
nova-8e1b74aa1c5a2f9113473eedc8e35b38b41445ea.zip
Added stack command-line tool
-rwxr-xr-xbin/nova-easy-api61
-rwxr-xr-xbin/stack145
-rw-r--r--nova/api/easy.py57
-rw-r--r--nova/compute/api.py6
-rw-r--r--nova/tests/easy_unittest.py6
-rw-r--r--nova/utils.py2
-rw-r--r--nova/wsgi.py3
7 files changed, 259 insertions, 21 deletions
diff --git a/bin/nova-easy-api b/bin/nova-easy-api
new file mode 100755
index 000000000..e8e86b4fb
--- /dev/null
+++ b/bin/nova-easy-api
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+# pylint: disable-msg=C0103
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+"""Starter script for Nova Easy API."""
+
+import gettext
+import os
+import sys
+
+# If ../nova/__init__.py exists, add ../ to Python search path, so that
+# it will override what happens to be installed in /usr/(local/)lib/python...
+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
+ os.pardir,
+ os.pardir))
+if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
+ sys.path.insert(0, possible_topdir)
+
+gettext.install('nova', unicode=1)
+
+from nova import flags
+from nova import utils
+from nova import wsgi
+from nova.api import easy
+from nova.compute import api as compute_api
+
+
+FLAGS = flags.FLAGS
+flags.DEFINE_integer('easy_port', 8001, 'Easy API port')
+flags.DEFINE_string('easy_host', '0.0.0.0', 'Easy API host')
+
+if __name__ == '__main__':
+ utils.default_flagfile()
+ FLAGS(sys.argv)
+
+ easy.register_service('compute', compute_api.ComputeAPI())
+ easy.register_service('reflect', easy.Reflection())
+ router = easy.SundayMorning()
+ with_json = easy.JsonParamsMiddleware(router)
+ with_req = easy.ReqParamsMiddleware(with_json)
+ with_auth = easy.DelegatedAuthMiddleware(with_req)
+
+ server = wsgi.Server()
+ server.start(with_auth, FLAGS.easy_port, host=FLAGS.easy_host)
+ server.wait()
diff --git a/bin/stack b/bin/stack
new file mode 100755
index 000000000..284dbf4fc
--- /dev/null
+++ b/bin/stack
@@ -0,0 +1,145 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+"""CLI for the Easy API."""
+
+import eventlet
+eventlet.monkey_patch()
+
+import os
+import pprint
+import sys
+import textwrap
+import urllib
+import urllib2
+
+# If ../nova/__init__.py exists, add ../ to Python search path, so that
+# it will override what happens to be installed in /usr/(local/)lib/python...
+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
+ os.pardir,
+ os.pardir))
+if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
+ sys.path.insert(0, possible_topdir)
+
+import gflags
+from nova import utils
+
+
+FLAGS = gflags.FLAGS
+gflags.DEFINE_string('host', '127.0.0.1', 'Easy API host')
+gflags.DEFINE_integer('port', 8001, 'Easy API host')
+gflags.DEFINE_string('user', 'user1', 'Easy API username')
+gflags.DEFINE_string('project', 'proj1', 'Easy API project')
+
+
+USAGE = """usage: stack [options] <controller> <method> [arg1=value arg2=value]
+
+ `stack help` should output the list of available controllers
+ `stack <controller>` should output the available methods for that controller
+ `stack help <controller>` should do the same
+ `stack help <controller> <method>` should output info for a method
+"""
+
+
+def format_help(d):
+ """Format help text, keys are labels and values are descriptions."""
+ indent = max([len(k) for k in d])
+ out = []
+ for k, v in d.iteritems():
+ t = textwrap.TextWrapper(initial_indent=' %s ' % k.ljust(indent),
+ subsequent_indent=' ' * (indent + 6))
+ out.extend(t.wrap(v))
+ return out
+
+
+def help_all():
+ rv = do_request('reflect', 'get_controllers')
+ out = format_help(rv)
+ return (USAGE + str(FLAGS.MainModuleHelp()) +
+ '\n\nAvailable controllers:\n' +
+ '\n'.join(out) + '\n')
+
+
+def help_controller(controller):
+ rv = do_request('reflect', 'get_methods')
+ methods = dict([(k.split('/')[2], v) for k, v in rv.iteritems()
+ if k.startswith('/%s' % controller)])
+ return ('Available methods for %s:\n' % controller +
+ '\n'.join(format_help(methods)))
+
+
+def help_method(controller, method):
+ rv = do_request('reflect',
+ 'get_method_info',
+ {'method': '/%s/%s' % (controller, method)})
+
+ sig = '%s(%s):' % (method, ', '.join(['='.join(x) for x in rv['args']]))
+ out = textwrap.wrap(sig, subsequent_indent=' ' * len('%s(' % method))
+ out.append('\n' + rv['doc'])
+ return '\n'.join(out)
+
+
+def do_request(controller, method, params=None):
+ if params:
+ data = urllib.urlencode(params)
+ else:
+ data = None
+
+ url = 'http://%s:%s/%s/%s' % (FLAGS.host, FLAGS.port, controller, method)
+ headers = {'X-OpenStack-User': FLAGS.user,
+ 'X-OpenStack-Project': FLAGS.project}
+
+ req = urllib2.Request(url, data, headers)
+ resp = urllib2.urlopen(req)
+ return utils.loads(resp.read())
+
+
+if __name__ == '__main__':
+ args = FLAGS(sys.argv)
+
+ cmd = args.pop(0)
+ if not args:
+ print help_all()
+ sys.exit()
+
+ first = args.pop(0)
+ if first == 'help':
+ action = help_all
+ params = []
+ if args:
+ params.append(args.pop(0))
+ action = help_controller
+ if args:
+ params.append(args.pop(0))
+ action = help_method
+ print action(*params)
+ sys.exit(0)
+
+ controller = first
+ if not args:
+ print help_controller(controller)
+ sys.exit()
+
+ method = args.pop(0)
+ params = {}
+ for x in args:
+ key, value = args.split('=', 1)
+ params[key] = value
+
+ pprint.pprint(do_request(controller, method, params))
diff --git a/nova/api/easy.py b/nova/api/easy.py
index 0e4f8a892..7468e3115 100644
--- a/nova/api/easy.py
+++ b/nova/api/easy.py
@@ -25,7 +25,7 @@ The general flow of a request is:
(/controller/method)
- Parameters are parsed from the request and passed to a method on the
controller as keyword arguments.
- - Optionally json_body is decoded to provide all the parameters.
+ - Optionally 'json' is decoded to provide all the parameters.
- Actual work is done and a result is returned.
- That result is turned into json and returned.
@@ -94,7 +94,7 @@ class SundayMorning(wsgi.Router):
def __init__(self, mapper=None):
if mapper is None:
mapper = routes.Mapper()
-
+
self._load_registered_routes(mapper)
super(SundayMorning, self).__init__(mapper=mapper)
@@ -103,14 +103,18 @@ class SundayMorning(wsgi.Router):
mapper.connect('/%s/{action}' % route,
controller=ServiceWrapper(EASY_ROUTES[route]))
-
+
class Reflection(object):
+ """Reflection methods to list available methods."""
def __init__(self):
self._methods = {}
+ self._controllers = {}
def _gather_methods(self):
methods = {}
+ controllers = {}
for route, handler in EASY_ROUTES.iteritems():
+ controllers[route] = handler.__doc__.split('\n')[0]
for k in dir(handler):
if k.startswith('_'):
continue
@@ -120,40 +124,63 @@ class Reflection(object):
# bunch of ugly formatting stuff
argspec = inspect.getargspec(f)
- args = [x for x in argspec[0] if x != 'self' and x != 'context']
+ args = [x for x in argspec[0]
+ if x != 'self' and x != 'context']
defaults = argspec[3] and argspec[3] or []
args_r = list(reversed(args))
defaults_r = list(reversed(defaults))
+
args_out = []
while args_r:
if defaults_r:
- args_out.append((args_r.pop(0), defaults_r.pop(0)))
+ args_out.append((args_r.pop(0),
+ repr(defaults_r.pop(0))))
else:
- args_out.append(str(args_r.pop(0)))
+ args_out.append((str(args_r.pop(0)),))
+
+ # if the method accepts keywords
+ if argspec[2]:
+ args_out.insert(0, ('**%s' % argspec[2],))
methods['/%s/%s' % (route, k)] = {
+ 'short_doc': f.__doc__.split('\n')[0],
+ 'doc': f.__doc__,
'name': k,
'args': list(reversed(args_out))}
- return methods
+
+ self._methods = methods
+ self._controllers = controllers
+
+ def get_controllers(self, context):
+ """List available controllers."""
+ if not self._controllers:
+ self._gather_methods()
+
+ return self._controllers
def get_methods(self, context):
+ """List available methods."""
if not self._methods:
- self._methods = self._gather_methods()
+ self._gather_methods()
method_list = self._methods.keys()
method_list.sort()
- return {'methods': method_list}
+ methods = {}
+ for k in method_list:
+ methods[k] = self._methods[k]['short_doc']
+ return methods
def get_method_info(self, context, method):
+ """Get detailed information about a method."""
if not self._methods:
- self._methods = self._gather_methods()
+ self._gather_methods()
return self._methods[method]
class ServiceWrapper(wsgi.Controller):
def __init__(self, service_handle):
self.service_handle = service_handle
-
+
@webob.dec.wsgify
def __call__(self, req):
arg_dict = req.environ['wsgiorg.routing_args'][1]
@@ -165,10 +192,10 @@ class ServiceWrapper(wsgi.Controller):
params = {}
if 'openstack.params' in req.environ:
params = req.environ['openstack.params']
-
+
# TODO(termie): do some basic normalization on methods
method = getattr(self.service_handle, action)
-
+
result = method(context, **params)
if type(result) is dict or type(result) is list:
return self._serialize(result, req)
@@ -181,7 +208,7 @@ class Proxy(object):
def __init__(self, app, prefix=None):
self.app = app
self.prefix = prefix
-
+
def __do_request(self, path, context, **kwargs):
req = webob.Request.blank(path)
req.method = 'POST'
@@ -196,7 +223,7 @@ class Proxy(object):
def __getattr__(self, key):
if self.prefix is None:
return self.__class__(self.app, prefix=key)
-
+
def _wrapper(context, **kwargs):
return self.__do_request('/%s/%s' % (self.prefix, key),
context,
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 5f18539a3..005ed7a68 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -40,6 +40,7 @@ def id_to_default_hostname(internal_id):
"""Default function to generate a hostname given an instance reference."""
return str(internal_id)
+
def id_to_ec2_hostname(internal_id):
digits = []
while internal_id != 0:
@@ -47,9 +48,11 @@ def id_to_ec2_hostname(internal_id):
digits.append('0123456789abcdefghijklmnopqrstuvwxyz'[remainder])
return "i-%s" % ''.join(reversed(digits))
+
HOSTNAME_FORMATTERS = {'default': id_to_default_hostname,
'ec2': id_to_ec2_hostname}
+
class ComputeAPI(base.Base):
"""API for interacting with the compute manager."""
@@ -63,6 +66,7 @@ class ComputeAPI(base.Base):
super(ComputeAPI, self).__init__(**kwargs)
def get_network_topic(self, context, instance_id):
+ """Get the network topic for an instance."""
try:
instance = self.db.instance_get_by_internal_id(context,
instance_id)
@@ -221,6 +225,7 @@ class ComputeAPI(base.Base):
return self.db.instance_update(context, instance_id, kwargs)
def delete_instance(self, context, instance_id):
+ """Terminate and remove an instance."""
logging.debug("Going to try and terminate %d" % instance_id)
try:
instance = self.db.instance_get_by_internal_id(context,
@@ -264,6 +269,7 @@ class ComputeAPI(base.Base):
return self.db.instance_get_all(context)
def get_instance(self, context, instance_id):
+ """Get information about a specific instance."""
rv = self.db.instance_get_by_internal_id(context, instance_id)
return dict(rv.iteritems())
diff --git a/nova/tests/easy_unittest.py b/nova/tests/easy_unittest.py
index 81990d842..cd13c7710 100644
--- a/nova/tests/easy_unittest.py
+++ b/nova/tests/easy_unittest.py
@@ -31,6 +31,7 @@ from nova.api import easy
from nova.compute import api as compute_api
from nova.tests import cloud_unittest
+
class FakeService(object):
def echo(self, context, data):
return {'data': data}
@@ -49,7 +50,7 @@ class EasyTestCase(test.TestCase):
easy.SundayMorning()))
self.auth_router = easy.DelegatedAuthMiddleware(self.router)
self.context = context.RequestContext('user1', 'proj1')
-
+
def tearDown(self):
easy.EASY_ROUTES = {}
@@ -61,7 +62,7 @@ class EasyTestCase(test.TestCase):
data = json.loads(resp.body)
self.assertEqual(data['user'], 'user1')
self.assertEqual(data['project'], 'proj1')
-
+
def test_json_params(self):
req = webob.Request.blank('/fake/echo')
req.environ['openstack.context'] = self.context
@@ -99,4 +100,3 @@ class EasyCloudTestCase(cloud_unittest.CloudTestCase):
def tearDown(self):
super(EasyCloudTestCase, self).tearDown()
easy.EASY_ROUTES = {}
-
diff --git a/nova/utils.py b/nova/utils.py
index 7a98ffa5a..337924f10 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -386,7 +386,7 @@ def dumps(value):
return json.dumps(value)
except TypeError:
pass
-
+
return json.dumps(to_primitive(value))
diff --git a/nova/wsgi.py b/nova/wsgi.py
index c40f043f9..564805ae7 100644
--- a/nova/wsgi.py
+++ b/nova/wsgi.py
@@ -105,8 +105,7 @@ class Application(object):
class Middleware(Application):
"""Base WSGI middleware.
-
- Modelled after Django's middleware this class allows you to
+
These classes require an application to be
initialized that will be called next. By default the middleware will
simply call its wrapped app, or you can override __call__ to customize its