First working version of Quantum API

This commit is contained in:
Salvatore Orlando 2011-05-30 01:08:46 +01:00
parent 4d0029e879
commit 9f1c248826
11 changed files with 700 additions and 172 deletions

View 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'))

View File

@ -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()

View 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')

View File

@ -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))

View File

@ -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']))

View 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

View 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
"""
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

View 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):
"""

0
test_scripts/__init__.py Normal file
View File

View File

@ -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

150
test_scripts/tests.py Normal file
View File

@ -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()