From 9f1c2488260868ce0c4a5ed60219c92679829281 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Mon, 30 May 2011 01:08:46 +0100 Subject: [PATCH] First working version of Quantum API --- quantum/api/__init__.py | 56 ++++----- quantum/api/api_common.py | 47 +++++++ quantum/api/faults.py | 97 +++++++++++++-- quantum/api/networks.py | 98 +++++---------- quantum/api/views/ports.py | 48 +++++++ quantum/common/exceptions.py | 54 +++++++- quantum/plugins/SamplePlugin.py | 214 ++++++++++++++++++++++---------- quantum/quantum_plugin_base.py | 10 +- test_scripts/__init__.py | 0 test_scripts/miniclient.py | 98 +++++++++++++++ test_scripts/tests.py | 150 ++++++++++++++++++++++ 11 files changed, 700 insertions(+), 172 deletions(-) create mode 100644 quantum/api/views/ports.py create mode 100644 test_scripts/__init__.py create mode 100644 test_scripts/miniclient.py create mode 100644 test_scripts/tests.py diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py index 1ea601c14e7..0b459e1772f 100644 --- a/quantum/api/__init__.py +++ b/quantum/api/__init__.py @@ -26,6 +26,7 @@ import webob.exc from quantum.api import faults from quantum.api import networks +from quantum.api import ports from quantum.common import flags from quantum.common import wsgi @@ -33,18 +34,6 @@ from quantum.common import wsgi LOG = logging.getLogger('quantum.api') FLAGS = flags.FLAGS -class FaultWrapper(wsgi.Middleware): - """Calls down the middleware stack, making exceptions into faults.""" - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - try: - return req.get_response(self.application) - except Exception as ex: - LOG.exception(_("Caught error: %s"), unicode(ex)) - exc = webob.exc.HTTPInternalServerError(explanation=unicode(ex)) - return faults.Fault(exc) - class APIRouterV01(wsgi.Router): """ @@ -57,26 +46,33 @@ class APIRouterV01(wsgi.Router): super(APIRouterV01, self).__init__(mapper) def _setup_routes(self, mapper): - #server_members = self.server_members - #server_members['action'] = 'POST' - #server_members['pause'] = 'POST' - #server_members['unpause'] = 'POST' - #server_members['diagnostics'] = 'GET' - #server_members['actions'] = 'GET' - #server_members['suspend'] = 'POST' - #server_members['resume'] = 'POST' - #server_members['rescue'] = 'POST' - #server_members['unrescue'] = 'POST' - #server_members['reset_network'] = 'POST' - #server_members['inject_network_info'] = 'POST' - mapper.resource("/tenants/{tenant_id}/network", "/tenants/{tenant_id}/networks", controller=networks.Controller()) + uri_prefix = '/tenants/{tenant_id}/' + mapper.resource('network', + 'networks', + controller=networks.Controller(), + path_prefix=uri_prefix) + mapper.resource("port", "ports", controller=ports.Controller(), + parent_resource=dict(member_name='network', + collection_name= uri_prefix + 'networks')) + + mapper.connect("get_resource", + uri_prefix + 'networks/{network_id}/ports/{id}/attachment{.format}', + controller=ports.Controller(), + action="get_resource", + conditions=dict(method=['GET'])) + mapper.connect("attach_resource", + uri_prefix + 'networks/{network_id}/ports/{id}/attachment{.format}', + controller=ports.Controller(), + action="attach_resource", + conditions=dict(method=['PUT'])) + mapper.connect("detach_resource", + uri_prefix + 'networks/{network_id}/ports/{id}/attachment{.format}', + controller=ports.Controller(), + action="detach_resource", + conditions=dict(method=['DELETE'])) + print "AFTER MAPPING" print mapper for route in mapper.matchlist: print "Found route:%s %s" %(route.defaults,route.conditions) - #mapper.resource("port", "ports", controller=ports.Controller(), - # collection=dict(public='GET', private='GET'), - # parent_resource=dict(member_name='network', - # collection_name='networks')) - diff --git a/quantum/api/api_common.py b/quantum/api/api_common.py index b33987b4d43..90de5092191 100644 --- a/quantum/api/api_common.py +++ b/quantum/api/api_common.py @@ -15,7 +15,54 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + +from webob import exc + +from quantum import manager +from quantum.common import wsgi XML_NS_V01 = 'http://netstack.org/quantum/api/v0.1' XML_NS_V10 = 'http://netstack.org/quantum/api/v1.0' +LOG = logging.getLogger('quantum.api.api_common') + +class QuantumController(wsgi.Controller): + """ Base controller class for Quantum API """ + + def __init__(self, plugin_conf_file=None): + self._setup_network_manager() + super(QuantumController, self).__init__() + + def _parse_request_params(self, req, params): + results = {} + for param in params: + param_name = param['param-name'] + param_value = None + # 1- parse request body + if req.body: + des_body = self._deserialize(req.body, req.best_match_content_type()) + data = des_body and des_body.get(self._resource_name, None) + param_value = data and data.get(param_name, None) + if not param_value: + # 2- parse request headers + # prepend param name with a 'x-' prefix + param_value = req.headers.get("x-" + param_name, None) + # 3- parse request query parameters + if not param_value: + try: + param_value = req.str_GET[param_name] + except KeyError: + #param not found + pass + if not param_value and param['required']: + msg = ("Failed to parse request. " + + "Parameter: %(param_name)s not specified" % locals()) + for line in msg.split('\n'): + LOG.error(line) + raise exc.HTTPBadRequest(msg) + results[param_name]=param_value or param.get('default-value') + return results + + def _setup_network_manager(self): + self.network_manager=manager.QuantumManager().get_manager() diff --git a/quantum/api/faults.py b/quantum/api/faults.py index d61ae79fa6b..03a33c4b3c6 100644 --- a/quantum/api/faults.py +++ b/quantum/api/faults.py @@ -28,11 +28,12 @@ class Fault(webob.exc.HTTPException): _fault_names = { 400: "malformedRequest", 401: "unauthorized", - 402: "networkNotFound", - 403: "requestedStateInvalid", - 460: "networkInUse", - 461: "alreadyAttached", - 462: "portInUse", + 420: "networkNotFound", + 421: "networkInUse", + 430: "portNotFound", + 431: "requestedStateInvalid", + 432: "portInUse", + 440: "alreadyAttached", 470: "serviceUnavailable", 471: "pluginFault" } @@ -50,8 +51,8 @@ class Fault(webob.exc.HTTPException): fault_data = { fault_name: { 'code': code, - 'message': self.wrapped_exc.explanation}} - #TODO (salvatore-orlando): place over-limit stuff here + 'message': self.wrapped_exc.explanation, + 'detail': self.wrapped_exc.detail}} # 'code' is an attribute on the fault tag itself metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} default_xmlns = common.XML_NS_V10 @@ -60,3 +61,85 @@ class Fault(webob.exc.HTTPException): self.wrapped_exc.body = serializer.serialize(fault_data, content_type) self.wrapped_exc.content_type = content_type return self.wrapped_exc + +class NetworkNotFound(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server did not find the network specified + in the HTTP request + + code: 420, title: Network not Found + """ + code = 420 + title = 'Network not Found' + explanation = ('Unable to find a network with the specified identifier.') + + +class NetworkInUse(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server could not delete the network as there is + at least an attachment plugged into its ports + + code: 421, title: Network In Use + """ + code = 421 + title = 'Network in Use' + explanation = ('Unable to remove the network: attachments still plugged.') + + +class PortNotFound(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server did not find the port specified + in the HTTP request for a given network + + code: 430, title: Port not Found + """ + code = 430 + title = 'Port not Found' + explanation = ('Unable to find a port with the specified identifier.') + + +class RequestedStateInvalid(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server could not update the port state to + to the request value + + code: 431, title: Requested State Invalid + """ + code = 431 + title = 'Requested State Invalid' + explanation = ('Unable to update port state with specified value.') + + +class PortInUse(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server could not remove o port or attach + a resource to it because there is an attachment plugged into the port + + code: 432, title: PortInUse + """ + code = 432 + title = 'Port in Use' + explanation = ('A resource is currently attached to the logical port') + +class AlreadyAttached(webob.exc.HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server refused an attempt to re-attach a resource + already attached to the network + + code: 440, title: AlreadyAttached + """ + code = 440 + title = 'Already Attached' + explanation = ('The resource is already attached to another port') diff --git a/quantum/api/networks.py b/quantum/api/networks.py index 88d56fcb8be..cccd48cf101 100644 --- a/quantum/api/networks.py +++ b/quantum/api/networks.py @@ -13,24 +13,19 @@ # License for the specific language governing permissions and limitations # under the License. -import httplib import logging from webob import exc -from xml.dom import minidom -from quantum import manager -from quantum.common import exceptions as exception -from quantum.common import flags -from quantum.common import wsgi +from quantum.api import api_common as common from quantum.api import faults from quantum.api.views import networks as networks_view +from quantum.common import exceptions as exception LOG = logging.getLogger('quantum.api.networks') -FLAGS = flags.FLAGS -class Controller(wsgi.Controller): +class Controller(common.QuantumController): """ Network API controller for Quantum API """ _network_ops_param_list = [{ @@ -41,40 +36,16 @@ class Controller(wsgi.Controller): "application/xml": { "attributes": { "network": ["id","name"], - "link": ["rel", "type", "href"], }, }, } def __init__(self, plugin_conf_file=None): - self._setup_network_manager() + self._resource_name = 'network' super(Controller, self).__init__() - def _parse_request_params(self, req, params): - results = {} - for param in params: - param_name = param['param-name'] - # 1- parse request body - # 2- parse request headers - # prepend param name with a 'x-' prefix - param_value = req.headers.get("x-" + param_name, None) - # 3- parse request query parameters - if not param_value: - param_value = req.str_GET[param_name] - if not param_value and param['required']: - msg = ("Failed to parse request. " + - "Parameter: %(param)s not specified" % locals()) - for line in msg.split('\n'): - LOG.error(line) - raise exc.HTTPBadRequest(msg) - results[param_name]=param_value - return results - - def _setup_network_manager(self): - self.network_manager=manager.QuantumManager().get_manager() - def index(self, req, tenant_id): - """ Returns a list of network names and ids """ + """ Returns a list of network ids """ #TODO: this should be for a given tenant!!! return self._items(req, tenant_id, is_detail=False) @@ -87,7 +58,7 @@ class Controller(wsgi.Controller): return dict(networks=result) def show(self, req, tenant_id, id): - """ Returns network details by network id """ + """ Returns network details for the given network id """ try: network = self.network_manager.get_network_details( tenant_id,id) @@ -95,14 +66,17 @@ class Controller(wsgi.Controller): #build response with details result = builder.build(network, True) return dict(networks=result) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) def create(self, req, tenant_id): """ Creates a new network for a given tenant """ #look for network name in request - req_params = \ - self._parse_request_params(req, self._network_ops_param_list) + try: + req_params = \ + self._parse_request_params(req, self._network_ops_param_list) + except exc.HTTPError as e: + return faults.Fault(e) network = self.network_manager.create_network(tenant_id, req_params['network-name']) builder = networks_view.get_view_builder(req) result = builder.build(network) @@ -111,36 +85,26 @@ class Controller(wsgi.Controller): def update(self, req, tenant_id, id): """ Updates the name for the network with the given id """ try: - network_name = req.headers['x-network-name'] - except KeyError as e: - msg = ("Failed to create network. Got error: %(e)s" % locals()) - for line in msg.split('\n'): - LOG.error(line) - raise exc.HTTPBadRequest(msg) - - network = self.network_manager.rename_network(tenant_id, - id,network_name) - if not network: - raise exc.HTTPNotFound("Network %(id)s could not be found" % locals()) - builder = networks_view.get_view_builder(req) - result = builder.build(network, True) - return dict(networks=result) + req_params = \ + self._parse_request_params(req, self._network_ops_param_list) + except exc.HTTPError as e: + return faults.Fault(e) + try: + network = self.network_manager.rename_network(tenant_id, + id,req_params['network-name']) + builder = networks_view.get_view_builder(req) + result = builder.build(network, True) + return dict(networks=result) + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) def delete(self, req, tenant_id, id): """ Destroys the network with the given id """ try: - network_name = req.headers['x-network-name'] - except KeyError as e: - msg = ("Failed to create network. Got error: %(e)s" % locals()) - for line in msg.split('\n'): - LOG.error(line) - raise exc.HTTPBadRequest(msg) - - network = self.network_manager.delete_network(tenant_id, id) - if not network: - raise exc.HTTPNotFound("Network %(id)s could not be found" % locals()) - - return exc.HTTPAccepted() - - + self.network_manager.delete_network(tenant_id, id) + return exc.HTTPAccepted() + except exception.NetworkNotFound as e: + return faults.Fault(faults.NetworkNotFound(e)) + except exception.NetworkInUse as e: + return faults.Fault(faults.NetworkInUse(e)) diff --git a/quantum/api/views/ports.py b/quantum/api/views/ports.py new file mode 100644 index 00000000000..2d93a35f60b --- /dev/null +++ b/quantum/api/views/ports.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Citrix Systems +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +def get_view_builder(req): + base_url = req.application_url + return ViewBuilder(base_url) + + +class ViewBuilder(object): + + def __init__(self, base_url): + """ + :param base_url: url of the root wsgi application + """ + self.base_url = base_url + + def build(self, port_data, is_detail=False): + """Generic method used to generate a port entity.""" + print "PORT-DATA:%s" %port_data + if is_detail: + port = self._build_detail(port_data) + else: + port = self._build_simple(port_data) + return port + + def _build_simple(self, port_data): + """Return a simple model of a server.""" + return dict(port=dict(id=port_data['port-id'])) + + def _build_detail(self, port_data): + """Return a simple model of a server.""" + return dict(port=dict(id=port_data['port-id'], + state=port_data['port-state'])) diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index 60dde349bb6..7b9784b921c 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -21,10 +21,30 @@ Quantum-type exceptions. SHOULD include dedicated exception logging. """ import logging -import sys -import traceback +class QuantumException(Exception): + """Base Quantum Exception + + Taken from nova.exception.NovaException + To correctly use this class, inherit from it and define + a 'message' property. That message will get printf'd + with the keyword arguments provided to the constructor. + + """ + message = _("An unknown exception occurred.") + + def __init__(self, **kwargs): + try: + self._error_string = self.message % kwargs + + except Exception: + # at least get the core message out if something happened + self._error_string = self.message + + def __str__(self): + return self._error_string + class ProcessExecutionError(IOError): def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, description=None): @@ -49,7 +69,7 @@ class ApiError(Error): super(ApiError, self).__init__('%s: %s' % (code, message)) -class NotFound(Error): +class NotFound(QuantumException): pass @@ -57,6 +77,34 @@ class ClassNotFound(NotFound): message = _("Class %(class_name)s could not be found") +class NetworkNotFound(NotFound): + message = _("Network %(net_id)s could not be found") + + +class PortNotFound(NotFound): + message = _("Port %(port_id)s could not be found " \ + "on network %(net_id)s") + + +class StateInvalid(QuantumException): + message = _("Unsupported port state: %(port_state)s") + + +class NetworkInUse(QuantumException): + message = _("Unable to complete operation on network %(net_id)s. " \ + "There is one or more attachments plugged into its ports.") + + +class PortInUse(QuantumException): + message = _("Unable to complete operation on port %(port_id)s " \ + "for network %(net_id)s. The attachment '%(att_id)s" \ + "is plugged into the logical port.") + +class AlreadyAttached(QuantumException): + message = _("Unable to plug the attachment %(att_id)s into port " \ + "%(port_id)s for network %(net_id)s. The attachment is " \ + "already plugged into port %(att_port_id)s") + class Duplicate(Error): pass diff --git a/quantum/plugins/SamplePlugin.py b/quantum/plugins/SamplePlugin.py index 431f5fbc70f..7b43b4c6d44 100644 --- a/quantum/plugins/SamplePlugin.py +++ b/quantum/plugins/SamplePlugin.py @@ -15,6 +15,8 @@ # under the License. # @author: Somik Behera, Nicira Networks, Inc. +from quantum.common import exceptions as exc + class QuantumEchoPlugin(object): """ @@ -267,20 +269,70 @@ class FakePlugin(object): in-memory data structures to aid in quantum client/cli/api development """ - def __init__(self): - #add a first sample network on init - self._networks={'001': - { - 'net-id':'001', - 'net-name':'pippotest' - }, - '002': - { - 'net-id':'002', - 'net-name':'cicciotest' - }} - self._net_counter=len(self._networks) + + #static data for networks and ports + _port_dict_1 = { + 1 : {'port-id': 1, + 'port-state': 'DOWN', + 'attachment': None}, + 2 : {'port-id': 2, + 'port-state':'UP', + 'attachment': None} + } + _port_dict_2 = { + 1 : {'port-id': 1, + 'port-state': 'UP', + 'attachment': 'SomeFormOfVIFID'}, + 2 : {'port-id': 2, + 'port-state':'DOWN', + 'attachment': None} + } + _networks={'001': + { + 'net-id':'001', + 'net-name':'pippotest', + 'net-ports': _port_dict_1 + }, + '002': + { + 'net-id':'002', + 'net-name':'cicciotest', + 'net-ports': _port_dict_2 + }} + + def __init__(self): + FakePlugin._net_counter=len(FakePlugin._networks) + + def _get_network(self, tenant_id, network_id): + network = FakePlugin._networks.get(network_id) + if not network: + raise exc.NetworkNotFound(net_id=network_id) + return network + + + def _get_port(self, tenant_id, network_id, port_id): + net = self._get_network(tenant_id, network_id) + port = net['net-ports'].get(int(port_id)) + if not port: + raise exc.PortNotFound(net_id=network_id, port_id=port_id) + return port + + def _validate_port_state(self, port_state): + if port_state.upper() not in ('UP','DOWN'): + raise exc.StateInvalid(port_state=port_state) + return True + + def _validate_attachment(self, tenant_id, network_id, port_id, + remote_interface_id): + network = self._get_network(tenant_id, network_id) + for port in network['net-ports'].values(): + if port['attachment'] == remote_interface_id: + raise exc.AlreadyAttached(net_id = network_id, + port_id = port_id, + att_id = port['attachment'], + att_port_id = port['port-id']) + def get_all_networks(self, tenant_id): """ Returns a dictionary containing all @@ -288,7 +340,7 @@ class FakePlugin(object): the specified tenant. """ print("get_all_networks() called\n") - return self._networks.values() + return FakePlugin._networks.values() def get_network_details(self, tenant_id, net_id): """ @@ -296,38 +348,42 @@ class FakePlugin(object): are attached to the network """ print("get_network_details() called\n") - return self._networks.get(net_id) + return self._get_network(tenant_id, net_id) - def create_network(self, tenant_id, net_name): """ Creates a new Virtual Network, and assigns it a symbolic name. """ print("create_network() called\n") - self._net_counter += 1 - new_net_id=("0" * (3 - len(str(self._net_counter)))) + \ - str(self._net_counter) + FakePlugin._net_counter += 1 + new_net_id=("0" * (3 - len(str(FakePlugin._net_counter)))) + \ + str(FakePlugin._net_counter) print new_net_id new_net_dict={'net-id':new_net_id, - 'net-name':net_name} - self._networks[new_net_id]=new_net_dict + 'net-name':net_name, + 'net-ports': {}} + FakePlugin._networks[new_net_id]=new_net_dict # return network_id of the created network return new_net_dict - def delete_network(self, tenant_id, net_id): """ Deletes the network with the specified network identifier belonging to the specified tenant. """ print("delete_network() called\n") - net = self._networks.get(net_id) + net = FakePlugin._networks.get(net_id) + # Verify that no attachments are plugged into the network if net: - self._networks.pop(net_id) + if net['net-ports']: + for port in net['net-ports'].values(): + if port['attachment']: + raise exc.NetworkInUse(net_id=net_id) + FakePlugin._networks.pop(net_id) return net - return None - + # Network not found + raise exc.NetworkNotFound(net_id=net_id) def rename_network(self, tenant_id, net_id, new_name): """ @@ -335,33 +391,55 @@ class FakePlugin(object): Virtual Network. """ print("rename_network() called\n") - net = self._networks.get(net_id, None) - if net: - net['net-name']=new_name - return net - return None - + net = self._get_network(tenant_id, net_id) + net['net-name']=new_name + return net - #TODO - neeed to update methods from this point onwards def get_all_ports(self, tenant_id, net_id): """ Retrieves all port identifiers belonging to the specified Virtual Network. """ print("get_all_ports() called\n") - port_ids_on_net = ["2", "3", "4"] - return port_ids_on_net - - - def create_port(self, tenant_id, net_id): + network = self._get_network(tenant_id, net_id) + ports_on_net = network['net-ports'].values() + return ports_on_net + + def get_port_details(self, tenant_id, net_id, port_id): + """ + This method allows the user to retrieve a remote interface + that is attached to this particular port. + """ + print("get_port_details() called\n") + return self._get_port(tenant_id, net_id, port_id) + + def create_port(self, tenant_id, net_id, port_state=None): """ Creates a port on the specified Virtual Network. """ print("create_port() called\n") - #return the port id - return 201 - - + net = self._get_network(tenant_id, net_id) + # check port state + # TODO(salvatore-orlando): Validate port state in API? + self._validate_port_state(port_state) + ports = net['net-ports'] + new_port_id = max(ports.keys())+1 + new_port_dict = {'port-id':new_port_id, + 'port-state': port_state, + 'attachment': None} + ports[new_port_id] = new_port_dict + return new_port_dict + + def update_port(self, tenant_id, net_id, port_id, port_state): + """ + Updates the state of a port on the specified Virtual Network. + """ + print("create_port() called\n") + port = self._get_port(tenant_id, net_id, port_id) + self._validate_port_state(port_state) + port['port-state'] = port_state + return port + def delete_port(self, tenant_id, net_id, port_id): """ Deletes a port on a specified Virtual Network, @@ -370,25 +448,39 @@ class FakePlugin(object): is deleted. """ print("delete_port() called\n") - - - def get_port_details(self, tenant_id, net_id, port_id): + net = self._get_network(tenant_id, net_id) + port = self._get_port(tenant_id, net_id, port_id) + if port['attachment']: + raise exc.PortInUse(net_id=net_id,port_id=port_id, + att_id=port['attachment']) + try: + net['net-ports'].pop(int(port_id)) + except KeyError: + raise exc.PortNotFound(net_id=net_id, port_id=port_id) + + def get_interface_details(self, tenant_id, net_id, port_id): """ - This method allows the user to retrieve a remote interface - that is attached to this particular port. + Retrieves the remote interface that is attached at this + particular port. """ - print("get_port_details() called\n") - #returns the remote interface UUID - return "/tenant1/networks/net_id/portid/vif2.1" - - + print("get_interface_details() called\n") + port = self._get_port(tenant_id, net_id, port_id) + return port['attachment'] + def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id): """ Attaches a remote interface to the specified port on the specified Virtual Network. """ print("plug_interface() called\n") - + # Validate attachment + self._validate_attachment(tenant_id, net_id, port_id, + remote_interface_id) + port = self._get_port(tenant_id, net_id, port_id) + if port['attachment']: + raise exc.PortInUse(net_id=net_id,port_id=port_id, + att_id=port['attachment']) + port['attachment'] = remote_interface_id def unplug_interface(self, tenant_id, net_id, port_id): """ @@ -396,18 +488,12 @@ class FakePlugin(object): specified Virtual Network. """ print("unplug_interface() called\n") + port = self._get_port(tenant_id, net_id, port_id) + # TODO(salvatore-orlando): + # Should unplug on port without attachment raise an Error? + port['attachment'] = None - - def get_interface_details(self, tenant_id, net_id, port_id): - """ - Retrieves the remote interface that is attached at this - particular port. - """ - print("get_interface_details() called\n") - #returns the remote interface UUID - return "/tenant1/networks/net_id/portid/vif2.0" - - + #TODO - neeed to update methods from this point onwards def get_all_attached_interfaces(self, tenant_id, net_id): """ Retrieves all remote interfaces that are attached to diff --git a/quantum/quantum_plugin_base.py b/quantum/quantum_plugin_base.py index b84940c706c..d90cb559542 100644 --- a/quantum/quantum_plugin_base.py +++ b/quantum/quantum_plugin_base.py @@ -79,12 +79,20 @@ class QuantumPluginBase(object): pass @abstractmethod - def create_port(self, tenant_id, net_id): + def create_port(self, tenant_id, net_id, port_state=None): """ Creates a port on the specified Virtual Network. """ pass + @abstractmethod + def update_port(self, tenant_id, net_id, port_id, port_state): + """ + Updates the state of a specific port on the + specified Virtual Network + """ + pass + @abstractmethod def delete_port(self, tenant_id, net_id, port_id): """ diff --git a/test_scripts/__init__.py b/test_scripts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test_scripts/miniclient.py b/test_scripts/miniclient.py new file mode 100644 index 00000000000..fb1ebc8fec7 --- /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 00000000000..589d9da224c --- /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