]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
First working version of Quantum API
authorSalvatore Orlando <salvatore.orlando@eu.citrix.com>
Mon, 30 May 2011 00:08:46 +0000 (01:08 +0100)
committerSalvatore Orlando <salvatore.orlando@eu.citrix.com>
Mon, 30 May 2011 00:08:46 +0000 (01:08 +0100)
quantum/api/__init__.py
quantum/api/api_common.py
quantum/api/faults.py
quantum/api/networks.py
quantum/api/views/ports.py [new file with mode: 0644]
quantum/common/exceptions.py
quantum/plugins/SamplePlugin.py
quantum/quantum_plugin_base.py
test_scripts/__init__.py [new file with mode: 0644]
test_scripts/miniclient.py [new file with mode: 0644]
test_scripts/tests.py [new file with mode: 0644]

index 1ea601c14e7977470de2b979939fd95019aa7e0b..0b459e1772f124865b28a0cbdb994d533edc47cf 100644 (file)
@@ -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'))
-
index b33987b4d43007bc7254ee466228060924a7590a..90de5092191c82d75ebd4f6171601c7ddb630e57 100644 (file)
 #    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()
 
index d61ae79fa6b77c755db3b48f04b3da109162c0f0..03a33c4b3c64b4ad0c5c38aff015176d13f67b54 100644 (file)
@@ -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')
index 88d56fcb8be2b3cf01c8b680d634094d6a83d92e..cccd48cf1015cb9841d99fb38be145ca29cd3b0e 100644 (file)
 #    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 (file)
index 0000000..2d93a35
--- /dev/null
@@ -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']))
index 60dde349bb6a35231df1d938cf547ae09277b030..7b9784b921c132234c0459a104ccc51d4ca221e8 100644 (file)
@@ -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
 
index 431f5fbc70fbcaa4ec92be36e77d272f5eb0e690..7b43b4c6d44c368f6bd51227f05db65f748c1a10 100644 (file)
@@ -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
     """
+
+    #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):
-        #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)
+        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
index b84940c706c438a468f6684d64b11a94422a85e7..d90cb5595423ced05395743b8cac69a14490dc4a 100644 (file)
@@ -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 (file)
index 0000000..e69de29
diff --git a/test_scripts/miniclient.py b/test_scripts/miniclient.py
new file mode 100644 (file)
index 0000000..fb1ebc8
--- /dev/null
@@ -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 (file)
index 0000000..589d9da
--- /dev/null
@@ -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