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 - diff --git a/bin/quantum b/bin/quantum old mode 100644 new mode 100755 index 16ef7346d..0913c31c0 --- a/bin/quantum +++ b/bin/quantum @@ -19,11 +19,10 @@ # If ../quantum/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... +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]), @@ -32,14 +31,15 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')): sys.path.insert(0, possible_topdir) +gettext.install('quantum', unicode=1) -from quantum.common import wsgi +from quantum import service 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) @@ -52,11 +52,10 @@ if __name__ == '__main__': (options, args) = config.parse_options(oparser) 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() + service = service.serve_wsgi(service.QuantumApiService, + options=options, + args=args) + service.wait() except RuntimeError, e: - sys.exit("ERROR: %s" % e) + sys.exit("ERROR: %s" % e) diff --git a/etc/quantum.conf b/etc/quantum.conf new file mode 100644 index 000000000..ba96a9a27 --- /dev/null +++ b/etc/quantum.conf @@ -0,0 +1,25 @@ +[DEFAULT] +# Show more verbose log output (sets INFO log level output) +verbose = True + +# Show debugging output in logs (sets DEBUG log level output) +debug = True + +# Address to bind the API server +bind_host = 0.0.0.0 + +# Port the bind the API server to +bind_port = 9696 + +[composite:quantum] +use = egg:Paste#urlmap +/: quantumversions +/v0.1: quantumapi + +[app:quantumversions] +paste.app_factory = quantum.api.versions:Versions.factory + +[app:quantumapi] +paste.app_factory = quantum.api:APIRouterV01.factory + + 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 diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py new file mode 100644 index 000000000..25b66f557 --- /dev/null +++ b/quantum/api/__init__.py @@ -0,0 +1,77 @@ +# 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. +# @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.api import ports +from quantum.common import flags +from quantum.common import wsgi + + +LOG = logging.getLogger('quantum.api') +FLAGS = flags.FLAGS + + +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): + + 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'])) diff --git a/quantum/api/api_common.py b/quantum/api/api_common.py new file mode 100644 index 000000000..df8608df3 --- /dev/null +++ b/quantum/api/api_common.py @@ -0,0 +1,70 @@ +# 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. + +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 new file mode 100644 index 000000000..a10364df1 --- /dev/null +++ b/quantum/api/faults.py @@ -0,0 +1,148 @@ +# 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", + 420: "networkNotFound", + 421: "networkInUse", + 430: "portNotFound", + 431: "requestedStateInvalid", + 432: "portInUse", + 440: "alreadyAttached", + 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, + '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 + 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 + + +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 new file mode 100644 index 000000000..98dd5e305 --- /dev/null +++ b/quantum/api/networks.py @@ -0,0 +1,111 @@ +# 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 networks as networks_view +from quantum.common import exceptions as exception + +LOG = logging.getLogger('quantum.api.networks') + + +class Controller(common.QuantumController): + """ Network API controller for Quantum API """ + + _network_ops_param_list = [{ + 'param-name': 'network-name', + 'required': True}, ] + + _serialization_metadata = { + "application/xml": { + "attributes": { + "network": ["id", "name"], + }, + }, + } + + def __init__(self, plugin_conf_file=None): + self._resource_name = 'network' + super(Controller, self).__init__() + + def index(self, req, tenant_id): + """ Returns a list of network ids """ + #TODO: this should be for a given tenant!!! + return self._items(req, tenant_id, is_detail=False) + + def _items(self, req, tenant_id, is_detail): + """ Returns a list of networks. """ + 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 for the given network id """ + try: + 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.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 + 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) + return dict(networks=result) + + def update(self, req, tenant_id, id): + """ Updates the name for the network with the given id """ + try: + 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: + 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/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)) diff --git a/quantum/api/versions.py b/quantum/api/versions.py new file mode 100644 index 000000000..18635040a --- /dev/null +++ b/quantum/api/versions.py @@ -0,0 +1,63 @@ +# 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 logging +import webob.dec + +from quantum.common import wsgi +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) + def __call__(self, req): + """Respond to a request for all Quantum API versions.""" + version_objs = [ + { + "id": "v0.1", + "status": "CURRENT", + }, + { + "id": "v1.0", + "status": "FUTURE", + }, + ] + + builder = versions_view.get_view_builder(req) + versions = [builder.build(version) for version in version_objs] + response = dict(versions=versions) + metadata = { + "application/xml": { + "attributes": { + "version": ["status", "id"], + "link": ["rel", "href"], + } + } + } + + content_type = req.best_match_content_type() + body = wsgi.Serializer(metadata=metadata). \ + serialize(response, content_type) + + response = webob.Response() + response.content_type = content_type + response.body = body + + return response diff --git a/quantum/api/views/__init__.py b/quantum/api/views/__init__.py new file mode 100644 index 000000000..ea9103400 --- /dev/null +++ b/quantum/api/views/__init__.py @@ -0,0 +1,16 @@ +# 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. +# @author: Somik Behera, Nicira Networks, Inc. \ No newline at end of file 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'])) 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/api/views/versions.py b/quantum/api/views/versions.py new file mode 100644 index 000000000..d0145c94a --- /dev/null +++ b/quantum/api/views/versions.py @@ -0,0 +1,59 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import 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, version_data): + """Generic method used to generate a version entity.""" + version = { + "id": version_data["id"], + "status": version_data["status"], + "links": self._build_links(version_data), + } + + return version + + def _build_links(self, version_data): + """Generate a container of links that refer to the provided version.""" + href = self.generate_href(version_data["id"]) + + links = [ + { + "rel": "self", + "href": href, + }, + ] + + return links + + def generate_href(self, version_number): + """Create an url that refers to a specific version_number.""" + return os.path.join(self.base_url, version_number) 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/common/config.py b/quantum/common/config.py index dbbcd260f..cda765078 100644 --- a/quantum/common/config.py +++ b/quantum/common/config.py @@ -31,11 +31,15 @@ import sys from paste import deploy -import quantum.common.exception as exception +from quantum.common import flags +from quantum.common import exceptions as exception DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" +FLAGS = flags.FLAGS +LOG = logging.getLogger('quantum.common.wsgi') + def parse_options(parser, cli_args=None): """ @@ -186,8 +190,8 @@ def find_config_file(options, args): * . * ~.quantum/ * ~ - * /etc/quantum - * /etc + * $FLAGS.state_path/etc/quantum + * $FLAGS.state_path/etc :retval Full path to config file, or None if no config file found """ @@ -204,9 +208,10 @@ def find_config_file(options, args): config_file_dirs = [fix_path(os.getcwd()), fix_path(os.path.join('~', '.quantum')), fix_path('~'), + os.path.join(FLAGS.state_path, 'etc'), + os.path.join(FLAGS.state_path, 'etc','quantum'), '/etc/quantum/', '/etc'] - for cfg_dir in config_file_dirs: cfg_file = os.path.join(cfg_dir, 'quantum.conf') if os.path.exists(cfg_file): @@ -239,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: @@ -250,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. @@ -271,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 - 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... - if debug: - logger = logging.getLogger(app_name) - logger.debug("*" * 80) - logger.debug("Configuration options gathered from config file:") - logger.debug(conf_file) - logger.debug("================================================") - items = dict([(k, v) for k, v in conf.items() - if k not in ('__file__', 'here')]) - for key, value in sorted(items.items()): - logger.debug("%(key)-30s %(value)s" % locals()) - logger.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/exceptions.py b/quantum/common/exceptions.py index c434e736e..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,10 +69,42 @@ class ApiError(Error): super(ApiError, self).__init__('%s: %s' % (code, message)) -class NotFound(Error): +class NotFound(QuantumException): pass +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 @@ -69,6 +121,10 @@ class Invalid(Error): pass +class InvalidContentType(Invalid): + message = _("Invalid content type %(content_type)s.") + + class BadInputError(Exception): """Error resulting from a client sending bad input to a server""" pass diff --git a/quantum/common/flags.py b/quantum/common/flags.py new file mode 100644 index 000000000..947999d0f --- /dev/null +++ b/quantum/common/flags.py @@ -0,0 +1,252 @@ +# 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 os +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 + +DEFINE_string('state_path', os.path.join(os.path.dirname(__file__), '../../'), + "Top-level directory for maintaining quantum's state") + diff --git a/quantum/common/utils.py b/quantum/common/utils.py index 435ec7b87..7ab4615b2 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -29,12 +29,13 @@ import socket import sys import ConfigParser -from common import exceptions +import exceptions as exception +import flags from exceptions import ProcessExecutionError TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" - +FLAGS = flags.FLAGS def int_from_bool_as_string(subject): """ @@ -71,9 +72,11 @@ def import_class(import_str): """Returns a class from a string including module and class""" mod_str, _sep, class_str = import_str.rpartition('.') try: + #mod_str = os.path.join(FLAGS.state_path, mod_str) __import__(mod_str) 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) diff --git a/quantum/common/wsgi.py b/quantum/common/wsgi.py index 6c7caa1dc..4caeab9ab 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. @@ -24,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) @@ -32,6 +35,10 @@ 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') class WritableLogger(object): """A thin wrapper that responds to `write` and logs.""" @@ -110,6 +117,104 @@ class Middleware(object): return self.process_response(response) +class Request(webob.Request): + + def best_match_content_type(self): + """Determine the most acceptable content-type. + + Based on the query extension then the Accept header. + + """ + parts = self.path.rsplit('.', 1) + LOG.debug("Request parts:%s",parts) + if len(parts) > 1: + format = parts[1] + if format in ['json', 'xml']: + return 'application/{0}'.format(parts[1]) + + 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): + allowed_types = ("application/xml", "application/json") + if not "Content-Type" in self.headers: + msg = _("Missing Content-Type") + LOG.debug(msg) + raise webob.exc.HTTPBadRequest(msg) + type = self.content_type + if type in allowed_types: + return type + LOG.debug(_("Wrong Content-Type: %s") % type) + raise webob.exc.HTTPBadRequest("Invalid content type") + + +class Application(object): + """Base WSGI application wrapper. Subclasses need to implement __call__.""" + + @classmethod + def factory(cls, global_config, **local_config): + """Used for paste app factories in paste.deploy config files. + + Any local configuration (that is, values under the [app:APPNAME] + section of the paste config) will be passed into the `__init__` method + as kwargs. + + A hypothetical configuration would look like: + + [app:wadl] + latest_version = 1.3 + paste.app_factory = nova.api.fancy_api:Wadl.factory + + which would result in a call to the `Wadl` class as + + import quantum.api.fancy_api + fancy_api.Wadl(latest_version='1.3') + + You could of course re-implement the `factory` method in subclasses, + but using the kwarg passing it shouldn't be necessary. + + """ + return cls(**local_config) + + def __call__(self, environ, start_response): + r"""Subclasses will probably want to implement __call__ like this: + + @webob.dec.wsgify(RequestClass=Request) + def __call__(self, req): + # Any of the following objects work as responses: + + # Option 1: simple string + res = 'message\n' + + # Option 2: a nicely formatted HTTP exception page + res = exc.HTTPForbidden(detail='Nice try') + + # Option 3: a webob Response object (in case you need to play with + # headers, or you want to be treated like an iterable, or or or) + res = Response(); + res.app_iter = open('somefile') + + # Option 4: any wsgi app to be run next + res = self.application + + # Option 5: you can get a Response object for a wsgi app, too, to + # play with headers etc + res = req.get_response(self.application) + + # You can then just return your response... + return res + # ... or set req.response and return None. + req.response = res + + See the end of http://pythonpaste.org/webob/modules/dec.html + for more info. + + """ + raise NotImplementedError(_('You must implement __call__')) + + class Debug(Middleware): """ Helper class that can be inserted into any WSGI application chain @@ -152,6 +257,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. @@ -169,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 @@ -186,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 @@ -204,92 +317,197 @@ 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. """ + 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'] - 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() + 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) + + 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", {}) - serializer = Serializer(request.environ, _metadata) - return serializer.to_content_type(data) + _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.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, environ, 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.environ = environ - self.metadata = metadata or {} - self._methods = { - 'application/json': self._to_json, - 'application/xml': self._to_xml} - def to_content_type(self, data): """ - Serialize a dictionary into a string. The format of the string - will be decided based on the Content Type requested in self.environ: - by Accept: header, or by URL suffix. + self.metadata = metadata or {} + 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: + raise exception.InvalidContentType(content_type=content_type) + + def serialize(self, data, content_type): + """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. + """ - # FIXME(sirp): for now, supporting json only - #mimetype = 'application/xml' - mimetype = 'application/json' - # TODO(gundlach): determine mimetype from request - return self._methods.get(mimetype, repr)(data) + return self.get_deserialize_handler(content_type)(datastring) + + def get_deserialize_handler(self, content_type): + handlers = { + 'application/json': self._from_json, + 'application/xml': self._from_xml, + } + + try: + return handlers[content_type] + except Exception: + raise exception.InvalidContentType(content_type=content_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. + + """ + 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 def _to_json(self, data): - def sanitizer(obj): - if isinstance(obj, datetime.datetime): - return obj.isoformat() - return obj - - return json.dumps(data, default=sanitizer) + 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) + 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] + 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'): @@ -300,6 +518,17 @@ 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] + 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(): if k in attrs: @@ -307,7 +536,10 @@ class Serializer(object): else: node = self._to_xml_node(doc, metadata, k, v) result.appendChild(node) - else: # atom + 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/manager.py b/quantum/manager.py index a36e9b28f..79a5139d8 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -23,26 +23,33 @@ 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 CONFIG_FILE = "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 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 # Added for temporary testing purposes @@ -55,4 +62,3 @@ def main(): # Standard boilerplate to call the main() function. if __name__ == '__main__': main() - diff --git a/quantum/plugins.ini b/quantum/plugins.ini index 61c7694cd..307d2b48d 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.FakePlugin diff --git a/quantum/plugins/SamplePlugin.py b/quantum/plugins/SamplePlugin.py index 5088b71fa..41dd3271d 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): """ @@ -89,6 +91,12 @@ class QuantumEchoPlugin(object): is deleted. """ print("delete_port() called\n") + + def update_port(self, tenant_id, net_id, port_id, port_state): + """ + Updates the state of a port on the specified Virtual Network. + """ + print("update_port() called\n") def get_port_details(self, tenant_id, net_id, port_id): @@ -113,22 +121,7 @@ class QuantumEchoPlugin(object): 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") - - - 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") + class DummyDataPlugin(object): @@ -202,6 +195,12 @@ class DummyDataPlugin(object): print("create_port() called\n") #return the port id return 201 + + def update_port(self, tenant_id, net_id, port_id, port_state): + """ + Updates the state of a port on the specified Virtual Network. + """ + print("update_port() called\n") def delete_port(self, tenant_id, net_id, port_id): @@ -240,23 +239,226 @@ class DummyDataPlugin(object): 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" +class FakePlugin(object): + """ + FakePlugin is a demo plugin that provides + in-memory data structures to aid in quantum + client/cli/api development + """ + + #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 get_all_attached_interfaces(self, tenant_id, net_id): + 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): """ - Retrieves all remote interfaces that are attached to - a particular Virtual Network. + Returns a dictionary containing all + for + the specified tenant. """ - 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 + print("get_all_networks() called\n") + return FakePlugin._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._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") + 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, + '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 = FakePlugin._networks.get(net_id) + # Verify that no attachments are plugged into the network + if net: + 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 + # Network not found + raise exc.NetworkNotFound(net_id=net_id) + + 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._get_network(tenant_id, net_id) + net['net-name']=new_name + return net + + 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") + 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") + 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, + 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") + 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 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): + """ + Detaches a remote interface from the specified port on the + 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 + \ No newline at end of file diff --git a/quantum/quantum_plugin_base.py b/quantum/quantum_plugin_base.py index b84940c70..0bc156d49 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): """ @@ -119,28 +127,13 @@ class QuantumPluginBase(object): """ pass - @abstractmethod - def get_interface_details(self, tenant_id, net_id, port_id): - """ - Retrieves the remote interface that is attached at this - particular port. - """ - pass - @abstractmethod - def get_all_attached_interfaces(self, tenant_id, net_id): - """ - Retrieves all remote interfaces that are attached to - 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. @@ -152,5 +145,3 @@ class QuantumPluginBase(object): return NotImplemented return True return NotImplemented - - diff --git a/quantum/service.py b/quantum/service.py index 50a8effa3..193725ef3 100644 --- a/quantum/service.py +++ b/quantum/service.py @@ -15,30 +15,99 @@ # License for the specific language governing permissions and limitations # under the License. -import json -import routes -from common import wsgi -from webob import Response +import logging +from quantum.common import config +from quantum.common import wsgi +from quantum.common import exceptions as exception -class NetworkController(wsgi.Controller): - - def version(self, request): - return "Quantum version 0.1" +LOG = logging.getLogger('quantum.service') -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 WsgiService(object): + """Base class for WSGI based services. + + 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() -def app_factory(global_conf, **local_conf): - conf = global_conf.copy() - conf.update(local_conf) - return API(conf) +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) + + # 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 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): + 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 diff --git a/quantum/utils.py b/quantum/utils.py index 284b18443..508debb35 100644 --- a/quantum/utils.py +++ b/quantum/utils.py @@ -35,6 +35,8 @@ import sys import time import types +from common import exceptions as exception + def import_class(import_str): """Returns a class from a string including module and class.""" @@ -55,3 +57,36 @@ 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) 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) 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