From f5f696b91a339710394bc3289a6c828903f2e1b2 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Fri, 20 May 2011 09:05:39 +0100 Subject: [PATCH 01/15] Adding flags.py to infrastructure code --- quantum/common/flags.py | 247 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 quantum/common/flags.py diff --git a/quantum/common/flags.py b/quantum/common/flags.py new file mode 100644 index 000000000..51cbe58be --- /dev/null +++ b/quantum/common/flags.py @@ -0,0 +1,247 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems, Inc. +# 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. + +"""Command-line flag library. + +Wraps gflags. +Global flags should be defined here, the rest are defined where they're used. + +""" + +import getopt +import string +import sys + +import gflags + + +class FlagValues(gflags.FlagValues): + """Extension of gflags.FlagValues that allows undefined and runtime flags. + + Unknown flags will be ignored when parsing the command line, but the + command line will be kept so that it can be replayed if new flags are + defined after the initial parsing. + + """ + + def __init__(self, extra_context=None): + gflags.FlagValues.__init__(self) + self.__dict__['__dirty'] = [] + self.__dict__['__was_already_parsed'] = False + self.__dict__['__stored_argv'] = [] + self.__dict__['__extra_context'] = extra_context + + def __call__(self, argv): + # We're doing some hacky stuff here so that we don't have to copy + # out all the code of the original verbatim and then tweak a few lines. + # We're hijacking the output of getopt so we can still return the + # leftover args at the end + sneaky_unparsed_args = {"value": None} + original_argv = list(argv) + + if self.IsGnuGetOpt(): + orig_getopt = getattr(getopt, 'gnu_getopt') + orig_name = 'gnu_getopt' + else: + orig_getopt = getattr(getopt, 'getopt') + orig_name = 'getopt' + + def _sneaky(*args, **kw): + optlist, unparsed_args = orig_getopt(*args, **kw) + sneaky_unparsed_args['value'] = unparsed_args + return optlist, unparsed_args + + try: + setattr(getopt, orig_name, _sneaky) + args = gflags.FlagValues.__call__(self, argv) + except gflags.UnrecognizedFlagError: + # Undefined args were found, for now we don't care so just + # act like everything went well + # (these three lines are copied pretty much verbatim from the end + # of the __call__ function we are wrapping) + unparsed_args = sneaky_unparsed_args['value'] + if unparsed_args: + if self.IsGnuGetOpt(): + args = argv[:1] + unparsed_args + else: + args = argv[:1] + original_argv[-len(unparsed_args):] + else: + args = argv[:1] + finally: + setattr(getopt, orig_name, orig_getopt) + + # Store the arguments for later, we'll need them for new flags + # added at runtime + self.__dict__['__stored_argv'] = original_argv + self.__dict__['__was_already_parsed'] = True + self.ClearDirty() + return args + + def Reset(self): + gflags.FlagValues.Reset(self) + self.__dict__['__dirty'] = [] + self.__dict__['__was_already_parsed'] = False + self.__dict__['__stored_argv'] = [] + + def SetDirty(self, name): + """Mark a flag as dirty so that accessing it will case a reparse.""" + self.__dict__['__dirty'].append(name) + + def IsDirty(self, name): + return name in self.__dict__['__dirty'] + + def ClearDirty(self): + self.__dict__['__is_dirty'] = [] + + def WasAlreadyParsed(self): + return self.__dict__['__was_already_parsed'] + + def ParseNewFlags(self): + if '__stored_argv' not in self.__dict__: + return + new_flags = FlagValues(self) + for k in self.__dict__['__dirty']: + new_flags[k] = gflags.FlagValues.__getitem__(self, k) + + new_flags(self.__dict__['__stored_argv']) + for k in self.__dict__['__dirty']: + setattr(self, k, getattr(new_flags, k)) + self.ClearDirty() + + def __setitem__(self, name, flag): + gflags.FlagValues.__setitem__(self, name, flag) + if self.WasAlreadyParsed(): + self.SetDirty(name) + + def __getitem__(self, name): + if self.IsDirty(name): + self.ParseNewFlags() + return gflags.FlagValues.__getitem__(self, name) + + def __getattr__(self, name): + if self.IsDirty(name): + self.ParseNewFlags() + val = gflags.FlagValues.__getattr__(self, name) + if type(val) is str: + tmpl = string.Template(val) + context = [self, self.__dict__['__extra_context']] + return tmpl.substitute(StrWrapper(context)) + return val + + +class StrWrapper(object): + """Wrapper around FlagValues objects. + + Wraps FlagValues objects for string.Template so that we're + sure to return strings. + + """ + def __init__(self, context_objs): + self.context_objs = context_objs + + def __getitem__(self, name): + for context in self.context_objs: + val = getattr(context, name, False) + if val: + return str(val) + raise KeyError(name) + + +# Copied from gflags with small mods to get the naming correct. +# Originally gflags checks for the first module that is not gflags that is +# in the call chain, we want to check for the first module that is not gflags +# and not this module. +def _GetCallingModule(): + """Returns the name of the module that's calling into this module. + + We generally use this function to get the name of the module calling a + DEFINE_foo... function. + + """ + # Walk down the stack to find the first globals dict that's not ours. + for depth in range(1, sys.getrecursionlimit()): + if not sys._getframe(depth).f_globals is globals(): + module_name = __GetModuleName(sys._getframe(depth).f_globals) + if module_name == 'gflags': + continue + if module_name is not None: + return module_name + raise AssertionError("No module was found") + + +# Copied from gflags because it is a private function +def __GetModuleName(globals_dict): + """Given a globals dict, returns the name of the module that defines it. + + Args: + globals_dict: A dictionary that should correspond to an environment + providing the values of the globals. + + Returns: + A string (the name of the module) or None (if the module could not + be identified. + + """ + for name, module in sys.modules.iteritems(): + if getattr(module, '__dict__', None) is globals_dict: + if name == '__main__': + return sys.argv[0] + return name + return None + + +def _wrapper(func): + def _wrapped(*args, **kw): + kw.setdefault('flag_values', FLAGS) + func(*args, **kw) + _wrapped.func_name = func.func_name + return _wrapped + + +FLAGS = FlagValues() +gflags.FLAGS = FLAGS +gflags._GetCallingModule = _GetCallingModule + + +DEFINE = _wrapper(gflags.DEFINE) +DEFINE_string = _wrapper(gflags.DEFINE_string) +DEFINE_integer = _wrapper(gflags.DEFINE_integer) +DEFINE_bool = _wrapper(gflags.DEFINE_bool) +DEFINE_boolean = _wrapper(gflags.DEFINE_boolean) +DEFINE_float = _wrapper(gflags.DEFINE_float) +DEFINE_enum = _wrapper(gflags.DEFINE_enum) +DEFINE_list = _wrapper(gflags.DEFINE_list) +DEFINE_spaceseplist = _wrapper(gflags.DEFINE_spaceseplist) +DEFINE_multistring = _wrapper(gflags.DEFINE_multistring) +DEFINE_multi_int = _wrapper(gflags.DEFINE_multi_int) +DEFINE_flag = _wrapper(gflags.DEFINE_flag) +HelpFlag = gflags.HelpFlag +HelpshortFlag = gflags.HelpshortFlag +HelpXMLFlag = gflags.HelpXMLFlag + + +def DECLARE(name, module_string, flag_values=FLAGS): + if module_string not in sys.modules: + __import__(module_string, globals(), locals()) + if name not in flag_values: + raise gflags.UnrecognizedFlag( + "%s not defined by %s" % (name, module_string)) + + +# __GLOBAL FLAGS ONLY__ +# Define any app-specific flags in their own files, docs at: +# http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#a9 From bf3a3b7f685459b7ab40b5b862e7aa8a25239158 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Fri, 20 May 2011 14:51:28 +0100 Subject: [PATCH 02/15] Branching from quantum-framework --- .pydevproject | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .pydevproject diff --git a/.pydevproject b/.pydevproject deleted file mode 100644 index a9cca037b..000000000 --- a/.pydevproject +++ /dev/null @@ -1,7 +0,0 @@ - - - - -Default -python 2.7 - From 3e809e45f2d7ddbbe2e9710bf5d6f4c80801d834 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Fri, 20 May 2011 14:53:02 +0100 Subject: [PATCH 03/15] Removing .pydevproject from version control --- .pydevproject | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .pydevproject diff --git a/.pydevproject b/.pydevproject deleted file mode 100644 index a9cca037b..000000000 --- a/.pydevproject +++ /dev/null @@ -1,7 +0,0 @@ - - - - -Default -python 2.7 - From 847ecc50286363d5ce3fbcce9e5454bd1af98b56 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Fri, 20 May 2011 15:30:12 +0100 Subject: [PATCH 04/15] Adding api paste configuration file --- etc/quantum/api-paste.ini | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 etc/quantum/api-paste.ini diff --git a/etc/quantum/api-paste.ini b/etc/quantum/api-paste.ini new file mode 100644 index 000000000..48f79849c --- /dev/null +++ b/etc/quantum/api-paste.ini @@ -0,0 +1,29 @@ +############# +# Quantum # +############# + +[composite:quantumapi] +use = egg:Paste#urlmap +/: quantumversions +/v1.0: quantumapi10 + +[pipeline:quantumapi10] +pipeline = faultwrap auth ratelimit quantumapiapp10 + +[filter:faultwrap] +paste.filter_factory = quantum.api:FaultWrapper.factory + +[filter:auth] +paste.filter_factory = quantum.api.auth:AuthMiddleware.factory + +[filter:ratelimit] +paste.filter_factory = quantum.api.limits:RateLimitingMiddleware.factory + +[app:quantumapiapp10] +paste.app_factory = nova.api.quantum:APIRouterV10.factory + +[pipeline:quantumversions] +pipeline = faultwrap quantumversionapp + +[app:quantumversionapp] +paste.app_factory = quantum.api.versions:Versions.factory From b6f2ca5aa0e774826dc5855f98fb446ee78a2430 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Fri, 20 May 2011 16:22:48 -0700 Subject: [PATCH 05/15] Minor fixes: indentation in bin/quantum and fix import in config.py --- bin/quantum | 13 ++++++------- quantum/common/config.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/bin/quantum b/bin/quantum index 16ef7346d..d19fa91ef 100644 --- a/bin/quantum +++ b/bin/quantum @@ -37,9 +37,9 @@ from quantum.common import wsgi from quantum.common import config def create_options(parser): - """ + """ Sets up the CLI and config-file options that may be - parsed and program commands. + parsed and program commands. :param parser: The option parser """ config.add_common_options(parser) @@ -53,10 +53,9 @@ if __name__ == '__main__': try: conf, app = config.load_paste_app('quantum', options, args) - - server = wsgi.Server() - server.start(app, int(conf['bind_port']), conf['bind_host']) - server.wait() + server = wsgi.Server() + server.start(app, int(conf['bind_port']), conf['bind_host']) + server.wait() except RuntimeError, e: - sys.exit("ERROR: %s" % e) + sys.exit("ERROR: %s" % e) diff --git a/quantum/common/config.py b/quantum/common/config.py index dbbcd260f..21c1b5951 100644 --- a/quantum/common/config.py +++ b/quantum/common/config.py @@ -31,7 +31,7 @@ import sys from paste import deploy -import quantum.common.exception as exception +import quantum.common.exceptions as exception DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" From 3a421e759ff8ead517770470e4edc7226c4dadb3 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Tue, 24 May 2011 17:45:16 +0100 Subject: [PATCH 06/15] Work in progress on network API --- bin/quantum | 17 +++- etc/quantum.conf | 12 ++- quantum/api/__init__.py | 67 ++++++++++++- quantum/api/api_common.py | 21 ++++ quantum/api/faults.py | 62 ++++++++++++ quantum/api/networks.py | 200 ++++++++++++++++++++++++++++++++++++++ quantum/common/config.py | 34 ++----- quantum/common/utils.py | 2 +- quantum/common/wsgi.py | 10 +- quantum/service.py | 108 ++++++++++++++++---- 10 files changed, 478 insertions(+), 55 deletions(-) create mode 100644 quantum/api/api_common.py create mode 100644 quantum/api/faults.py create mode 100644 quantum/api/networks.py diff --git a/bin/quantum b/bin/quantum index 780ccc61a..9971e795a 100755 --- a/bin/quantum +++ b/bin/quantum @@ -35,6 +35,7 @@ if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')): gettext.install('quantum', unicode=1) +from quantum import service from quantum.common import wsgi from quantum.common import config @@ -54,10 +55,18 @@ if __name__ == '__main__': (options, args) = config.parse_options(oparser) try: - conf, app = config.load_paste_app('quantumversionapp', options, args) - server = wsgi.Server() - server.start(app, int(conf['bind_port']), conf['bind_host']) - server.wait() + print "HERE-1" + service = service.serve_wsgi(service.QuantumApiService, + options=options, + args=args) + #version_conf, version_app = config.load_paste_app('quantumversion', options, args) + print "HERE-2" + service.wait() + #api_conf, api_app = config.load_paste_app('quantum', options, args) + #server = wsgi.Server() + #server.start(version_app, int(version_conf['bind_port']), version_conf['bind_host']) + #server.start(api_app, int(api_conf['bind_port']), api_conf['bind_host']) + #server.wait() except RuntimeError, e: sys.exit("ERROR: %s" % e) diff --git a/etc/quantum.conf b/etc/quantum.conf index 91904603d..ba96a9a27 100644 --- a/etc/quantum.conf +++ b/etc/quantum.conf @@ -11,9 +11,15 @@ bind_host = 0.0.0.0 # Port the bind the API server to bind_port = 9696 -#[app:quantum] -#paste.app_factory = quantum.service:app_factory +[composite:quantum] +use = egg:Paste#urlmap +/: quantumversions +/v0.1: quantumapi -[app:quantumversionapp] +[app:quantumversions] paste.app_factory = quantum.api.versions:Versions.factory +[app:quantumapi] +paste.app_factory = quantum.api:APIRouterV01.factory + + diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py index 9602374ca..9e7d55497 100644 --- a/quantum/api/__init__.py +++ b/quantum/api/__init__.py @@ -13,4 +13,69 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# @author: Somik Behera, Nicira Networks, Inc. \ No newline at end of file +# @author: Salvatore Orlando, Citrix Systems + +""" +Quantum API controllers. +""" + +import logging +import routes +import webob.dec +import webob.exc + +from quantum.api import faults +from quantum.api import networks +from quantum.common import flags +from quantum.common import wsgi + + +LOG = logging.getLogger('quantum.api') +FLAGS = flags.FLAGS + +class FaultWrapper(wsgi.Middleware): + """Calls down the middleware stack, making exceptions into faults.""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + try: + return req.get_response(self.application) + except Exception as ex: + LOG.exception(_("Caught error: %s"), unicode(ex)) + exc = webob.exc.HTTPInternalServerError(explanation=unicode(ex)) + return faults.Fault(exc) + + +class APIRouterV01(wsgi.Router): + """ + Routes requests on the Quantum API to the appropriate controller + """ + + def __init__(self, ext_mgr=None): + mapper = routes.Mapper() + self._setup_routes(mapper) + super(APIRouterV01, self).__init__(mapper) + + def _setup_routes(self, mapper): + #server_members = self.server_members + #server_members['action'] = 'POST' + + #server_members['pause'] = 'POST' + #server_members['unpause'] = 'POST' + #server_members['diagnostics'] = 'GET' + #server_members['actions'] = 'GET' + #server_members['suspend'] = 'POST' + #server_members['resume'] = 'POST' + #server_members['rescue'] = 'POST' + #server_members['unrescue'] = 'POST' + #server_members['reset_network'] = 'POST' + #server_members['inject_network_info'] = 'POST' + + mapper.resource("network", "networks", controller=networks.Controller(), + collection={'detail': 'GET'}) + print mapper + #mapper.resource("port", "ports", controller=ports.Controller(), + # collection=dict(public='GET', private='GET'), + # parent_resource=dict(member_name='network', + # collection_name='networks')) + diff --git a/quantum/api/api_common.py b/quantum/api/api_common.py new file mode 100644 index 000000000..b33987b4d --- /dev/null +++ b/quantum/api/api_common.py @@ -0,0 +1,21 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix System. +# 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. + + +XML_NS_V01 = 'http://netstack.org/quantum/api/v0.1' +XML_NS_V10 = 'http://netstack.org/quantum/api/v1.0' + diff --git a/quantum/api/faults.py b/quantum/api/faults.py new file mode 100644 index 000000000..d61ae79fa --- /dev/null +++ b/quantum/api/faults.py @@ -0,0 +1,62 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems. +# 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 webob.dec +import webob.exc + +from quantum.api import api_common as common +from quantum.common import wsgi + +class Fault(webob.exc.HTTPException): + """Error codes for API faults""" + + _fault_names = { + 400: "malformedRequest", + 401: "unauthorized", + 402: "networkNotFound", + 403: "requestedStateInvalid", + 460: "networkInUse", + 461: "alreadyAttached", + 462: "portInUse", + 470: "serviceUnavailable", + 471: "pluginFault" + } + + def __init__(self, exception): + """Create a Fault for the given webob.exc.exception.""" + self.wrapped_exc = exception + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + """Generate a WSGI response based on the exception passed to ctor.""" + # Replace the body with fault details. + code = self.wrapped_exc.status_int + fault_name = self._fault_names.get(code, "quantumServiceFault") + fault_data = { + fault_name: { + 'code': code, + 'message': self.wrapped_exc.explanation}} + #TODO (salvatore-orlando): place over-limit stuff here + # 'code' is an attribute on the fault tag itself + metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} + default_xmlns = common.XML_NS_V10 + serializer = wsgi.Serializer(metadata, default_xmlns) + content_type = req.best_match_content_type() + self.wrapped_exc.body = serializer.serialize(fault_data, content_type) + self.wrapped_exc.content_type = content_type + return self.wrapped_exc diff --git a/quantum/api/networks.py b/quantum/api/networks.py new file mode 100644 index 000000000..f98aa43e5 --- /dev/null +++ b/quantum/api/networks.py @@ -0,0 +1,200 @@ +# Copyright 2011 Citrix Systems. +# 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 base64 +import logging +import traceback + +from webob import exc +from xml.dom import minidom + +from quantum import manager +from quantum import quantum_plugin_base +from quantum.common import exceptions as exception +from quantum.common import flags +from quantum.common import wsgi +from quantum import utils +from quantum.api import api_common as common +from quantum.api import faults +import quantum.api + +LOG = logging.getLogger('quantum.api.networks') +FLAGS = flags.FLAGS + + +class Controller(wsgi.Controller): + """ Network API controller for Quantum API """ + + #TODO (salvatore-orlando): adjust metadata for quantum + _serialization_metadata = { + "application/xml": { + "attributes": { + "server": ["id", "imageId", "name", "flavorId", "hostId", + "status", "progress", "adminPass", "flavorRef", + "imageRef"], + "link": ["rel", "type", "href"], + }, + "dict_collections": { + "metadata": {"item_name": "meta", "item_key": "key"}, + }, + "list_collections": { + "public": {"item_name": "ip", "item_key": "addr"}, + "private": {"item_name": "ip", "item_key": "addr"}, + }, + }, + } + + def index(self, request): + """ Returns a list of network names and ids """ + #TODO: this should be for a given tenant!!! + print "PIPPO" + LOG.debug("HERE - index") + return self._items(request, is_detail=False) + + def _items(self, req, is_detail): + """ Returns a list of networks. """ + #TODO: we should return networks for a given tenant only + #TODO: network controller should be retrieved here!!! + test = { 'ciao':'bello','porco':'mondo' } + #builder = self._get_view_builder(req) + #servers = [builder.build(inst, is_detail)['server'] + # for inst in limited_list] + #return dict(servers=servers) + return test + + def show(self, req, id): + """ Returns network details by network id """ + try: + return "TEST NETWORK DETAILS" + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + def delete(self, req, id): + """ Destroys the network with the given id """ + try: + return "TEST NETWORK DELETE" + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + def create(self, req): + """ Creates a new network for a given tenant """ + #env = self._deserialize_create(req) + #if not env: + # return faults.Fault(exc.HTTPUnprocessableEntity()) + return "TEST NETWORK CREATE" + + def _deserialize_create(self, request): + """ + Deserialize a create request + Overrides normal behavior in the case of xml content + """ + #if request.content_type == "application/xml": + # deserializer = ServerCreateRequestXMLDeserializer() + # return deserializer.deserialize(request.body) + #else: + # return self._deserialize(request.body, request.get_content_type()) + pass + + def update(self, req, id): + """ Updates the name for the network wit the given id """ + if len(req.body) == 0: + raise exc.HTTPUnprocessableEntity() + + inst_dict = self._deserialize(req.body, req.get_content_type()) + if not inst_dict: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + try: + return "TEST NETWORK UPDATE" + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPNoContent() + + +class NetworkCreateRequestXMLDeserializer(object): + """ + Deserializer to handle xml-formatted server create requests. + + Handles standard server attributes as well as optional metadata + and personality attributes + """ + + def deserialize(self, string): + """Deserialize an xml-formatted server create request""" + dom = minidom.parseString(string) + server = self._extract_server(dom) + return {'server': server} + + def _extract_server(self, node): + """Marshal the server attribute of a parsed request""" + server = {} + server_node = self._find_first_child_named(node, 'server') + for attr in ["name", "imageId", "flavorId"]: + server[attr] = server_node.getAttribute(attr) + metadata = self._extract_metadata(server_node) + if metadata is not None: + server["metadata"] = metadata + personality = self._extract_personality(server_node) + if personality is not None: + server["personality"] = personality + return server + + def _extract_metadata(self, server_node): + """Marshal the metadata attribute of a parsed request""" + metadata_node = self._find_first_child_named(server_node, "metadata") + if metadata_node is None: + return None + metadata = {} + for meta_node in self._find_children_named(metadata_node, "meta"): + key = meta_node.getAttribute("key") + metadata[key] = self._extract_text(meta_node) + return metadata + + def _extract_personality(self, server_node): + """Marshal the personality attribute of a parsed request""" + personality_node = \ + self._find_first_child_named(server_node, "personality") + if personality_node is None: + return None + personality = [] + for file_node in self._find_children_named(personality_node, "file"): + item = {} + if file_node.hasAttribute("path"): + item["path"] = file_node.getAttribute("path") + item["contents"] = self._extract_text(file_node) + personality.append(item) + return personality + + def _find_first_child_named(self, parent, name): + """Search a nodes children for the first child with a given name""" + for node in parent.childNodes: + if node.nodeName == name: + return node + return None + + def _find_children_named(self, parent, name): + """Return all of a nodes children who have the given name""" + for node in parent.childNodes: + if node.nodeName == name: + yield node + + def _extract_text(self, node): + """Get the text field contained by the given node""" + if len(node.childNodes) == 1: + child = node.childNodes[0] + if child.nodeType == child.TEXT_NODE: + return child.nodeValue + return "" diff --git a/quantum/common/config.py b/quantum/common/config.py index 2d858ed35..cda765078 100644 --- a/quantum/common/config.py +++ b/quantum/common/config.py @@ -244,10 +244,12 @@ def load_paste_config(app_name, options, args): problem loading the configuration file. """ conf_file = find_config_file(options, args) + print "Conf_file:%s" %conf_file if not conf_file: raise RuntimeError("Unable to locate any configuration file. " "Cannot load application %s" % app_name) try: + print "App_name:%s" %app_name conf = deploy.appconfig("config:%s" % conf_file, name=app_name) return conf_file, conf except Exception, e: @@ -255,7 +257,7 @@ def load_paste_config(app_name, options, args): % (conf_file, e)) -def load_paste_app(app_name, options, args): +def load_paste_app(conf_file, app_name): """ Builds and returns a WSGI app from a paste config file. @@ -276,40 +278,16 @@ def load_paste_app(app_name, options, args): :raises RuntimeError when config file cannot be located or application cannot be loaded from config file """ - conf_file, conf = load_paste_config(app_name, options, args) + #conf_file, conf = load_paste_config(app_name, options, args) try: - # Setup logging early, supplying both the CLI options and the - # configuration mapping from the config file - print "OPTIONS:%s" %options - print "CONF:%s" %conf - setup_logging(options, conf) - - # We only update the conf dict for the verbose and debug - # flags. Everything else must be set up in the conf file... - debug = options.get('debug') or \ - get_option(conf, 'debug', type='bool', default=False) - verbose = options.get('verbose') or \ - get_option(conf, 'verbose', type='bool', default=False) - conf['debug'] = debug - conf['verbose'] = verbose - - # Log the options used when starting if we're in debug mode... - LOG.debug("*" * 80) - LOG.debug("Configuration options gathered from config file:") - LOG.debug(conf_file) - LOG.debug("================================================") - items = dict([(k, v) for k, v in conf.items() - if k not in ('__file__', 'here')]) - for key, value in sorted(items.items()): - LOG.debug("%(key)-30s %(value)s" % locals()) - LOG.debug("*" * 80) + conf_file = os.path.abspath(conf_file) app = deploy.loadapp("config:%s" % conf_file, name=app_name) except (LookupError, ImportError), e: raise RuntimeError("Unable to load %(app_name)s from " "configuration file %(conf_file)s." "\nGot: %(e)r" % locals()) - return conf, app + return app def get_option(options, option, **kwargs): diff --git a/quantum/common/utils.py b/quantum/common/utils.py index 435ec7b87..c56a53ac1 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -29,7 +29,7 @@ import socket import sys import ConfigParser -from common import exceptions +from quantum.common import exceptions from exceptions import ProcessExecutionError diff --git a/quantum/common/wsgi.py b/quantum/common/wsgi.py index 73b826ef9..9a2b5bb58 100644 --- a/quantum/common/wsgi.py +++ b/quantum/common/wsgi.py @@ -1,3 +1,4 @@ + # vim: tabstop=4 shiftwidth=4 softtabstop=4 # # Copyright 2011, Nicira Networks, Inc. @@ -253,6 +254,13 @@ class Router(object): WSGI middleware that maps incoming requests to WSGI apps. """ + @classmethod + def factory(cls, global_config, **local_config): + """ + Returns an instance of the WSGI Router class + """ + return cls() + def __init__(self, mapper): """ Create a router for the given routes.Mapper. @@ -337,7 +345,7 @@ class Controller(object): MIME types to information needed to serialize to that type. """ _metadata = getattr(type(self), "_serialization_metadata", {}) - serializer = Serializer(request.environ, _metadata) + serializer = Serializer(_metadata) return serializer.to_content_type(data) diff --git a/quantum/service.py b/quantum/service.py index 50a8effa3..760263bf9 100644 --- a/quantum/service.py +++ b/quantum/service.py @@ -15,30 +15,104 @@ # License for the specific language governing permissions and limitations # under the License. +import logging import json import routes -from common import wsgi +from quantum.common import config +from quantum.common import wsgi +from quantum.common import exceptions as exception from webob import Response +LOG = logging.getLogger('quantum.service') -class NetworkController(wsgi.Controller): +class WsgiService(object): + """Base class for WSGI based services. - def version(self, request): - return "Quantum version 0.1" + For each api you define, you must also define these flags: + :_listen: The address on which to listen + :_listen_port: The port on which to listen + + """ + + def __init__(self, app_name, conf_file, conf): + self.app_name = app_name + self.conf_file = conf_file + self.conf = conf + self.wsgi_app = None + + def start(self): + self.wsgi_app = _run_wsgi(self.app_name, self.conf, self.conf_file) + + def wait(self): + self.wsgi_app.wait() -class API(wsgi.Router): - def __init__(self, options): - self.options = options - mapper = routes.Mapper() - network_controller = NetworkController() - mapper.resource("net_controller", "/network", - controller=network_controller) - mapper.connect("/", controller=network_controller, action="version") - super(API, self).__init__(mapper) +class QuantumApiService(WsgiService): + """Class for quantum-api service.""" + + @classmethod + def create(cls, conf=None, options=None, args=None): + app_name = "quantum" + if not conf: + conf_file, conf = config.load_paste_config( + app_name, options, args) + if not conf: + message = (_('No paste configuration found for: %s'), + app_name) + raise exception.Error(message) + print "OPTIONS:%s" %options + print "CONF:%s" %conf + + # Setup logging early, supplying both the CLI options and the + # configuration mapping from the config file + # We only update the conf dict for the verbose and debug + # flags. Everything else must be set up in the conf file... + # Log the options used when starting if we're in debug mode... + + config.setup_logging(options, conf) + debug = options.get('debug') or \ + config.get_option(conf, 'debug', + type='bool', default=False) + verbose = options.get('verbose') or \ + config.get_option(conf, 'verbose', + type='bool', default=False) + conf['debug'] = debug + conf['verbose'] = verbose + LOG.debug("*" * 80) + LOG.debug("Configuration options gathered from config file:") + LOG.debug(conf_file) + LOG.debug("================================================") + items = dict([(k, v) for k, v in conf.items() + if k not in ('__file__', 'here')]) + for key, value in sorted(items.items()): + LOG.debug("%(key)-30s %(value)s" % locals()) + LOG.debug("*" * 80) + service = cls(app_name, conf_file, conf) + return service -def app_factory(global_conf, **local_conf): - conf = global_conf.copy() - conf.update(local_conf) - return API(conf) +def serve_wsgi(cls, conf=None, options = None, args = None): + try: + service = cls.create(conf, options, args) + except Exception: + logging.exception('in WsgiService.create()') + raise + + service.start() + + return service + + +def _run_wsgi(app_name, paste_conf, paste_config_file): + print "CICCIO" + LOG.info(_('Using paste.deploy config at: %s'), paste_config_file) + app = config.load_paste_app(paste_config_file, app_name) + if not app: + LOG.error(_('No known API applications configured in %s.'), + paste_config_file) + return + server = wsgi.Server() + server.start(app, + int(paste_conf['bind_port']),paste_conf['bind_host']) + return server + From 64ed5da2053bc2e5b25516913e44cf26b9970a16 Mon Sep 17 00:00:00 2001 From: salvatore <> Date: Wed, 25 May 2011 13:11:12 +0100 Subject: [PATCH 07/15] adpating wsgi files --- quantum/api/networks.py | 4 +- quantum/common/exceptions.py | 4 + quantum/common/wsgi.py | 165 ++++++++++++++++++++++++++--------- quantum/utils.py | 33 +++++++ 4 files changed, 165 insertions(+), 41 deletions(-) diff --git a/quantum/api/networks.py b/quantum/api/networks.py index f98aa43e5..48cd89619 100644 --- a/quantum/api/networks.py +++ b/quantum/api/networks.py @@ -56,12 +56,12 @@ class Controller(wsgi.Controller): }, } - def index(self, request): + def index(self, req): """ Returns a list of network names and ids """ #TODO: this should be for a given tenant!!! print "PIPPO" LOG.debug("HERE - index") - return self._items(request, is_detail=False) + return self._items(req, is_detail=False) def _items(self, req, is_detail): """ Returns a list of networks. """ diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index bcc7696a2..60dde349b 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -53,6 +53,10 @@ class NotFound(Error): pass +class ClassNotFound(NotFound): + message = _("Class %(class_name)s could not be found") + + class Duplicate(Error): pass diff --git a/quantum/common/wsgi.py b/quantum/common/wsgi.py index 9a2b5bb58..d2d7badb0 100644 --- a/quantum/common/wsgi.py +++ b/quantum/common/wsgi.py @@ -25,6 +25,8 @@ import logging import sys import datetime +from xml.dom import minidom + import eventlet import eventlet.wsgi eventlet.patcher.monkey_patch(all=False, socket=True) @@ -33,6 +35,7 @@ import routes.middleware import webob.dec import webob.exc +from quantum import utils from quantum.common import exceptions as exception LOG = logging.getLogger('quantum.common.wsgi') @@ -313,66 +316,95 @@ class Router(object): class Controller(object): - """ + """WSGI app that dispatched to methods. + WSGI app that reads routing information supplied by RoutesMiddleware and calls the requested action method upon itself. All action methods must, in addition to their normal parameters, accept a 'req' argument - which is the incoming webob.Request. They raise a webob.exc exception, + which is the incoming wsgi.Request. They raise a webob.exc exception, or return a dict which will be serialized by requested content type. + """ - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): - """ - Call the method specified in req.environ by RoutesMiddleware. - """ + """Call the method specified in req.environ by RoutesMiddleware.""" arg_dict = req.environ['wsgiorg.routing_args'][1] action = arg_dict['action'] method = getattr(self, action) + LOG.debug("%s %s" % (req.method, req.url)) del arg_dict['controller'] del arg_dict['action'] - arg_dict['request'] = req + if 'format' in arg_dict: + del arg_dict['format'] + arg_dict['req'] = req result = method(**arg_dict) + if type(result) is dict: - return self._serialize(result, req) + content_type = req.best_match_content_type() + default_xmlns = self.get_default_xmlns(req) + body = self._serialize(result, content_type, default_xmlns) + + response = webob.Response() + response.headers['Content-Type'] = content_type + response.body = body + msg_dict = dict(url=req.url, status=response.status_int) + msg = _("%(url)s returned with HTTP %(status)d") % msg_dict + LOG.debug(msg) + return response else: return result - def _serialize(self, data, request): - """ - Serialize the given dict to the response type requested in request. + def _serialize(self, data, content_type, default_xmlns): + """Serialize the given dict to the provided content_type. + Uses self._serialization_metadata if it exists, which is a dict mapping MIME types to information needed to serialize to that type. + """ - _metadata = getattr(type(self), "_serialization_metadata", {}) + _metadata = getattr(type(self), '_serialization_metadata', {}) + + serializer = Serializer(_metadata, default_xmlns) + try: + return serializer.serialize(data, content_type) + except exception.InvalidContentType: + raise webob.exc.HTTPNotAcceptable() + + def _deserialize(self, data, content_type): + """Deserialize the request body to the specefied content type. + + Uses self._serialization_metadata if it exists, which is a dict mapping + MIME types to information needed to serialize to that type. + + """ + _metadata = getattr(type(self), '_serialization_metadata', {}) serializer = Serializer(_metadata) - return serializer.to_content_type(data) - + return serializer.deserialize(data, content_type) + def get_default_xmlns(self, req): + """Provide the XML namespace to use if none is otherwise specified.""" + return None class Serializer(object): - """ - Serializes a dictionary to a Content Type specified by a WSGI environment. - """ + """Serializes and deserializes dictionaries to certain MIME types.""" + + def __init__(self, metadata=None, default_xmlns=None): + """Create a serializer based on the given WSGI environment. - def __init__(self,metadata=None): - """ - Create a serializer based on the given WSGI environment. 'metadata' is an optional dict mapping MIME types to information needed to serialize a dictionary to that type. + """ self.metadata = metadata or {} - self._methods = { - 'application/json': self._to_json, - 'application/xml': self._to_xml} - + self.default_xmlns = default_xmlns def _get_serialize_handler(self, content_type): handlers = { 'application/json': self._to_json, 'application/xml': self._to_xml, } + try: return handlers[content_type] except Exception: @@ -382,6 +414,14 @@ class Serializer(object): """Serialize a dictionary into the specified content type.""" return self._get_serialize_handler(content_type)(data) + def deserialize(self, datastring, content_type): + """Deserialize a string to a dictionary. + + The string must be in the format of a supported MIME type. + + """ + return self.get_deserialize_handler(content_type)(datastring) + def get_deserialize_handler(self, content_type): handlers = { 'application/json': self._from_json, @@ -392,36 +432,72 @@ class Serializer(object): return handlers[content_type] except Exception: raise exception.InvalidContentType(content_type=content_type) - - def deserialize(self, datastring, content_type): - """Deserialize a string to a dictionary. - The string must be in the format of a supported MIME type. + def _from_json(self, datastring): + return utils.loads(datastring) + + def _from_xml(self, datastring): + xmldata = self.metadata.get('application/xml', {}) + plurals = set(xmldata.get('plurals', {})) + node = minidom.parseString(datastring).childNodes[0] + return {node.nodeName: self._from_xml_node(node, plurals)} + + def _from_xml_node(self, node, listnames): + """Convert a minidom node to a simple Python type. + + listnames is a collection of names of XML nodes whose subnodes should + be considered list items. """ - return self.get_deserialize_handler(content_type)(datastring) - - def _to_json(self, data): - def sanitizer(obj): - if isinstance(obj, datetime.datetime): - return obj.isoformat() - return obj + if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3: + return node.childNodes[0].nodeValue + elif node.nodeName in listnames: + return [self._from_xml_node(n, listnames) for n in node.childNodes] + else: + result = dict() + for attr in node.attributes.keys(): + result[attr] = node.attributes[attr].nodeValue + for child in node.childNodes: + if child.nodeType != node.TEXT_NODE: + result[child.nodeName] = self._from_xml_node(child, + listnames) + return result - return json.dumps(data, default=sanitizer) + def _to_json(self, data): + return utils.dumps(data) def _to_xml(self, data): metadata = self.metadata.get('application/xml', {}) # We expect data to contain a single key which is the XML root. root_key = data.keys()[0] - from xml.dom import minidom doc = minidom.Document() node = self._to_xml_node(doc, metadata, root_key, data[root_key]) + + xmlns = node.getAttribute('xmlns') + if not xmlns and self.default_xmlns: + node.setAttribute('xmlns', self.default_xmlns) + return node.toprettyxml(indent=' ') def _to_xml_node(self, doc, metadata, nodename, data): """Recursive method to convert data members to XML nodes.""" result = doc.createElement(nodename) + + # Set the xml namespace if one is specified + # TODO(justinsb): We could also use prefixes on the keys + xmlns = metadata.get('xmlns', None) + if xmlns: + result.setAttribute('xmlns', xmlns) + if type(data) is list: + collections = metadata.get('list_collections', {}) + if nodename in collections: + metadata = collections[nodename] + for item in data: + node = doc.createElement(metadata['item_name']) + node.setAttribute(metadata['item_key'], str(item)) + result.appendChild(node) + return result singular = metadata.get('plurals', {}).get(nodename, None) if singular is None: if nodename.endswith('s'): @@ -432,15 +508,26 @@ class Serializer(object): node = self._to_xml_node(doc, metadata, singular, item) result.appendChild(node) elif type(data) is dict: + collections = metadata.get('dict_collections', {}) + if nodename in collections: + metadata = collections[nodename] + for k, v in data.items(): + node = doc.createElement(metadata['item_name']) + node.setAttribute(metadata['item_key'], str(k)) + text = doc.createTextNode(str(v)) + node.appendChild(text) + result.appendChild(node) + return result attrs = metadata.get('attributes', {}).get(nodename, {}) for k, v in data.items(): - LOG.debug("K:%s - V:%s",k,v) if k in attrs: result.setAttribute(k, str(v)) else: node = self._to_xml_node(doc, metadata, k, v) result.appendChild(node) - else: # atom + else: + # Type is atom node = doc.createTextNode(str(data)) result.appendChild(node) return result + diff --git a/quantum/utils.py b/quantum/utils.py index 284b18443..c56631627 100644 --- a/quantum/utils.py +++ b/quantum/utils.py @@ -35,6 +35,8 @@ import sys import time import types +from quantum.common import exceptions as exception + def import_class(import_str): """Returns a class from a string including module and class.""" @@ -55,3 +57,34 @@ def import_object(import_str): except ImportError: cls = import_class(import_str) return cls() + +def to_primitive(value): + if type(value) is type([]) or type(value) is type((None,)): + o = [] + for v in value: + o.append(to_primitive(v)) + return o + elif type(value) is type({}): + o = {} + for k, v in value.iteritems(): + o[k] = to_primitive(v) + return o + elif isinstance(value, datetime.datetime): + return str(value) + elif hasattr(value, 'iteritems'): + return to_primitive(dict(value.iteritems())) + elif hasattr(value, '__iter__'): + return to_primitive(list(value)) + else: + return value + +def dumps(value): + try: + return json.dumps(value) + except TypeError: + pass + return json.dumps(to_primitive(value)) + + +def loads(s): + return json.loads(s) From 5022c07f6b28931a4df2adf197bee733cf3e5d8a Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Thu, 26 May 2011 18:53:48 +0100 Subject: [PATCH 08/15] Implementing interface with plugin --- quantum/api/__init__.py | 9 +++++---- quantum/api/networks.py | 24 ++++++++++++++---------- quantum/common/utils.py | 16 ++++++++++++---- quantum/common/wsgi.py | 9 ++++++++- quantum/manager.py | 3 ++- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py index 9e7d55497..1ea601c14 100644 --- a/quantum/api/__init__.py +++ b/quantum/api/__init__.py @@ -70,10 +70,11 @@ class APIRouterV01(wsgi.Router): #server_members['unrescue'] = 'POST' #server_members['reset_network'] = 'POST' #server_members['inject_network_info'] = 'POST' - - mapper.resource("network", "networks", controller=networks.Controller(), - collection={'detail': 'GET'}) - print mapper + mapper.resource("/tenants/{tenant_id}/network", "/tenants/{tenant_id}/networks", controller=networks.Controller()) + print "AFTER MAPPING" + print mapper + for route in mapper.matchlist: + print "Found route:%s %s" %(route.defaults,route.conditions) #mapper.resource("port", "ports", controller=ports.Controller(), # collection=dict(public='GET', private='GET'), # parent_resource=dict(member_name='network', diff --git a/quantum/api/networks.py b/quantum/api/networks.py index 48cd89619..5700a1996 100644 --- a/quantum/api/networks.py +++ b/quantum/api/networks.py @@ -56,28 +56,32 @@ class Controller(wsgi.Controller): }, } - def index(self, req): + def __init__(self): + self._setup_network_manager() + super(Controller, self).__init__() + + def _setup_network_manager(self): + self.network_manager=manager.QuantumManager().get_manager() + + def index(self, req, tenant_id): """ Returns a list of network names and ids """ #TODO: this should be for a given tenant!!! - print "PIPPO" - LOG.debug("HERE - index") - return self._items(req, is_detail=False) + LOG.debug("HERE - Controller.index") + return self._items(req, tenant_id, is_detail=False) - def _items(self, req, is_detail): + def _items(self, req, tenant_id, is_detail): """ Returns a list of networks. """ - #TODO: we should return networks for a given tenant only - #TODO: network controller should be retrieved here!!! - test = { 'ciao':'bello','porco':'mondo' } + test = self.network_manager.get_all_networks(tenant_id) #builder = self._get_view_builder(req) #servers = [builder.build(inst, is_detail)['server'] # for inst in limited_list] #return dict(servers=servers) return test - def show(self, req, id): + def show(self, req, tenant_id, id): """ Returns network details by network id """ try: - return "TEST NETWORK DETAILS" + return "SHOW NETWORK %s FOR TENANT %s" %(id,tenant_id) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) diff --git a/quantum/common/utils.py b/quantum/common/utils.py index c56a53ac1..4acea9852 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -29,12 +29,13 @@ import socket import sys import ConfigParser -from quantum.common import exceptions +from quantum.common import exceptions as exception +from quantum.common import flags from exceptions import ProcessExecutionError TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" - +FLAGS = flags.FLAGS def int_from_bool_as_string(subject): """ @@ -70,10 +71,16 @@ def bool_from_string(subject): def import_class(import_str): """Returns a class from a string including module and class""" mod_str, _sep, class_str = import_str.rpartition('.') + print "MOD_STR:%s SEP:%s CLASS_STR:%s" %(mod_str, _sep, class_str) try: + #mod_str = os.path.join(FLAGS.state_path, mod_str) + print "MODULE PATH:%s" %mod_str + print "CUR DIR:%s" %os.getcwd() __import__(mod_str) + print "IO SONO QUI" return getattr(sys.modules[mod_str], class_str) - except (ImportError, ValueError, AttributeError): + except (ImportError, ValueError, AttributeError) as e: + print e raise exception.NotFound('Class %s cannot be found' % class_str) @@ -186,8 +193,9 @@ def parse_isotime(timestr): return datetime.datetime.strptime(timestr, TIME_FORMAT) def getPluginFromConfig(file="config.ini"): + print "FILE:%s" %os.path.join(FLAGS.state_path, file) Config = ConfigParser.ConfigParser() - Config.read(file) + Config.read(os.path.join(FLAGS.state_path, file)) return Config.get("PLUGIN", "provider") diff --git a/quantum/common/wsgi.py b/quantum/common/wsgi.py index d2d7badb0..277b482d6 100644 --- a/quantum/common/wsgi.py +++ b/quantum/common/wsgi.py @@ -298,6 +298,7 @@ class Router(object): Route the incoming request to a controller based on self.map. If no match, return a 404. """ + LOG.debug("HERE - wsgi.Router.__call__") return self._router @staticmethod @@ -328,10 +329,16 @@ class Controller(object): @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): - """Call the method specified in req.environ by RoutesMiddleware.""" + """ + Call the method specified in req.environ by RoutesMiddleware. + """ + LOG.debug("HERE - wsgi.Controller.__call__") arg_dict = req.environ['wsgiorg.routing_args'][1] action = arg_dict['action'] method = getattr(self, action) + LOG.debug("ARG_DICT:%s",arg_dict) + LOG.debug("Action:%s",action) + LOG.debug("Method:%s",method) LOG.debug("%s %s" % (req.method, req.url)) del arg_dict['controller'] del arg_dict['action'] diff --git a/quantum/manager.py b/quantum/manager.py index a36e9b28f..ed09f365c 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -27,13 +27,14 @@ The caller should make sure that QuantumManager is a singleton. from common import utils from quantum_plugin_base import QuantumPluginBase -CONFIG_FILE = "plugins.ini" +CONFIG_FILE = "quantum/plugins.ini" class QuantumManager(object): def __init__(self,config=CONFIG_FILE): self.configuration_file = CONFIG_FILE plugin_location = utils.getPluginFromConfig(CONFIG_FILE) + print "PLUGIN LOCATION:%s" %plugin_location plugin_klass = utils.import_class(plugin_location) if not issubclass(plugin_klass, QuantumPluginBase): raise Exception("Configured Quantum plug-in didn't pass compatibility test") From 55599d71849b460a1b0a90e7406542162ba8efdc Mon Sep 17 00:00:00 2001 From: salvatore <> Date: Fri, 27 May 2011 00:52:11 +0100 Subject: [PATCH 09/15] networks api with final URL structure. No serialization yet --- bin/quantum | 1 + quantum/common/utils.py | 7 ++++--- quantum/manager.py | 7 ++++++- quantum/plugins.ini | 2 +- quantum/utils.py | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/bin/quantum b/bin/quantum index 9971e795a..0c4a5dd6c 100755 --- a/bin/quantum +++ b/bin/quantum @@ -56,6 +56,7 @@ if __name__ == '__main__': try: print "HERE-1" + print sys.path service = service.serve_wsgi(service.QuantumApiService, options=options, args=args) diff --git a/quantum/common/utils.py b/quantum/common/utils.py index 4acea9852..1317bc157 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -29,8 +29,8 @@ import socket import sys import ConfigParser -from quantum.common import exceptions as exception -from quantum.common import flags +import exceptions as exception +import flags from exceptions import ProcessExecutionError @@ -76,7 +76,7 @@ def import_class(import_str): #mod_str = os.path.join(FLAGS.state_path, mod_str) print "MODULE PATH:%s" %mod_str print "CUR DIR:%s" %os.getcwd() - __import__(mod_str) + __import__(mod_str, level=2) print "IO SONO QUI" return getattr(sys.modules[mod_str], class_str) except (ImportError, ValueError, AttributeError) as e: @@ -194,6 +194,7 @@ def parse_isotime(timestr): def getPluginFromConfig(file="config.ini"): print "FILE:%s" %os.path.join(FLAGS.state_path, file) + print "Globals:%s" %globals() Config = ConfigParser.ConfigParser() Config.read(os.path.join(FLAGS.state_path, file)) return Config.get("PLUGIN", "provider") diff --git a/quantum/manager.py b/quantum/manager.py index ed09f365c..69e78561a 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -23,7 +23,9 @@ plugin that concretely implement quantum_plugin_base class The caller should make sure that QuantumManager is a singleton. """ - +import gettext +gettext.install('quantum', unicode=1) + from common import utils from quantum_plugin_base import QuantumPluginBase @@ -45,6 +47,9 @@ class QuantumManager(object): def get_manager(self): return self.plugin + + + # TODO(somik): rmove the main class # Added for temporary testing purposes def main(): diff --git a/quantum/plugins.ini b/quantum/plugins.ini index 61c7694cd..e6dc080d4 100644 --- a/quantum/plugins.ini +++ b/quantum/plugins.ini @@ -1,3 +1,3 @@ [PLUGIN] # Quantum plugin provider module -provider = plugins.SamplePlugin.DummyDataPlugin +provider = quantum.plugins.SamplePlugin.DummyDataPlugin diff --git a/quantum/utils.py b/quantum/utils.py index c56631627..c14135d47 100644 --- a/quantum/utils.py +++ b/quantum/utils.py @@ -35,7 +35,7 @@ import sys import time import types -from quantum.common import exceptions as exception +from common import exceptions as exception def import_class(import_str): From e8c29b8b96eeb40fe0bdbba340b8057036427dd7 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Fri, 27 May 2011 17:52:06 +0100 Subject: [PATCH 10/15] Adding serialization/deserilization for network resources. Adding fake plugin --- bin/quantum | 12 -- quantum/api/networks.py | 218 ++++++++++++-------------------- quantum/common/utils.py | 8 +- quantum/common/wsgi.py | 13 +- quantum/plugins.ini | 2 +- quantum/plugins/SamplePlugin.py | 158 +++++++++++++++++++++++ 6 files changed, 249 insertions(+), 162 deletions(-) diff --git a/bin/quantum b/bin/quantum index 0c4a5dd6c..0913c31c0 100755 --- a/bin/quantum +++ b/bin/quantum @@ -22,9 +22,7 @@ import gettext import optparse import os -import re import sys -import time possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), @@ -36,7 +34,6 @@ if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')): gettext.install('quantum', unicode=1) from quantum import service -from quantum.common import wsgi from quantum.common import config def create_options(parser): @@ -55,19 +52,10 @@ if __name__ == '__main__': (options, args) = config.parse_options(oparser) try: - print "HERE-1" - print sys.path service = service.serve_wsgi(service.QuantumApiService, options=options, args=args) - #version_conf, version_app = config.load_paste_app('quantumversion', options, args) - print "HERE-2" service.wait() - #api_conf, api_app = config.load_paste_app('quantum', options, args) - #server = wsgi.Server() - #server.start(version_app, int(version_conf['bind_port']), version_conf['bind_host']) - #server.start(api_app, int(api_conf['bind_port']), api_conf['bind_host']) - #server.wait() except RuntimeError, e: sys.exit("ERROR: %s" % e) diff --git a/quantum/api/networks.py b/quantum/api/networks.py index 5700a1996..88d56fcb8 100644 --- a/quantum/api/networks.py +++ b/quantum/api/networks.py @@ -13,22 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. -import base64 +import httplib import logging -import traceback from webob import exc from xml.dom import minidom from quantum import manager -from quantum import quantum_plugin_base from quantum.common import exceptions as exception from quantum.common import flags from quantum.common import wsgi -from quantum import utils -from quantum.api import api_common as common from quantum.api import faults -import quantum.api +from quantum.api.views import networks as networks_view LOG = logging.getLogger('quantum.api.networks') FLAGS = flags.FLAGS @@ -37,168 +33,114 @@ FLAGS = flags.FLAGS class Controller(wsgi.Controller): """ Network API controller for Quantum API """ - #TODO (salvatore-orlando): adjust metadata for quantum + _network_ops_param_list = [{ + 'param-name': 'network-name', + 'required': True},] + _serialization_metadata = { "application/xml": { "attributes": { - "server": ["id", "imageId", "name", "flavorId", "hostId", - "status", "progress", "adminPass", "flavorRef", - "imageRef"], + "network": ["id","name"], "link": ["rel", "type", "href"], }, - "dict_collections": { - "metadata": {"item_name": "meta", "item_key": "key"}, - }, - "list_collections": { - "public": {"item_name": "ip", "item_key": "addr"}, - "private": {"item_name": "ip", "item_key": "addr"}, - }, }, } - def __init__(self): + def __init__(self, plugin_conf_file=None): self._setup_network_manager() super(Controller, self).__init__() + def _parse_request_params(self, req, params): + results = {} + for param in params: + param_name = param['param-name'] + # 1- parse request body + # 2- parse request headers + # prepend param name with a 'x-' prefix + param_value = req.headers.get("x-" + param_name, None) + # 3- parse request query parameters + if not param_value: + param_value = req.str_GET[param_name] + if not param_value and param['required']: + msg = ("Failed to parse request. " + + "Parameter: %(param)s not specified" % locals()) + for line in msg.split('\n'): + LOG.error(line) + raise exc.HTTPBadRequest(msg) + results[param_name]=param_value + return results + def _setup_network_manager(self): self.network_manager=manager.QuantumManager().get_manager() def index(self, req, tenant_id): """ Returns a list of network names and ids """ #TODO: this should be for a given tenant!!! - LOG.debug("HERE - Controller.index") return self._items(req, tenant_id, is_detail=False) def _items(self, req, tenant_id, is_detail): """ Returns a list of networks. """ - test = self.network_manager.get_all_networks(tenant_id) - #builder = self._get_view_builder(req) - #servers = [builder.build(inst, is_detail)['server'] - # for inst in limited_list] - #return dict(servers=servers) - return test + networks = self.network_manager.get_all_networks(tenant_id) + builder = networks_view.get_view_builder(req) + result = [builder.build(network, is_detail)['network'] + for network in networks] + return dict(networks=result) def show(self, req, tenant_id, id): """ Returns network details by network id """ try: - return "SHOW NETWORK %s FOR TENANT %s" %(id,tenant_id) + network = self.network_manager.get_network_details( + tenant_id,id) + builder = networks_view.get_view_builder(req) + #build response with details + result = builder.build(network, True) + return dict(networks=result) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) - def delete(self, req, id): + def create(self, req, tenant_id): + """ Creates a new network for a given tenant """ + #look for network name in request + req_params = \ + self._parse_request_params(req, self._network_ops_param_list) + network = self.network_manager.create_network(tenant_id, req_params['network-name']) + builder = networks_view.get_view_builder(req) + result = builder.build(network) + return dict(networks=result) + + def update(self, req, tenant_id, id): + """ Updates the name for the network with the given id """ + try: + network_name = req.headers['x-network-name'] + except KeyError as e: + msg = ("Failed to create network. Got error: %(e)s" % locals()) + for line in msg.split('\n'): + LOG.error(line) + raise exc.HTTPBadRequest(msg) + + network = self.network_manager.rename_network(tenant_id, + id,network_name) + if not network: + raise exc.HTTPNotFound("Network %(id)s could not be found" % locals()) + builder = networks_view.get_view_builder(req) + result = builder.build(network, True) + return dict(networks=result) + + + def delete(self, req, tenant_id, id): """ Destroys the network with the given id """ try: - return "TEST NETWORK DELETE" - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) + network_name = req.headers['x-network-name'] + except KeyError as e: + msg = ("Failed to create network. Got error: %(e)s" % locals()) + for line in msg.split('\n'): + LOG.error(line) + raise exc.HTTPBadRequest(msg) + + network = self.network_manager.delete_network(tenant_id, id) + if not network: + raise exc.HTTPNotFound("Network %(id)s could not be found" % locals()) + return exc.HTTPAccepted() - def create(self, req): - """ Creates a new network for a given tenant """ - #env = self._deserialize_create(req) - #if not env: - # return faults.Fault(exc.HTTPUnprocessableEntity()) - return "TEST NETWORK CREATE" - def _deserialize_create(self, request): - """ - Deserialize a create request - Overrides normal behavior in the case of xml content - """ - #if request.content_type == "application/xml": - # deserializer = ServerCreateRequestXMLDeserializer() - # return deserializer.deserialize(request.body) - #else: - # return self._deserialize(request.body, request.get_content_type()) - pass - - def update(self, req, id): - """ Updates the name for the network wit the given id """ - if len(req.body) == 0: - raise exc.HTTPUnprocessableEntity() - - inst_dict = self._deserialize(req.body, req.get_content_type()) - if not inst_dict: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - try: - return "TEST NETWORK UPDATE" - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPNoContent() - - -class NetworkCreateRequestXMLDeserializer(object): - """ - Deserializer to handle xml-formatted server create requests. - - Handles standard server attributes as well as optional metadata - and personality attributes - """ - - def deserialize(self, string): - """Deserialize an xml-formatted server create request""" - dom = minidom.parseString(string) - server = self._extract_server(dom) - return {'server': server} - - def _extract_server(self, node): - """Marshal the server attribute of a parsed request""" - server = {} - server_node = self._find_first_child_named(node, 'server') - for attr in ["name", "imageId", "flavorId"]: - server[attr] = server_node.getAttribute(attr) - metadata = self._extract_metadata(server_node) - if metadata is not None: - server["metadata"] = metadata - personality = self._extract_personality(server_node) - if personality is not None: - server["personality"] = personality - return server - - def _extract_metadata(self, server_node): - """Marshal the metadata attribute of a parsed request""" - metadata_node = self._find_first_child_named(server_node, "metadata") - if metadata_node is None: - return None - metadata = {} - for meta_node in self._find_children_named(metadata_node, "meta"): - key = meta_node.getAttribute("key") - metadata[key] = self._extract_text(meta_node) - return metadata - - def _extract_personality(self, server_node): - """Marshal the personality attribute of a parsed request""" - personality_node = \ - self._find_first_child_named(server_node, "personality") - if personality_node is None: - return None - personality = [] - for file_node in self._find_children_named(personality_node, "file"): - item = {} - if file_node.hasAttribute("path"): - item["path"] = file_node.getAttribute("path") - item["contents"] = self._extract_text(file_node) - personality.append(item) - return personality - - def _find_first_child_named(self, parent, name): - """Search a nodes children for the first child with a given name""" - for node in parent.childNodes: - if node.nodeName == name: - return node - return None - - def _find_children_named(self, parent, name): - """Return all of a nodes children who have the given name""" - for node in parent.childNodes: - if node.nodeName == name: - yield node - - def _extract_text(self, node): - """Get the text field contained by the given node""" - if len(node.childNodes) == 1: - child = node.childNodes[0] - if child.nodeType == child.TEXT_NODE: - return child.nodeValue - return "" diff --git a/quantum/common/utils.py b/quantum/common/utils.py index 1317bc157..c80ac9ccc 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -71,13 +71,9 @@ def bool_from_string(subject): def import_class(import_str): """Returns a class from a string including module and class""" mod_str, _sep, class_str = import_str.rpartition('.') - print "MOD_STR:%s SEP:%s CLASS_STR:%s" %(mod_str, _sep, class_str) try: #mod_str = os.path.join(FLAGS.state_path, mod_str) - print "MODULE PATH:%s" %mod_str - print "CUR DIR:%s" %os.getcwd() - __import__(mod_str, level=2) - print "IO SONO QUI" + __import__(mod_str) return getattr(sys.modules[mod_str], class_str) except (ImportError, ValueError, AttributeError) as e: print e @@ -193,8 +189,6 @@ def parse_isotime(timestr): return datetime.datetime.strptime(timestr, TIME_FORMAT) def getPluginFromConfig(file="config.ini"): - print "FILE:%s" %os.path.join(FLAGS.state_path, file) - print "Globals:%s" %globals() Config = ConfigParser.ConfigParser() Config.read(os.path.join(FLAGS.state_path, file)) return Config.get("PLUGIN", "provider") diff --git a/quantum/common/wsgi.py b/quantum/common/wsgi.py index 277b482d6..4caeab9ab 100644 --- a/quantum/common/wsgi.py +++ b/quantum/common/wsgi.py @@ -126,7 +126,7 @@ class Request(webob.Request): """ parts = self.path.rsplit('.', 1) - + LOG.debug("Request parts:%s",parts) if len(parts) > 1: format = parts[1] if format in ['json', 'xml']: @@ -134,7 +134,7 @@ class Request(webob.Request): ctypes = ['application/json', 'application/xml'] bm = self.accept.best_match(ctypes) - + LOG.debug("BM:%s",bm) return bm or 'application/json' def get_content_type(self): @@ -281,7 +281,7 @@ class Router(object): mapper.connect(None, "/svrlist", controller=sc, action="list") # Actions are all implicitly defined - mapper.resource("server", "servers", controller=sc) + mapper.resource("network", "networks", controller=nc) # Pointing to an arbitrary WSGI app. You can specify the # {path_info:.*} parameter so the target app can be handed just that @@ -349,6 +349,8 @@ class Controller(object): if type(result) is dict: content_type = req.best_match_content_type() + LOG.debug("Content type:%s",content_type) + LOG.debug("Result:%s",result) default_xmlns = self.get_default_xmlns(req) body = self._serialize(result, content_type, default_xmlns) @@ -495,8 +497,9 @@ class Serializer(object): xmlns = metadata.get('xmlns', None) if xmlns: result.setAttribute('xmlns', xmlns) - + LOG.debug("DATA:%s",data) if type(data) is list: + LOG.debug("TYPE IS LIST") collections = metadata.get('list_collections', {}) if nodename in collections: metadata = collections[nodename] @@ -515,6 +518,7 @@ class Serializer(object): node = self._to_xml_node(doc, metadata, singular, item) result.appendChild(node) elif type(data) is dict: + LOG.debug("TYPE IS DICT") collections = metadata.get('dict_collections', {}) if nodename in collections: metadata = collections[nodename] @@ -534,6 +538,7 @@ class Serializer(object): result.appendChild(node) else: # Type is atom + LOG.debug("TYPE IS ATOM:%s",data) node = doc.createTextNode(str(data)) result.appendChild(node) return result diff --git a/quantum/plugins.ini b/quantum/plugins.ini index e6dc080d4..307d2b48d 100644 --- a/quantum/plugins.ini +++ b/quantum/plugins.ini @@ -1,3 +1,3 @@ [PLUGIN] # Quantum plugin provider module -provider = quantum.plugins.SamplePlugin.DummyDataPlugin +provider = quantum.plugins.SamplePlugin.FakePlugin diff --git a/quantum/plugins/SamplePlugin.py b/quantum/plugins/SamplePlugin.py index 5088b71fa..431f5fbc7 100644 --- a/quantum/plugins/SamplePlugin.py +++ b/quantum/plugins/SamplePlugin.py @@ -259,4 +259,162 @@ class DummyDataPlugin(object): # returns a list of all attached remote interfaces vifs_on_net = ["/tenant1/networks/net_id/portid/vif2.0", "/tenant1/networks/10/121/vif1.1"] return vifs_on_net + + +class FakePlugin(object): + """ + FakePlugin is a demo plugin that provides + in-memory data structures to aid in quantum + client/cli/api development + """ + def __init__(self): + #add a first sample network on init + self._networks={'001': + { + 'net-id':'001', + 'net-name':'pippotest' + }, + '002': + { + 'net-id':'002', + 'net-name':'cicciotest' + }} + self._net_counter=len(self._networks) + + def get_all_networks(self, tenant_id): + """ + Returns a dictionary containing all + for + the specified tenant. + """ + print("get_all_networks() called\n") + return self._networks.values() + + def get_network_details(self, tenant_id, net_id): + """ + retrieved a list of all the remote vifs that + are attached to the network + """ + print("get_network_details() called\n") + return self._networks.get(net_id) + + + def create_network(self, tenant_id, net_name): + """ + Creates a new Virtual Network, and assigns it + a symbolic name. + """ + print("create_network() called\n") + self._net_counter += 1 + new_net_id=("0" * (3 - len(str(self._net_counter)))) + \ + str(self._net_counter) + print new_net_id + new_net_dict={'net-id':new_net_id, + 'net-name':net_name} + self._networks[new_net_id]=new_net_dict + # return network_id of the created network + return new_net_dict + + + def delete_network(self, tenant_id, net_id): + """ + Deletes the network with the specified network identifier + belonging to the specified tenant. + """ + print("delete_network() called\n") + net = self._networks.get(net_id) + if net: + self._networks.pop(net_id) + return net + return None + + + def rename_network(self, tenant_id, net_id, new_name): + """ + Updates the symbolic name belonging to a particular + Virtual Network. + """ + print("rename_network() called\n") + net = self._networks.get(net_id, None) + if net: + net['net-name']=new_name + return net + return None + + + #TODO - neeed to update methods from this point onwards + def get_all_ports(self, tenant_id, net_id): + """ + Retrieves all port identifiers belonging to the + specified Virtual Network. + """ + print("get_all_ports() called\n") + port_ids_on_net = ["2", "3", "4"] + return port_ids_on_net + + + def create_port(self, tenant_id, net_id): + """ + Creates a port on the specified Virtual Network. + """ + print("create_port() called\n") + #return the port id + return 201 + + + def delete_port(self, tenant_id, net_id, port_id): + """ + Deletes a port on a specified Virtual Network, + if the port contains a remote interface attachment, + the remote interface is first un-plugged and then the port + is deleted. + """ + print("delete_port() called\n") + + + def get_port_details(self, tenant_id, net_id, port_id): + """ + This method allows the user to retrieve a remote interface + that is attached to this particular port. + """ + print("get_port_details() called\n") + #returns the remote interface UUID + return "/tenant1/networks/net_id/portid/vif2.1" + + + def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id): + """ + Attaches a remote interface to the specified port on the + specified Virtual Network. + """ + print("plug_interface() called\n") + + + def unplug_interface(self, tenant_id, net_id, port_id): + """ + Detaches a remote interface from the specified port on the + specified Virtual Network. + """ + print("unplug_interface() called\n") + + + def get_interface_details(self, tenant_id, net_id, port_id): + """ + Retrieves the remote interface that is attached at this + particular port. + """ + print("get_interface_details() called\n") + #returns the remote interface UUID + return "/tenant1/networks/net_id/portid/vif2.0" + + + def get_all_attached_interfaces(self, tenant_id, net_id): + """ + Retrieves all remote interfaces that are attached to + a particular Virtual Network. + """ + print("get_all_attached_interfaces() called\n") + # returns a list of all attached remote interfaces + vifs_on_net = ["/tenant1/networks/net_id/portid/vif2.0", "/tenant1/networks/10/121/vif1.1"] + return vifs_on_net \ No newline at end of file From 4d0029e8797729d1be697f587c8c3808dc5b3b7a Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Sat, 28 May 2011 21:52:09 +0100 Subject: [PATCH 11/15] Adding views/networks.py to bzr --- quantum/api/views/networks.py | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 quantum/api/views/networks.py diff --git a/quantum/api/views/networks.py b/quantum/api/views/networks.py new file mode 100644 index 000000000..2a64d87f0 --- /dev/null +++ b/quantum/api/views/networks.py @@ -0,0 +1,50 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems +# 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 os + + +def get_view_builder(req): + base_url = req.application_url + return ViewBuilder(base_url) + + +class ViewBuilder(object): + + def __init__(self, base_url): + """ + :param base_url: url of the root wsgi application + """ + self.base_url = base_url + + def build(self, network_data, is_detail=False): + """Generic method used to generate a network entity.""" + print "NETWORK-DATA:%s" %network_data + if is_detail: + network = self._build_detail(network_data) + else: + network = self._build_simple(network_data) + return network + + def _build_simple(self, network_data): + """Return a simple model of a server.""" + return dict(network=dict(id=network_data['net-id'])) + + def _build_detail(self, network_data): + """Return a simple model of a server.""" + return dict(network=dict(id=network_data['net-id'], + name=network_data['net-name'])) From 9f1c2488260868ce0c4a5ed60219c92679829281 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Mon, 30 May 2011 01:08:46 +0100 Subject: [PATCH 12/15] First working version of Quantum API --- quantum/api/__init__.py | 56 ++++----- quantum/api/api_common.py | 47 +++++++ quantum/api/faults.py | 97 +++++++++++++-- quantum/api/networks.py | 98 +++++---------- quantum/api/views/ports.py | 48 +++++++ quantum/common/exceptions.py | 54 +++++++- quantum/plugins/SamplePlugin.py | 214 ++++++++++++++++++++++---------- quantum/quantum_plugin_base.py | 10 +- test_scripts/__init__.py | 0 test_scripts/miniclient.py | 98 +++++++++++++++ test_scripts/tests.py | 150 ++++++++++++++++++++++ 11 files changed, 700 insertions(+), 172 deletions(-) create mode 100644 quantum/api/views/ports.py create mode 100644 test_scripts/__init__.py create mode 100644 test_scripts/miniclient.py create mode 100644 test_scripts/tests.py diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py index 1ea601c14..0b459e177 100644 --- a/quantum/api/__init__.py +++ b/quantum/api/__init__.py @@ -26,6 +26,7 @@ import webob.exc from quantum.api import faults from quantum.api import networks +from quantum.api import ports from quantum.common import flags from quantum.common import wsgi @@ -33,18 +34,6 @@ from quantum.common import wsgi LOG = logging.getLogger('quantum.api') FLAGS = flags.FLAGS -class FaultWrapper(wsgi.Middleware): - """Calls down the middleware stack, making exceptions into faults.""" - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - try: - return req.get_response(self.application) - except Exception as ex: - LOG.exception(_("Caught error: %s"), unicode(ex)) - exc = webob.exc.HTTPInternalServerError(explanation=unicode(ex)) - return faults.Fault(exc) - class APIRouterV01(wsgi.Router): """ @@ -57,26 +46,33 @@ class APIRouterV01(wsgi.Router): super(APIRouterV01, self).__init__(mapper) def _setup_routes(self, mapper): - #server_members = self.server_members - #server_members['action'] = 'POST' - #server_members['pause'] = 'POST' - #server_members['unpause'] = 'POST' - #server_members['diagnostics'] = 'GET' - #server_members['actions'] = 'GET' - #server_members['suspend'] = 'POST' - #server_members['resume'] = 'POST' - #server_members['rescue'] = 'POST' - #server_members['unrescue'] = 'POST' - #server_members['reset_network'] = 'POST' - #server_members['inject_network_info'] = 'POST' - mapper.resource("/tenants/{tenant_id}/network", "/tenants/{tenant_id}/networks", controller=networks.Controller()) + uri_prefix = '/tenants/{tenant_id}/' + mapper.resource('network', + 'networks', + controller=networks.Controller(), + path_prefix=uri_prefix) + mapper.resource("port", "ports", controller=ports.Controller(), + parent_resource=dict(member_name='network', + collection_name= uri_prefix + 'networks')) + + mapper.connect("get_resource", + uri_prefix + 'networks/{network_id}/ports/{id}/attachment{.format}', + controller=ports.Controller(), + action="get_resource", + conditions=dict(method=['GET'])) + mapper.connect("attach_resource", + uri_prefix + 'networks/{network_id}/ports/{id}/attachment{.format}', + controller=ports.Controller(), + action="attach_resource", + conditions=dict(method=['PUT'])) + mapper.connect("detach_resource", + uri_prefix + 'networks/{network_id}/ports/{id}/attachment{.format}', + controller=ports.Controller(), + action="detach_resource", + conditions=dict(method=['DELETE'])) + print "AFTER MAPPING" print mapper for route in mapper.matchlist: print "Found route:%s %s" %(route.defaults,route.conditions) - #mapper.resource("port", "ports", controller=ports.Controller(), - # collection=dict(public='GET', private='GET'), - # parent_resource=dict(member_name='network', - # collection_name='networks')) - diff --git a/quantum/api/api_common.py b/quantum/api/api_common.py index b33987b4d..90de50921 100644 --- a/quantum/api/api_common.py +++ b/quantum/api/api_common.py @@ -15,7 +15,54 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + +from webob import exc + +from quantum import manager +from quantum.common import wsgi XML_NS_V01 = 'http://netstack.org/quantum/api/v0.1' XML_NS_V10 = 'http://netstack.org/quantum/api/v1.0' +LOG = logging.getLogger('quantum.api.api_common') + +class QuantumController(wsgi.Controller): + """ Base controller class for Quantum API """ + + def __init__(self, plugin_conf_file=None): + self._setup_network_manager() + super(QuantumController, self).__init__() + + def _parse_request_params(self, req, params): + results = {} + for param in params: + param_name = param['param-name'] + param_value = None + # 1- parse request body + if req.body: + des_body = self._deserialize(req.body, req.best_match_content_type()) + data = des_body and des_body.get(self._resource_name, None) + param_value = data and data.get(param_name, None) + if not param_value: + # 2- parse request headers + # prepend param name with a 'x-' prefix + param_value = req.headers.get("x-" + param_name, None) + # 3- parse request query parameters + if not param_value: + try: + param_value = req.str_GET[param_name] + except KeyError: + #param not found + pass + if not param_value and param['required']: + msg = ("Failed to parse request. " + + "Parameter: %(param_name)s not specified" % locals()) + for line in msg.split('\n'): + LOG.error(line) + raise exc.HTTPBadRequest(msg) + results[param_name]=param_value or param.get('default-value') + return results + + def _setup_network_manager(self): + self.network_manager=manager.QuantumManager().get_manager() diff --git a/quantum/api/faults.py b/quantum/api/faults.py index d61ae79fa..03a33c4b3 100644 --- a/quantum/api/faults.py +++ b/quantum/api/faults.py @@ -28,11 +28,12 @@ class Fault(webob.exc.HTTPException): _fault_names = { 400: "malformedRequest", 401: "unauthorized", - 402: "networkNotFound", - 403: "requestedStateInvalid", - 460: "networkInUse", - 461: "alreadyAttached", - 462: "portInUse", + 420: "networkNotFound", + 421: "networkInUse", + 430: "portNotFound", + 431: "requestedStateInvalid", + 432: "portInUse", + 440: "alreadyAttached", 470: "serviceUnavailable", 471: "pluginFault" } @@ -50,8 +51,8 @@ class Fault(webob.exc.HTTPException): fault_data = { fault_name: { 'code': code, - 'message': self.wrapped_exc.explanation}} - #TODO (salvatore-orlando): place over-limit stuff here + 'message': self.wrapped_exc.explanation, + 'detail': self.wrapped_exc.detail}} # 'code' is an attribute on the fault tag itself metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} default_xmlns = common.XML_NS_V10 @@ -60,3 +61,85 @@ class Fault(webob.exc.HTTPException): self.wrapped_exc.body = serializer.serialize(fault_data, content_type) self.wrapped_exc.content_type = content_type return self.wrapped_exc + +class NetworkNotFound(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server did not find the network specified + in the HTTP request + + code: 420, title: Network not Found + """ + code = 420 + title = 'Network not Found' + explanation = ('Unable to find a network with the specified identifier.') + + +class NetworkInUse(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server could not delete the network as there is + at least an attachment plugged into its ports + + code: 421, title: Network In Use + """ + code = 421 + title = 'Network in Use' + explanation = ('Unable to remove the network: attachments still plugged.') + + +class PortNotFound(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server did not find the port specified + in the HTTP request for a given network + + code: 430, title: Port not Found + """ + code = 430 + title = 'Port not Found' + explanation = ('Unable to find a port with the specified identifier.') + + +class RequestedStateInvalid(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server could not update the port state to + to the request value + + code: 431, title: Requested State Invalid + """ + code = 431 + title = 'Requested State Invalid' + explanation = ('Unable to update port state with specified value.') + + +class PortInUse(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server could not remove o port or attach + a resource to it because there is an attachment plugged into the port + + code: 432, title: PortInUse + """ + code = 432 + title = 'Port in Use' + explanation = ('A resource is currently attached to the logical port') + +class AlreadyAttached(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server refused an attempt to re-attach a resource + already attached to the network + + code: 440, title: AlreadyAttached + """ + code = 440 + title = 'Already Attached' + explanation = ('The resource is already attached to another port') diff --git a/quantum/api/networks.py b/quantum/api/networks.py index 88d56fcb8..cccd48cf1 100644 --- a/quantum/api/networks.py +++ b/quantum/api/networks.py @@ -13,24 +13,19 @@ # License for the specific language governing permissions and limitations # under the License. -import httplib import logging from webob import exc -from xml.dom import minidom -from quantum import manager -from quantum.common import exceptions as exception -from quantum.common import flags -from quantum.common import wsgi +from quantum.api import api_common as common from quantum.api import faults from quantum.api.views import networks as networks_view +from quantum.common import exceptions as exception LOG = logging.getLogger('quantum.api.networks') -FLAGS = flags.FLAGS -class Controller(wsgi.Controller): +class Controller(common.QuantumController): """ Network API controller for Quantum API """ _network_ops_param_list = [{ @@ -41,40 +36,16 @@ class Controller(wsgi.Controller): "application/xml": { "attributes": { "network": ["id","name"], - "link": ["rel", "type", "href"], }, }, } def __init__(self, plugin_conf_file=None): - self._setup_network_manager() + self._resource_name = 'network' super(Controller, self).__init__() - def _parse_request_params(self, req, params): - results = {} - for param in params: - param_name = param['param-name'] - # 1- parse request body - # 2- parse request headers - # prepend param name with a 'x-' prefix - param_value = req.headers.get("x-" + param_name, None) - # 3- parse request query parameters - if not param_value: - param_value = req.str_GET[param_name] - if not param_value and param['required']: - msg = ("Failed to parse request. " + - "Parameter: %(param)s not specified" % locals()) - for line in msg.split('\n'): - LOG.error(line) - raise exc.HTTPBadRequest(msg) - results[param_name]=param_value - return results - - def _setup_network_manager(self): - self.network_manager=manager.QuantumManager().get_manager() - def index(self, req, tenant_id): - """ Returns a list of network names and ids """ + """ Returns a list of network ids """ #TODO: this should be for a given tenant!!! return self._items(req, tenant_id, is_detail=False) @@ -87,7 +58,7 @@ class Controller(wsgi.Controller): return dict(networks=result) def show(self, req, tenant_id, id): - """ Returns network details by network id """ + """ Returns network details for the given network id """ try: network = self.network_manager.get_network_details( tenant_id,id) @@ -95,14 +66,17 @@ class Controller(wsgi.Controller): #build response with details result = builder.build(network, True) return dict(networks=result) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) def create(self, req, tenant_id): """ Creates a new network for a given tenant """ #look for network name in request - req_params = \ - self._parse_request_params(req, self._network_ops_param_list) + try: + req_params = \ + self._parse_request_params(req, self._network_ops_param_list) + except exc.HTTPError as e: + return faults.Fault(e) network = self.network_manager.create_network(tenant_id, req_params['network-name']) builder = networks_view.get_view_builder(req) result = builder.build(network) @@ -111,36 +85,26 @@ class Controller(wsgi.Controller): def update(self, req, tenant_id, id): """ Updates the name for the network with the given id """ try: - network_name = req.headers['x-network-name'] - except KeyError as e: - msg = ("Failed to create network. Got error: %(e)s" % locals()) - for line in msg.split('\n'): - LOG.error(line) - raise exc.HTTPBadRequest(msg) - - network = self.network_manager.rename_network(tenant_id, - id,network_name) - if not network: - raise exc.HTTPNotFound("Network %(id)s could not be found" % locals()) - builder = networks_view.get_view_builder(req) - result = builder.build(network, True) - return dict(networks=result) + req_params = \ + self._parse_request_params(req, self._network_ops_param_list) + except exc.HTTPError as e: + return faults.Fault(e) + try: + network = self.network_manager.rename_network(tenant_id, + id,req_params['network-name']) + builder = networks_view.get_view_builder(req) + result = builder.build(network, True) + return dict(networks=result) + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) def delete(self, req, tenant_id, id): """ Destroys the network with the given id """ try: - network_name = req.headers['x-network-name'] - except KeyError as e: - msg = ("Failed to create network. Got error: %(e)s" % locals()) - for line in msg.split('\n'): - LOG.error(line) - raise exc.HTTPBadRequest(msg) - - network = self.network_manager.delete_network(tenant_id, id) - if not network: - raise exc.HTTPNotFound("Network %(id)s could not be found" % locals()) - - return exc.HTTPAccepted() - - + self.network_manager.delete_network(tenant_id, id) + return exc.HTTPAccepted() + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) + except exception.NetworkInUse as e: + return faults.Fault(faults.NetworkInUse(e)) diff --git a/quantum/api/views/ports.py b/quantum/api/views/ports.py new file mode 100644 index 000000000..2d93a35f6 --- /dev/null +++ b/quantum/api/views/ports.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems +# 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. + + +def get_view_builder(req): + base_url = req.application_url + return ViewBuilder(base_url) + + +class ViewBuilder(object): + + def __init__(self, base_url): + """ + :param base_url: url of the root wsgi application + """ + self.base_url = base_url + + def build(self, port_data, is_detail=False): + """Generic method used to generate a port entity.""" + print "PORT-DATA:%s" %port_data + if is_detail: + port = self._build_detail(port_data) + else: + port = self._build_simple(port_data) + return port + + def _build_simple(self, port_data): + """Return a simple model of a server.""" + return dict(port=dict(id=port_data['port-id'])) + + def _build_detail(self, port_data): + """Return a simple model of a server.""" + return dict(port=dict(id=port_data['port-id'], + state=port_data['port-state'])) diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index 60dde349b..7b9784b92 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -21,10 +21,30 @@ Quantum-type exceptions. SHOULD include dedicated exception logging. """ import logging -import sys -import traceback +class QuantumException(Exception): + """Base Quantum Exception + + Taken from nova.exception.NovaException + To correctly use this class, inherit from it and define + a 'message' property. That message will get printf'd + with the keyword arguments provided to the constructor. + + """ + message = _("An unknown exception occurred.") + + def __init__(self, **kwargs): + try: + self._error_string = self.message % kwargs + + except Exception: + # at least get the core message out if something happened + self._error_string = self.message + + def __str__(self): + return self._error_string + class ProcessExecutionError(IOError): def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, description=None): @@ -49,7 +69,7 @@ class ApiError(Error): super(ApiError, self).__init__('%s: %s' % (code, message)) -class NotFound(Error): +class NotFound(QuantumException): pass @@ -57,6 +77,34 @@ class ClassNotFound(NotFound): message = _("Class %(class_name)s could not be found") +class NetworkNotFound(NotFound): + message = _("Network %(net_id)s could not be found") + + +class PortNotFound(NotFound): + message = _("Port %(port_id)s could not be found " \ + "on network %(net_id)s") + + +class StateInvalid(QuantumException): + message = _("Unsupported port state: %(port_state)s") + + +class NetworkInUse(QuantumException): + message = _("Unable to complete operation on network %(net_id)s. " \ + "There is one or more attachments plugged into its ports.") + + +class PortInUse(QuantumException): + message = _("Unable to complete operation on port %(port_id)s " \ + "for network %(net_id)s. The attachment '%(att_id)s" \ + "is plugged into the logical port.") + +class AlreadyAttached(QuantumException): + message = _("Unable to plug the attachment %(att_id)s into port " \ + "%(port_id)s for network %(net_id)s. The attachment is " \ + "already plugged into port %(att_port_id)s") + class Duplicate(Error): pass diff --git a/quantum/plugins/SamplePlugin.py b/quantum/plugins/SamplePlugin.py index 431f5fbc7..7b43b4c6d 100644 --- a/quantum/plugins/SamplePlugin.py +++ b/quantum/plugins/SamplePlugin.py @@ -15,6 +15,8 @@ # under the License. # @author: Somik Behera, Nicira Networks, Inc. +from quantum.common import exceptions as exc + class QuantumEchoPlugin(object): """ @@ -267,20 +269,70 @@ class FakePlugin(object): in-memory data structures to aid in quantum client/cli/api development """ - def __init__(self): - #add a first sample network on init - self._networks={'001': - { - 'net-id':'001', - 'net-name':'pippotest' - }, - '002': - { - 'net-id':'002', - 'net-name':'cicciotest' - }} - self._net_counter=len(self._networks) + + #static data for networks and ports + _port_dict_1 = { + 1 : {'port-id': 1, + 'port-state': 'DOWN', + 'attachment': None}, + 2 : {'port-id': 2, + 'port-state':'UP', + 'attachment': None} + } + _port_dict_2 = { + 1 : {'port-id': 1, + 'port-state': 'UP', + 'attachment': 'SomeFormOfVIFID'}, + 2 : {'port-id': 2, + 'port-state':'DOWN', + 'attachment': None} + } + _networks={'001': + { + 'net-id':'001', + 'net-name':'pippotest', + 'net-ports': _port_dict_1 + }, + '002': + { + 'net-id':'002', + 'net-name':'cicciotest', + 'net-ports': _port_dict_2 + }} + + def __init__(self): + FakePlugin._net_counter=len(FakePlugin._networks) + + def _get_network(self, tenant_id, network_id): + network = FakePlugin._networks.get(network_id) + if not network: + raise exc.NetworkNotFound(net_id=network_id) + return network + + + def _get_port(self, tenant_id, network_id, port_id): + net = self._get_network(tenant_id, network_id) + port = net['net-ports'].get(int(port_id)) + if not port: + raise exc.PortNotFound(net_id=network_id, port_id=port_id) + return port + + def _validate_port_state(self, port_state): + if port_state.upper() not in ('UP','DOWN'): + raise exc.StateInvalid(port_state=port_state) + return True + + def _validate_attachment(self, tenant_id, network_id, port_id, + remote_interface_id): + network = self._get_network(tenant_id, network_id) + for port in network['net-ports'].values(): + if port['attachment'] == remote_interface_id: + raise exc.AlreadyAttached(net_id = network_id, + port_id = port_id, + att_id = port['attachment'], + att_port_id = port['port-id']) + def get_all_networks(self, tenant_id): """ Returns a dictionary containing all @@ -288,7 +340,7 @@ class FakePlugin(object): the specified tenant. """ print("get_all_networks() called\n") - return self._networks.values() + return FakePlugin._networks.values() def get_network_details(self, tenant_id, net_id): """ @@ -296,38 +348,42 @@ class FakePlugin(object): are attached to the network """ print("get_network_details() called\n") - return self._networks.get(net_id) + return self._get_network(tenant_id, net_id) - def create_network(self, tenant_id, net_name): """ Creates a new Virtual Network, and assigns it a symbolic name. """ print("create_network() called\n") - self._net_counter += 1 - new_net_id=("0" * (3 - len(str(self._net_counter)))) + \ - str(self._net_counter) + FakePlugin._net_counter += 1 + new_net_id=("0" * (3 - len(str(FakePlugin._net_counter)))) + \ + str(FakePlugin._net_counter) print new_net_id new_net_dict={'net-id':new_net_id, - 'net-name':net_name} - self._networks[new_net_id]=new_net_dict + 'net-name':net_name, + 'net-ports': {}} + FakePlugin._networks[new_net_id]=new_net_dict # return network_id of the created network return new_net_dict - def delete_network(self, tenant_id, net_id): """ Deletes the network with the specified network identifier belonging to the specified tenant. """ print("delete_network() called\n") - net = self._networks.get(net_id) + net = FakePlugin._networks.get(net_id) + # Verify that no attachments are plugged into the network if net: - self._networks.pop(net_id) + if net['net-ports']: + for port in net['net-ports'].values(): + if port['attachment']: + raise exc.NetworkInUse(net_id=net_id) + FakePlugin._networks.pop(net_id) return net - return None - + # Network not found + raise exc.NetworkNotFound(net_id=net_id) def rename_network(self, tenant_id, net_id, new_name): """ @@ -335,33 +391,55 @@ class FakePlugin(object): Virtual Network. """ print("rename_network() called\n") - net = self._networks.get(net_id, None) - if net: - net['net-name']=new_name - return net - return None - + net = self._get_network(tenant_id, net_id) + net['net-name']=new_name + return net - #TODO - neeed to update methods from this point onwards def get_all_ports(self, tenant_id, net_id): """ Retrieves all port identifiers belonging to the specified Virtual Network. """ print("get_all_ports() called\n") - port_ids_on_net = ["2", "3", "4"] - return port_ids_on_net - - - def create_port(self, tenant_id, net_id): + network = self._get_network(tenant_id, net_id) + ports_on_net = network['net-ports'].values() + return ports_on_net + + def get_port_details(self, tenant_id, net_id, port_id): + """ + This method allows the user to retrieve a remote interface + that is attached to this particular port. + """ + print("get_port_details() called\n") + return self._get_port(tenant_id, net_id, port_id) + + def create_port(self, tenant_id, net_id, port_state=None): """ Creates a port on the specified Virtual Network. """ print("create_port() called\n") - #return the port id - return 201 - - + net = self._get_network(tenant_id, net_id) + # check port state + # TODO(salvatore-orlando): Validate port state in API? + self._validate_port_state(port_state) + ports = net['net-ports'] + new_port_id = max(ports.keys())+1 + new_port_dict = {'port-id':new_port_id, + 'port-state': port_state, + 'attachment': None} + ports[new_port_id] = new_port_dict + return new_port_dict + + def update_port(self, tenant_id, net_id, port_id, port_state): + """ + Updates the state of a port on the specified Virtual Network. + """ + print("create_port() called\n") + port = self._get_port(tenant_id, net_id, port_id) + self._validate_port_state(port_state) + port['port-state'] = port_state + return port + def delete_port(self, tenant_id, net_id, port_id): """ Deletes a port on a specified Virtual Network, @@ -370,25 +448,39 @@ class FakePlugin(object): is deleted. """ print("delete_port() called\n") - - - def get_port_details(self, tenant_id, net_id, port_id): + net = self._get_network(tenant_id, net_id) + port = self._get_port(tenant_id, net_id, port_id) + if port['attachment']: + raise exc.PortInUse(net_id=net_id,port_id=port_id, + att_id=port['attachment']) + try: + net['net-ports'].pop(int(port_id)) + except KeyError: + raise exc.PortNotFound(net_id=net_id, port_id=port_id) + + def get_interface_details(self, tenant_id, net_id, port_id): """ - This method allows the user to retrieve a remote interface - that is attached to this particular port. + Retrieves the remote interface that is attached at this + particular port. """ - print("get_port_details() called\n") - #returns the remote interface UUID - return "/tenant1/networks/net_id/portid/vif2.1" - - + print("get_interface_details() called\n") + port = self._get_port(tenant_id, net_id, port_id) + return port['attachment'] + def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id): """ Attaches a remote interface to the specified port on the specified Virtual Network. """ print("plug_interface() called\n") - + # Validate attachment + self._validate_attachment(tenant_id, net_id, port_id, + remote_interface_id) + port = self._get_port(tenant_id, net_id, port_id) + if port['attachment']: + raise exc.PortInUse(net_id=net_id,port_id=port_id, + att_id=port['attachment']) + port['attachment'] = remote_interface_id def unplug_interface(self, tenant_id, net_id, port_id): """ @@ -396,18 +488,12 @@ class FakePlugin(object): specified Virtual Network. """ print("unplug_interface() called\n") + port = self._get_port(tenant_id, net_id, port_id) + # TODO(salvatore-orlando): + # Should unplug on port without attachment raise an Error? + port['attachment'] = None - - def get_interface_details(self, tenant_id, net_id, port_id): - """ - Retrieves the remote interface that is attached at this - particular port. - """ - print("get_interface_details() called\n") - #returns the remote interface UUID - return "/tenant1/networks/net_id/portid/vif2.0" - - + #TODO - neeed to update methods from this point onwards def get_all_attached_interfaces(self, tenant_id, net_id): """ Retrieves all remote interfaces that are attached to diff --git a/quantum/quantum_plugin_base.py b/quantum/quantum_plugin_base.py index b84940c70..d90cb5595 100644 --- a/quantum/quantum_plugin_base.py +++ b/quantum/quantum_plugin_base.py @@ -79,12 +79,20 @@ class QuantumPluginBase(object): pass @abstractmethod - def create_port(self, tenant_id, net_id): + def create_port(self, tenant_id, net_id, port_state=None): """ Creates a port on the specified Virtual Network. """ pass + @abstractmethod + def update_port(self, tenant_id, net_id, port_id, port_state): + """ + Updates the state of a specific port on the + specified Virtual Network + """ + pass + @abstractmethod def delete_port(self, tenant_id, net_id, port_id): """ diff --git a/test_scripts/__init__.py b/test_scripts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_scripts/miniclient.py b/test_scripts/miniclient.py new file mode 100644 index 000000000..fb1ebc8fe --- /dev/null +++ b/test_scripts/miniclient.py @@ -0,0 +1,98 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems +# 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 httplib +import socket +import urllib + +class MiniClient(object): + + """A base client class - derived from Glance.BaseClient""" + + action_prefix = '/v0.1/tenants/{tenant_id}' + + def __init__(self, host, port, use_ssl): + """ + Creates a new client to some service. + + :param host: The host where service resides + :param port: The port where service resides + :param use_ssl: Should we use HTTPS? + """ + self.host = host + self.port = port + self.use_ssl = use_ssl + self.connection = None + + def get_connection_type(self): + """ + Returns the proper connection type + """ + if self.use_ssl: + return httplib.HTTPSConnection + else: + return httplib.HTTPConnection + + def do_request(self, tenant, method, action, body=None, + headers=None, params=None): + """ + Connects to the server and issues a request. + Returns the result data, or raises an appropriate exception if + HTTP status code is not 2xx + + :param method: HTTP method ("GET", "POST", "PUT", etc...) + :param body: string of data to send, or None (default) + :param headers: mapping of key/value pairs to add as headers + :param params: dictionary of key/value pairs to add to append + to action + + """ + action = MiniClient.action_prefix + action + action = action.replace('{tenant_id}',tenant) + if type(params) is dict: + action += '?' + urllib.urlencode(params) + + try: + connection_type = self.get_connection_type() + headers = headers or {} + + # Open connection and send request + c = connection_type(self.host, self.port) + c.request(method, action, body, headers) + res = c.getresponse() + status_code = self.get_status_code(res) + if status_code in (httplib.OK, + httplib.CREATED, + httplib.ACCEPTED, + httplib.NO_CONTENT): + return res + else: + raise Exception("Server returned error: %s" % res.read()) + + except (socket.error, IOError), e: + raise Exception("Unable to connect to " + "server. Got error: %s" % e) + + def get_status_code(self, response): + """ + Returns the integer status code from the response, which + can be either a Webob.Response (used in testing) or httplib.Response + """ + if hasattr(response, 'status_int'): + return response.status_int + else: + return response.status \ No newline at end of file diff --git a/test_scripts/tests.py b/test_scripts/tests.py new file mode 100644 index 000000000..589d9da22 --- /dev/null +++ b/test_scripts/tests.py @@ -0,0 +1,150 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems +# 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 gettext + +gettext.install('quantum', unicode=1) + +from miniclient import MiniClient +from quantum.common.wsgi import Serializer + +HOST = '127.0.0.1' +PORT = 9696 +USE_SSL = False +TENANT_ID = 'totore' + +test_network_data = \ + {'network': {'network-name': 'test' }} + +def print_response(res): + content = res.read() + print "Status: %s" %res.status + print "Content: %s" %content + return content + +def test_list_networks_and_ports(format = 'xml'): + client = MiniClient(HOST, PORT, USE_SSL) + print "TEST LIST NETWORKS AND PORTS -- FORMAT:%s" %format + print "----------------------------" + print "--> Step 1 - List All Networks" + res = client.do_request(TENANT_ID,'GET', "/networks." + format) + print_response(res) + print "--> Step 2 - Details for Network 001" + res = client.do_request(TENANT_ID,'GET', "/networks/001." + format) + print_response(res) + print "--> Step 3 - Ports for Network 001" + res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format) + print_response(res) + print "--> Step 4 - Details for Port 1" + res = client.do_request(TENANT_ID,'GET', "/networks/001/ports/1." + format) + print_response(res) + print "COMPLETED" + print "----------------------------" + +def test_create_network(format = 'xml'): + client = MiniClient(HOST, PORT, USE_SSL) + print "TEST CREATE NETWORK -- FORMAT:%s" %format + print "----------------------------" + print "--> Step 1 - Create Network" + content_type = "application/" + format + body = Serializer().serialize(test_network_data, content_type) + res = client.do_request(TENANT_ID,'POST', "/networks." + format, body=body) + print_response(res) + print "--> Step 2 - List All Networks" + res = client.do_request(TENANT_ID,'GET', "/networks." + format) + print_response(res) + print "COMPLETED" + print "----------------------------" + +def test_rename_network(format = 'xml'): + client = MiniClient(HOST, PORT, USE_SSL) + content_type = "application/" + format + print "TEST RENAME NETWORK -- FORMAT:%s" %format + print "----------------------------" + print "--> Step 1 - Retrieve network" + res = client.do_request(TENANT_ID,'GET', "/networks/001." + format) + print_response(res) + print "--> Step 2 - Rename network to 'test_renamed'" + test_network_data['network']['network-name'] = 'test_renamed' + body = Serializer().serialize(test_network_data, content_type) + res = client.do_request(TENANT_ID,'PUT', "/networks/001." + format, body=body) + print_response(res) + print "--> Step 2 - Retrieve network (again)" + res = client.do_request(TENANT_ID,'GET', "/networks/001." + format) + print_response(res) + print "COMPLETED" + print "----------------------------" + +def test_delete_network(format = 'xml'): + client = MiniClient(HOST, PORT, USE_SSL) + content_type = "application/" + format + print "TEST DELETE NETWORK -- FORMAT:%s" %format + print "----------------------------" + print "--> Step 1 - List All Networks" + res = client.do_request(TENANT_ID,'GET', "/networks." + format) + content = print_response(res) + network_data = Serializer().deserialize(content, content_type) + print network_data + net_id = network_data['networks'][0]['id'] + print "--> Step 2 - Delete network %s" %net_id + res = client.do_request(TENANT_ID,'DELETE', + "/networks/" + net_id + "." + format) + print_response(res) + print "--> Step 3 - List All Networks (Again)" + res = client.do_request(TENANT_ID,'GET', "/networks." + format) + print_response(res) + print "COMPLETED" + print "----------------------------" + + +def test_create_port(format = 'xml'): + client = MiniClient(HOST, PORT, USE_SSL) + print "TEST CREATE PORT -- FORMAT:%s" %format + print "----------------------------" + print "--> Step 1 - List Ports for network 001" + res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format) + print_response(res) + print "--> Step 2 - Create Port for network 001" + res = client.do_request(TENANT_ID,'POST', "/networks/001/ports." + format) + print_response(res) + print "--> Step 3 - List Ports for network 001 (again)" + res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format) + print_response(res) + print "COMPLETED" + print "----------------------------" + + +def main(): + test_list_networks_and_ports('xml') + test_list_networks_and_ports('json') + test_create_network('xml') + test_create_network('json') + test_rename_network('xml') + test_rename_network('json') + # NOTE: XML deserializer does not work properly + # disabling XML test - this is NOT a server-side issue + #test_delete_network('xml') + test_delete_network('json') + test_create_port('xml') + test_create_port('json') + + pass + + +# Standard boilerplate to call the main() function. +if __name__ == '__main__': + main() \ No newline at end of file From 75b67e63e2ecf7538772c12babda2ba8fe16118c Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Tue, 31 May 2011 18:15:00 +0100 Subject: [PATCH 13/15] pep8 fixes (1st batch) --- quantum/api/__init__.py | 23 +++++++------- quantum/api/api_common.py | 24 +++++++------- quantum/api/faults.py | 27 +++++++++------- quantum/api/networks.py | 25 ++++++++------- quantum/api/versions.py | 9 +++--- quantum/cli.py | 58 ++++++++++++++++++++-------------- quantum/manager.py | 22 ++++++------- quantum/quantum_plugin_base.py | 10 +++--- quantum/service.py | 21 +++++------- quantum/utils.py | 4 ++- 10 files changed, 118 insertions(+), 105 deletions(-) diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py index 0b459e177..25b66f557 100644 --- a/quantum/api/__init__.py +++ b/quantum/api/__init__.py @@ -48,31 +48,30 @@ class APIRouterV01(wsgi.Router): def _setup_routes(self, mapper): uri_prefix = '/tenants/{tenant_id}/' - mapper.resource('network', - 'networks', + mapper.resource('network', 'networks', controller=networks.Controller(), path_prefix=uri_prefix) - mapper.resource("port", "ports", controller=ports.Controller(), + mapper.resource('port', 'ports', + controller=ports.Controller(), parent_resource=dict(member_name='network', - collection_name= uri_prefix + 'networks')) + collection_name=\ + uri_prefix + 'networks')) mapper.connect("get_resource", - uri_prefix + 'networks/{network_id}/ports/{id}/attachment{.format}', + uri_prefix + 'networks/{network_id}/' \ + 'ports/{id}/attachment{.format}', controller=ports.Controller(), action="get_resource", conditions=dict(method=['GET'])) mapper.connect("attach_resource", - uri_prefix + 'networks/{network_id}/ports/{id}/attachment{.format}', + uri_prefix + 'networks/{network_id}/' \ + 'ports/{id}/attachment{.format}', controller=ports.Controller(), action="attach_resource", conditions=dict(method=['PUT'])) mapper.connect("detach_resource", - uri_prefix + 'networks/{network_id}/ports/{id}/attachment{.format}', + uri_prefix + 'networks/{network_id}/' \ + 'ports/{id}/attachment{.format}', controller=ports.Controller(), action="detach_resource", conditions=dict(method=['DELETE'])) - - print "AFTER MAPPING" - print mapper - for route in mapper.matchlist: - print "Found route:%s %s" %(route.defaults,route.conditions) diff --git a/quantum/api/api_common.py b/quantum/api/api_common.py index 90de50921..df8608df3 100644 --- a/quantum/api/api_common.py +++ b/quantum/api/api_common.py @@ -15,17 +15,18 @@ # License for the specific language governing permissions and limitations # under the License. -import logging +import logging from webob import exc from quantum import manager -from quantum.common import wsgi +from quantum.common import wsgi XML_NS_V01 = 'http://netstack.org/quantum/api/v0.1' XML_NS_V10 = 'http://netstack.org/quantum/api/v1.0' LOG = logging.getLogger('quantum.api.api_common') + class QuantumController(wsgi.Controller): """ Base controller class for Quantum API """ @@ -40,7 +41,8 @@ class QuantumController(wsgi.Controller): param_value = None # 1- parse request body if req.body: - des_body = self._deserialize(req.body, req.best_match_content_type()) + des_body = self._deserialize(req.body, + req.best_match_content_type()) data = des_body and des_body.get(self._resource_name, None) param_value = data and data.get(param_name, None) if not param_value: @@ -52,17 +54,17 @@ class QuantumController(wsgi.Controller): try: param_value = req.str_GET[param_name] except KeyError: - #param not found + #param not found pass - if not param_value and param['required']: + if not param_value and param['required']: msg = ("Failed to parse request. " + - "Parameter: %(param_name)s not specified" % locals()) + "Parameter: %(param_name)s " + + "not specified" % locals()) for line in msg.split('\n'): LOG.error(line) raise exc.HTTPBadRequest(msg) - results[param_name]=param_value or param.get('default-value') - return results - - def _setup_network_manager(self): - self.network_manager=manager.QuantumManager().get_manager() + results[param_name] = param_value or param.get('default-value') + return results + def _setup_network_manager(self): + self.network_manager = manager.QuantumManager().get_manager() diff --git a/quantum/api/faults.py b/quantum/api/faults.py index 03a33c4b3..a10364df1 100644 --- a/quantum/api/faults.py +++ b/quantum/api/faults.py @@ -22,6 +22,7 @@ import webob.exc from quantum.api import api_common as common from quantum.common import wsgi + class Fault(webob.exc.HTTPException): """Error codes for API faults""" @@ -29,7 +30,7 @@ class Fault(webob.exc.HTTPException): 400: "malformedRequest", 401: "unauthorized", 420: "networkNotFound", - 421: "networkInUse", + 421: "networkInUse", 430: "portNotFound", 431: "requestedStateInvalid", 432: "portInUse", @@ -61,20 +62,21 @@ class Fault(webob.exc.HTTPException): self.wrapped_exc.body = serializer.serialize(fault_data, content_type) self.wrapped_exc.content_type = content_type return self.wrapped_exc - + + class NetworkNotFound(webob.exc.HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server did not find the network specified in the HTTP request - + code: 420, title: Network not Found """ code = 420 title = 'Network not Found' explanation = ('Unable to find a network with the specified identifier.') - + class NetworkInUse(webob.exc.HTTPClientError): """ @@ -82,7 +84,7 @@ class NetworkInUse(webob.exc.HTTPClientError): This indicates that the server could not delete the network as there is at least an attachment plugged into its ports - + code: 421, title: Network In Use """ code = 421 @@ -96,27 +98,27 @@ class PortNotFound(webob.exc.HTTPClientError): This indicates that the server did not find the port specified in the HTTP request for a given network - + code: 430, title: Port not Found """ code = 430 title = 'Port not Found' explanation = ('Unable to find a port with the specified identifier.') - + class RequestedStateInvalid(webob.exc.HTTPClientError): """ subclass of :class:`~HTTPClientError` - This indicates that the server could not update the port state to + This indicates that the server could not update the port state to to the request value - + code: 431, title: Requested State Invalid """ code = 431 title = 'Requested State Invalid' explanation = ('Unable to update port state with specified value.') - + class PortInUse(webob.exc.HTTPClientError): """ @@ -124,20 +126,21 @@ class PortInUse(webob.exc.HTTPClientError): This indicates that the server could not remove o port or attach a resource to it because there is an attachment plugged into the port - + code: 432, title: PortInUse """ code = 432 title = 'Port in Use' explanation = ('A resource is currently attached to the logical port') + class AlreadyAttached(webob.exc.HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server refused an attempt to re-attach a resource already attached to the network - + code: 440, title: AlreadyAttached """ code = 440 diff --git a/quantum/api/networks.py b/quantum/api/networks.py index cccd48cf1..98dd5e305 100644 --- a/quantum/api/networks.py +++ b/quantum/api/networks.py @@ -29,13 +29,13 @@ class Controller(common.QuantumController): """ Network API controller for Quantum API """ _network_ops_param_list = [{ - 'param-name': 'network-name', - 'required': True},] - + 'param-name': 'network-name', + 'required': True}, ] + _serialization_metadata = { "application/xml": { "attributes": { - "network": ["id","name"], + "network": ["id", "name"], }, }, } @@ -56,12 +56,12 @@ class Controller(common.QuantumController): result = [builder.build(network, is_detail)['network'] for network in networks] return dict(networks=result) - + def show(self, req, tenant_id, id): """ Returns network details for the given network id """ try: network = self.network_manager.get_network_details( - tenant_id,id) + tenant_id, id) builder = networks_view.get_view_builder(req) #build response with details result = builder.build(network, True) @@ -77,7 +77,8 @@ class Controller(common.QuantumController): self._parse_request_params(req, self._network_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) - network = self.network_manager.create_network(tenant_id, req_params['network-name']) + network = self.network_manager.\ + create_network(tenant_id,req_params['network-name']) builder = networks_view.get_view_builder(req) result = builder.build(network) return dict(networks=result) @@ -89,15 +90,15 @@ class Controller(common.QuantumController): self._parse_request_params(req, self._network_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) - try: + try: network = self.network_manager.rename_network(tenant_id, - id,req_params['network-name']) + id, req_params['network-name']) builder = networks_view.get_view_builder(req) result = builder.build(network, True) return dict(networks=result) except exception.NetworkNotFound as e: - return faults.Fault(faults.NetworkNotFound(e)) + return faults.Fault(faults.NetworkNotFound(e)) def delete(self, req, tenant_id, id): """ Destroys the network with the given id """ @@ -105,6 +106,6 @@ class Controller(common.QuantumController): self.network_manager.delete_network(tenant_id, id) return exc.HTTPAccepted() except exception.NetworkNotFound as e: - return faults.Fault(faults.NetworkNotFound(e)) + return faults.Fault(faults.NetworkNotFound(e)) except exception.NetworkInUse as e: - return faults.Fault(faults.NetworkInUse(e)) + return faults.Fault(faults.NetworkInUse(e)) diff --git a/quantum/api/versions.py b/quantum/api/versions.py index 36cd274d1..18635040a 100644 --- a/quantum/api/versions.py +++ b/quantum/api/versions.py @@ -23,6 +23,7 @@ from quantum.api.views import versions as versions_view LOG = logging.getLogger('quantum.api.versions') + class Versions(wsgi.Application): @webob.dec.wsgify(RequestClass=wsgi.Request) @@ -42,7 +43,6 @@ class Versions(wsgi.Application): builder = versions_view.get_view_builder(req) versions = [builder.build(version) for version in version_objs] response = dict(versions=versions) - LOG.debug("response:%s",response) metadata = { "application/xml": { "attributes": { @@ -51,12 +51,13 @@ class Versions(wsgi.Application): } } } - + content_type = req.best_match_content_type() - body = wsgi.Serializer(metadata=metadata).serialize(response, content_type) + body = wsgi.Serializer(metadata=metadata). \ + serialize(response, content_type) response = webob.Response() response.content_type = content_type response.body = body - return response \ No newline at end of file + return response diff --git a/quantum/cli.py b/quantum/cli.py index 0614f95f2..78f1a6b48 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -20,37 +20,37 @@ import sys from manager import QuantumManager -def usage(): - print "\nUsage:" - print "list_nets " +def usage(): + print "\nUsage:" + print "list_nets " print "create_net " - print "delete_net " + print "delete_net " print "detail_net " print "rename_net " print "list_ports " print "create_port " print "delete_port " - print "detail_port " + print "detail_port " print "plug_iface " print "unplug_iface " print "detail_iface " print "list_iface \n" if len(sys.argv) < 2 or len(sys.argv) > 6: - usage() - exit(1) - + usage() + exit(1) + quantum = QuantumManager() manager = quantum.get_manager() -if sys.argv[1] == "list_nets" and len(sys.argv) == 3: +if sys.argv[1] == "list_nets" and len(sys.argv) == 3: network_on_tenant = manager.get_all_networks(sys.argv[2]) print "Virtual Networks on Tenant:%s\n" % sys.argv[2] for k, v in network_on_tenant.iteritems(): print"\tNetwork ID:%s \n\tNetwork Name:%s \n" % (k, v) elif sys.argv[1] == "create_net" and len(sys.argv) == 4: new_net_id = manager.create_network(sys.argv[2], sys.argv[3]) - print "Created a new Virtual Network with ID:%s\n" % new_net_id + print "Created a new Virtual Network with ID:%s\n" % new_net_id elif sys.argv[1] == "delete_net" and len(sys.argv) == 4: manager.delete_network(sys.argv[2], sys.argv[3]) print "Deleted Virtual Network with ID:%s" % sys.argv[3] @@ -58,7 +58,7 @@ elif sys.argv[1] == "detail_net" and len(sys.argv) == 4: vif_list = manager.get_network_details(sys.argv[2], sys.argv[3]) print "Remote Interfaces on Virtual Network:%s\n" % sys.argv[3] for iface in vif_list: - print "\tRemote interface :%s" % iface + print "\tRemote interface :%s" % iface elif sys.argv[1] == "rename_net" and len(sys.argv) == 5: manager.rename_network(sys.argv[2], sys.argv[3], sys.argv[4]) print "Renamed Virtual Network with ID:%s" % sys.argv[3] @@ -66,33 +66,45 @@ elif sys.argv[1] == "list_ports" and len(sys.argv) == 4: ports = manager.get_all_ports(sys.argv[2], sys.argv[3]) print " Virtual Ports on Virtual Network:%s\n" % sys.argv[3] for port in ports: - print "\tVirtual Port:%s" % port + print "\tVirtual Port:%s" % port elif sys.argv[1] == "create_port" and len(sys.argv) == 4: new_port = manager.create_port(sys.argv[2], sys.argv[3]) - print "Created Virtual Port:%s on Virtual Network:%s" % (new_port, sys.argv[3]) + print "Created Virtual Port:%s " \ + "on Virtual Network:%s" % (new_port, sys.argv[3]) elif sys.argv[1] == "delete_port" and len(sys.argv) == 5: manager.delete_port(sys.argv[2], sys.argv[3], sys.argv[4]) - print "Deleted Virtual Port:%s on Virtual Network:%s" % (sys.argv[3], sys.argv[4]) + print "Deleted Virtual Port:%s " \ + "on Virtual Network:%s" % (sys.argv[3], sys.argv[4]) elif sys.argv[1] == "detail_port" and len(sys.argv) == 5: - port_detail = manager.get_port_details(sys.argv[2], sys.argv[3], sys.argv[4]) - print "Virtual Port:%s on Virtual Network:%s contains remote interface:%s" % (sys.argv[3], sys.argv[4], port_detail) + port_detail = manager.get_port_details(sys.argv[2], + sys.argv[3], sys.argv[4]) + print "Virtual Port:%s on Virtual Network:%s " \ + "contains remote interface:%s" % (sys.argv[3], + sys.argv[4], + port_detail) elif sys.argv[1] == "plug_iface" and len(sys.argv) == 6: manager.plug_interface(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]) - print "Plugged remote interface:%s into Virtual Network:%s" % (sys.argv[5], sys.argv[3]) + print "Plugged remote interface:%s " \ + "into Virtual Network:%s" % (sys.argv[5], sys.argv[3]) elif sys.argv[1] == "unplug_iface" and len(sys.argv) == 5: manager.unplug_interface(sys.argv[2], sys.argv[3], sys.argv[4]) - print "UnPlugged remote interface from Virtual Port:%s Virtual Network:%s" % (sys.argv[4], sys.argv[3]) + print "UnPlugged remote interface " \ + "from Virtual Port:%s Virtual Network:%s" % (sys.argv[4], + sys.argv[3]) elif sys.argv[1] == "detail_iface" and len(sys.argv) == 5: - remote_iface = manager.get_interface_details(sys.argv[2], sys.argv[3], sys.argv[4]) - print "Remote interface on Virtual Port:%s Virtual Network:%s is %s" % (sys.argv[4], sys.argv[3], remote_iface) + remote_iface = manager.get_interface_details(sys.argv[2], + sys.argv[3], sys.argv[4]) + print "Remote interface on Virtual Port:%s " \ + "Virtual Network:%s is %s" % (sys.argv[4], + sys.argv[3], remote_iface) elif sys.argv[1] == "list_iface" and len(sys.argv) == 4: iface_list = manager.get_all_attached_interfaces(sys.argv[2], sys.argv[3]) print "Remote Interfaces on Virtual Network:%s\n" % sys.argv[3] for iface in iface_list: - print "\tRemote interface :%s" % iface + print "\tRemote interface :%s" % iface elif sys.argv[1] == "all" and len(sys.argv) == 2: print "Not Implemented" -else: - print "invalid arguments: %s" % str(sys.argv) +else: + print "invalid arguments: %s" % str(sys.argv) usage() diff --git a/quantum/manager.py b/quantum/manager.py index 69e78561a..dc3f39d75 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -25,29 +25,30 @@ The caller should make sure that QuantumManager is a singleton. """ import gettext gettext.install('quantum', unicode=1) - + from common import utils from quantum_plugin_base import QuantumPluginBase CONFIG_FILE = "quantum/plugins.ini" + class QuantumManager(object): - - def __init__(self,config=CONFIG_FILE): + + def __init__(self,config=CONFIG_FILE): self.configuration_file = CONFIG_FILE plugin_location = utils.getPluginFromConfig(CONFIG_FILE) - print "PLUGIN LOCATION:%s" %plugin_location + print "PLUGIN LOCATION:%s" % plugin_location plugin_klass = utils.import_class(plugin_location) if not issubclass(plugin_klass, QuantumPluginBase): - raise Exception("Configured Quantum plug-in didn't pass compatibility test") + raise Exception("Configured Quantum plug-in " \ + "didn't pass compatibility test") else: - print("Successfully imported Quantum plug-in. All compatibility tests passed\n") + print("Successfully imported Quantum plug-in." \ + "All compatibility tests passed\n") self.plugin = plugin_klass() - - def get_manager(self): - return self.plugin - + def get_manager(self): + return self.plugin # TODO(somik): rmove the main class @@ -61,4 +62,3 @@ def main(): # Standard boilerplate to call the main() function. if __name__ == '__main__': main() - diff --git a/quantum/quantum_plugin_base.py b/quantum/quantum_plugin_base.py index d90cb5595..3d79d3a19 100644 --- a/quantum/quantum_plugin_base.py +++ b/quantum/quantum_plugin_base.py @@ -88,11 +88,11 @@ class QuantumPluginBase(object): @abstractmethod def update_port(self, tenant_id, net_id, port_id, port_state): """ - Updates the state of a specific port on the + Updates the state of a specific port on the specified Virtual Network """ pass - + @abstractmethod def delete_port(self, tenant_id, net_id, port_id): """ @@ -142,13 +142,13 @@ class QuantumPluginBase(object): a particular Virtual Network. """ pass - + @classmethod def __subclasshook__(cls, klass): """ The __subclasshook__ method is a class method that will be called everytime a class is tested - using issubclass(klass, Plugin). + using issubclass(klass, Plugin). In that case, it will check that every method marked with the abstractmethod decorator is provided by the plugin class. @@ -160,5 +160,3 @@ class QuantumPluginBase(object): return NotImplemented return True return NotImplemented - - diff --git a/quantum/service.py b/quantum/service.py index 760263bf9..193725ef3 100644 --- a/quantum/service.py +++ b/quantum/service.py @@ -16,15 +16,14 @@ # under the License. import logging -import json -import routes from quantum.common import config from quantum.common import wsgi from quantum.common import exceptions as exception -from webob import Response + LOG = logging.getLogger('quantum.service') + class WsgiService(object): """Base class for WSGI based services. @@ -60,21 +59,19 @@ class QuantumApiService(WsgiService): message = (_('No paste configuration found for: %s'), app_name) raise exception.Error(message) - print "OPTIONS:%s" %options - print "CONF:%s" %conf # Setup logging early, supplying both the CLI options and the # configuration mapping from the config file # We only update the conf dict for the verbose and debug # flags. Everything else must be set up in the conf file... # Log the options used when starting if we're in debug mode... - + config.setup_logging(options, conf) debug = options.get('debug') or \ - config.get_option(conf, 'debug', + config.get_option(conf, 'debug', type='bool', default=False) verbose = options.get('verbose') or \ - config.get_option(conf, 'verbose', + config.get_option(conf, 'verbose', type='bool', default=False) conf['debug'] = debug conf['verbose'] = verbose @@ -91,7 +88,7 @@ class QuantumApiService(WsgiService): return service -def serve_wsgi(cls, conf=None, options = None, args = None): +def serve_wsgi(cls, conf=None, options = None, args=None): try: service = cls.create(conf, options, args) except Exception: @@ -104,7 +101,6 @@ def serve_wsgi(cls, conf=None, options = None, args = None): def _run_wsgi(app_name, paste_conf, paste_config_file): - print "CICCIO" LOG.info(_('Using paste.deploy config at: %s'), paste_config_file) app = config.load_paste_app(paste_config_file, app_name) if not app: @@ -112,7 +108,6 @@ def _run_wsgi(app_name, paste_conf, paste_config_file): paste_config_file) return server = wsgi.Server() - server.start(app, - int(paste_conf['bind_port']),paste_conf['bind_host']) + server.start(app, + int(paste_conf['bind_port']), paste_conf['bind_host']) return server - diff --git a/quantum/utils.py b/quantum/utils.py index c14135d47..508debb35 100644 --- a/quantum/utils.py +++ b/quantum/utils.py @@ -58,6 +58,7 @@ def import_object(import_str): cls = import_class(import_str) return cls() + def to_primitive(value): if type(value) is type([]) or type(value) is type((None,)): o = [] @@ -77,7 +78,8 @@ def to_primitive(value): return to_primitive(list(value)) else: return value - + + def dumps(value): try: return json.dumps(value) From dafac9726b775106317787ccb2625086732df82e Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Tue, 31 May 2011 18:48:43 +0100 Subject: [PATCH 14/15] Adding ports.py to source control --- quantum/api/ports.py | 183 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 quantum/api/ports.py diff --git a/quantum/api/ports.py b/quantum/api/ports.py new file mode 100644 index 000000000..2b93cdec7 --- /dev/null +++ b/quantum/api/ports.py @@ -0,0 +1,183 @@ +# Copyright 2011 Citrix Systems. +# 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 logging + +from webob import exc + +from quantum.api import api_common as common +from quantum.api import faults +from quantum.api.views import ports as ports_view +from quantum.common import exceptions as exception + +LOG = logging.getLogger('quantum.api.ports') + +class Controller(common.QuantumController): + """ Port API controller for Quantum API """ + + _port_ops_param_list = [{ + 'param-name': 'port-state', + 'default-value': 'DOWN', + 'required': False},] + + + _attachment_ops_param_list = [{ + 'param-name': 'attachment-id', + 'required': True},] + + + _serialization_metadata = { + "application/xml": { + "attributes": { + "port": ["id","state"], + }, + }, + } + + def __init__(self, plugin_conf_file=None): + self._resource_name = 'port' + super(Controller, self).__init__() + + def index(self, req, tenant_id, network_id): + """ Returns a list of port ids for a given network """ + return self._items(req, tenant_id, network_id, is_detail=False) + + def _items(self, req, tenant_id, network_id, is_detail): + """ Returns a list of networks. """ + try : + ports = self.network_manager.get_all_ports(tenant_id, network_id) + builder = ports_view.get_view_builder(req) + result = [builder.build(port, is_detail)['port'] + for port in ports] + return dict(ports=result) + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) + + def show(self, req, tenant_id, network_id, id): + """ Returns port details for given port and network """ + try: + port = self.network_manager.get_port_details( + tenant_id, network_id, id) + builder = ports_view.get_view_builder(req) + #build response with details + result = builder.build(port, True) + return dict(ports=result) + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) + except exception.PortNotFound as e: + return faults.Fault(faults.PortNotFound(e)) + + def create(self, req, tenant_id, network_id): + """ Creates a new port for a given network """ + #look for port state in request + try: + req_params = \ + self._parse_request_params(req, self._port_ops_param_list) + except exc.HTTPError as e: + return faults.Fault(e) + try: + port = self.network_manager.create_port(tenant_id, + network_id, + req_params['port-state']) + builder = ports_view.get_view_builder(req) + result = builder.build(port) + return dict(ports=result) + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) + except exception.StateInvalid as e: + return faults.Fault(faults.RequestedStateInvalid(e)) + + def update(self, req, tenant_id, network_id, id): + """ Updates the state of a port for a given network """ + #look for port state in request + try: + req_params = \ + self._parse_request_params(req, self._port_ops_param_list) + except exc.HTTPError as e: + return faults.Fault(e) + try: + port = self.network_manager.update_port(tenant_id,network_id, id, + req_params['port-state']) + builder = ports_view.get_view_builder(req) + result = builder.build(port, True) + return dict(ports=result) + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) + except exception.PortNotFound as e: + return faults.Fault(faults.PortNotFound(e)) + except exception.StateInvalid as e: + return faults.Fault(faults.RequestedStateInvalid(e)) + + + def delete(self, req, tenant_id, network_id, id): + """ Destroys the port with the given id """ + #look for port state in request + try: + self.network_manager.delete_port(tenant_id, network_id, id) + return exc.HTTPAccepted() + #TODO(salvatore-orlando): Handle portInUse error + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) + except exception.PortNotFound as e: + return faults.Fault(faults.PortNotFound(e)) + except exception.PortInUse as e: + return faults.Fault(faults.PortInUse(e)) + + + def get_resource(self,req,tenant_id, network_id, id): + try: + result = self.network_manager.get_interface_details( + tenant_id, network_id, id) + return dict(attachment=result) + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) + except exception.PortNotFound as e: + return faults.Fault(faults.PortNotFound(e)) + + #TODO - Complete implementation of these APIs + def attach_resource(self,req,tenant_id, network_id, id): + content_type = req.best_match_content_type() + print "Content type:%s" %content_type + try: + req_params = \ + self._parse_request_params(req, + self._attachment_ops_param_list) + except exc.HTTPError as e: + return faults.Fault(e) + try: + self.network_manager.plug_interface(tenant_id, + network_id,id, + req_params['attachment-id']) + return exc.HTTPAccepted() + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) + except exception.PortNotFound as e: + return faults.Fault(faults.PortNotFound(e)) + except exception.PortInUse as e: + return faults.Fault(faults.PortInUse(e)) + except exception.AlreadyAttached as e: + return faults.Fault(faults.AlreadyAttached(e)) + + + #TODO - Complete implementation of these APIs + def detach_resource(self,req,tenant_id, network_id, id): + try: + self.network_manager.unplug_interface(tenant_id, + network_id,id) + return exc.HTTPAccepted() + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) + except exception.PortNotFound as e: + return faults.Fault(faults.PortNotFound(e)) From 6d97d94509d14e237c01cf6b25cdd2ad1cf2ed57 Mon Sep 17 00:00:00 2001 From: Brad Hall Date: Wed, 1 Jun 2011 11:00:15 -0700 Subject: [PATCH 15/15] Copy over miniclient from testscripts and port tests.py to use unittest --- smoketests/__init__.py | 0 smoketests/miniclient.py | 98 +++++++++++++++++++++++++++++ smoketests/tests.py | 133 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 smoketests/__init__.py create mode 100644 smoketests/miniclient.py create mode 100644 smoketests/tests.py diff --git a/smoketests/__init__.py b/smoketests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/smoketests/miniclient.py b/smoketests/miniclient.py new file mode 100644 index 000000000..fb1ebc8fe --- /dev/null +++ b/smoketests/miniclient.py @@ -0,0 +1,98 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems +# 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 httplib +import socket +import urllib + +class MiniClient(object): + + """A base client class - derived from Glance.BaseClient""" + + action_prefix = '/v0.1/tenants/{tenant_id}' + + def __init__(self, host, port, use_ssl): + """ + Creates a new client to some service. + + :param host: The host where service resides + :param port: The port where service resides + :param use_ssl: Should we use HTTPS? + """ + self.host = host + self.port = port + self.use_ssl = use_ssl + self.connection = None + + def get_connection_type(self): + """ + Returns the proper connection type + """ + if self.use_ssl: + return httplib.HTTPSConnection + else: + return httplib.HTTPConnection + + def do_request(self, tenant, method, action, body=None, + headers=None, params=None): + """ + Connects to the server and issues a request. + Returns the result data, or raises an appropriate exception if + HTTP status code is not 2xx + + :param method: HTTP method ("GET", "POST", "PUT", etc...) + :param body: string of data to send, or None (default) + :param headers: mapping of key/value pairs to add as headers + :param params: dictionary of key/value pairs to add to append + to action + + """ + action = MiniClient.action_prefix + action + action = action.replace('{tenant_id}',tenant) + if type(params) is dict: + action += '?' + urllib.urlencode(params) + + try: + connection_type = self.get_connection_type() + headers = headers or {} + + # Open connection and send request + c = connection_type(self.host, self.port) + c.request(method, action, body, headers) + res = c.getresponse() + status_code = self.get_status_code(res) + if status_code in (httplib.OK, + httplib.CREATED, + httplib.ACCEPTED, + httplib.NO_CONTENT): + return res + else: + raise Exception("Server returned error: %s" % res.read()) + + except (socket.error, IOError), e: + raise Exception("Unable to connect to " + "server. Got error: %s" % e) + + def get_status_code(self, response): + """ + Returns the integer status code from the response, which + can be either a Webob.Response (used in testing) or httplib.Response + """ + if hasattr(response, 'status_int'): + return response.status_int + else: + return response.status \ No newline at end of file diff --git a/smoketests/tests.py b/smoketests/tests.py new file mode 100644 index 000000000..f9cd77418 --- /dev/null +++ b/smoketests/tests.py @@ -0,0 +1,133 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems +# Copyright 2011 Nicira Networks +# 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 gettext +import simplejson +import sys +import unittest + +gettext.install('quantum', unicode=1) + +from miniclient import MiniClient +from quantum.common.wsgi import Serializer + +HOST = '127.0.0.1' +PORT = 9696 +USE_SSL = False + +TENANT_ID = 'totore' +FORMAT = "json" + +test_network1_data = \ + {'network': {'network-name': 'test1' }} +test_network2_data = \ + {'network': {'network-name': 'test2' }} + +def print_response(res): + content = res.read() + print "Status: %s" %res.status + print "Content: %s" %content + return content + +class QuantumTest(unittest.TestCase): + def setUp(self): + self.client = MiniClient(HOST, PORT, USE_SSL) + + def create_network(self, data): + content_type = "application/" + FORMAT + body = Serializer().serialize(data, content_type) + res = self.client.do_request(TENANT_ID, 'POST', "/networks." + FORMAT, + body=body) + self.assertEqual(res.status, 200, "bad response: %s" % res.read()) + + def test_listNetworks(self): + self.create_network(test_network1_data) + self.create_network(test_network2_data) + res = self.client.do_request(TENANT_ID,'GET', "/networks." + FORMAT) + self.assertEqual(res.status, 200, "bad response: %s" % res.read()) + + def test_createNetwork(self): + self.create_network(test_network1_data) + + def test_createPort(self): + self.create_network(test_network1_data) + res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT) + resdict = simplejson.loads(res.read()) + for n in resdict["networks"]: + net_id = n["id"] + + # Step 1 - List Ports for network (should not find any) + res = self.client.do_request(TENANT_ID, 'GET', + "/networks/%s/ports.%s" % (net_id, FORMAT)) + self.assertEqual(res.status, 200, "Bad response: %s" % res.read()) + output = res.read() + self.assertTrue(len(output) == 0, + "Found unexpected ports: %s" % output) + + # Step 2 - Create Port for network + res = self.client.do_request(TENANT_ID, 'POST', + "/networks/%s/ports.%s" % (net_id, FORMAT)) + self.assertEqual(res.status, 200, "Bad response: %s" % output) + + # Step 3 - List Ports for network (again); should find one + res = self.client.do_request(TENANT_ID, 'GET', + "/networks/%s/ports.%s" % (net_id, FORMAT)) + output = res.read() + self.assertEqual(res.status, 200, "Bad response: %s" % output) + resdict = simplejson.loads(output) + ids = [] + for p in resdict["ports"]: + ids.append(p["id"]) + self.assertTrue(len(ids) == 1, + "Didn't find expected # of ports (1): %s" % ids) + + def test_renameNetwork(self): + self.create_network(test_network1_data) + res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT) + resdict = simplejson.loads(res.read()) + net_id = resdict["networks"][0]["id"] + + data = test_network1_data.copy() + data['network']['network-name'] = 'test_renamed' + content_type = "application/" + FORMAT + body = Serializer().serialize(data, content_type) + res = self.client.do_request(TENANT_ID, 'PUT', + "/networks/%s.%s" % (net_id, FORMAT), body=body) + resdict = simplejson.loads(res.read()) + self.assertTrue(resdict["networks"]["network"]["id"] == net_id, + "Network_rename: renamed network has a different uuid") + self.assertTrue(resdict["networks"]["network"]["name"] == "test_renamed", + "Network rename didn't take effect") + + def delete_networks(self): + # Remove all the networks created on the tenant + res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT) + resdict = simplejson.loads(res.read()) + for n in resdict["networks"]: + net_id = n["id"] + res = self.client.do_request(TENANT_ID, 'DELETE', + "/networks/" + net_id + "." + FORMAT) + self.assertEqual(res.status, 202) + + def tearDown(self): + self.delete_networks() + +# Standard boilerplate to call the main() function. +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase(QuantumTest) + unittest.TextTestRunner(verbosity=2).run(suite)