Merge trunk
This commit is contained in:
commit
6554f92989
63
README
63
README
@ -84,6 +84,69 @@ $ export TENANT=t1
|
||||
$ PYTHONPATH=. python quantum/cli.py -v create_net $TENANT network1
|
||||
Created a new Virtual Network with ID:e754e7c0-a8eb-40e5-861a-b182d30c3441
|
||||
|
||||
# -- Authentication and Authorization
|
||||
|
||||
Requests to Quantum API are authenticated with the Keystone identity service
|
||||
using a token-based authentication protocol.
|
||||
|
||||
1) Enabling Authentication and Authorization
|
||||
The Keystone identity service is a requirement. It must be installed, although
|
||||
not necessarily on the same machine where Quantum is running; both Keystone's
|
||||
admin API and service API should be running
|
||||
|
||||
Authentication and Authorization middleware should be enabled in the Quantum
|
||||
pipeline. To this aim, uncomment the following line in /etc/quantum.conf:
|
||||
|
||||
pipeline = authN authZ extensions quantumapiapp
|
||||
|
||||
The final step concerns configuring access to Keystone. The following attributes
|
||||
must be specified in the [filter:authN] section of quantum.conf:
|
||||
|
||||
auth_host IP address or host name of the server where Keystone is running
|
||||
auth_port Port where the Keystone Admin API is listening
|
||||
auth_protocol Protocol used for communicating with Keystone (http/https)
|
||||
auth_version Keystone API version (default: 2.0)
|
||||
auth_admin_token Keystone token for administrative access
|
||||
auth_admin_user Keystone user with administrative rights
|
||||
auth_admin_password Password for the user specified with auth_admin_user
|
||||
|
||||
NOTE: aut_admin_token and auth_admin_user/password are exclusive.
|
||||
If both are specified, auth_admin_token has priority.
|
||||
|
||||
2) Authenticating and Authorizing request for Quantum API
|
||||
|
||||
A user should first authenticate with Keystone, supplying user credentials;
|
||||
the Keystone service will return an authentication token, together with
|
||||
informations concerning token expirations and endpoint where that token can
|
||||
be used.
|
||||
|
||||
The authentication token must be included in every request for the Quantum
|
||||
API, in the 'X_AUTH_TOKEN' header. Quantum will look for the authentication
|
||||
token in this header, and validate it with the Keystone service.
|
||||
|
||||
In order to validate authentication tokens, Quantum uses Keystone's
|
||||
administrative API. It therefore requires credentials for an administrative
|
||||
user, which can be specified in Quantum's configuration file
|
||||
(etc/quantum.conf)
|
||||
Either username and password, or an authentication token for an administrative
|
||||
user can be specified in the configuration file:
|
||||
|
||||
- Credentials:
|
||||
|
||||
auth_admin_user = admin
|
||||
auth_admin_password = secrete
|
||||
|
||||
- Admin token:
|
||||
|
||||
auth_admin_token = 9a82c95a-99e9-4c3a-b5ee-199f6ba7ff04
|
||||
|
||||
As of the current release, any user for a tenant is allowed to perform
|
||||
every operation on the networks owned by the tenant itself, except for
|
||||
plugging interfaces. In order to perform such operation, the user must have
|
||||
the Quantum:NetworkAdmin roles. Roles can be configured in Keystone using
|
||||
the administrative API.
|
||||
|
||||
|
||||
# -- Writing your own Quantum plug-in
|
||||
|
||||
If you wish the write your own Quantum plugin, please refer to some concrete as
|
||||
|
5
bin/cli
5
bin/cli
@ -126,6 +126,8 @@ if __name__ == "__main__":
|
||||
action="store_true", default=False, help="turn on verbose logging")
|
||||
parser.add_option("-f", "--logfile", dest="logfile",
|
||||
type="string", default="syslog", help="log file path")
|
||||
parser.add_option("-t", "--token", dest="token",
|
||||
type="string", default=None, help="authentication token")
|
||||
options, args = parser.parse_args()
|
||||
|
||||
if options.verbose:
|
||||
@ -158,7 +160,8 @@ if __name__ == "__main__":
|
||||
LOG.info("Executing command \"%s\" with args: %s" % (cmd, args))
|
||||
|
||||
client = Client(options.host, options.port, options.ssl,
|
||||
args[0], FORMAT)
|
||||
args[0], FORMAT,
|
||||
auth_token=options.token)
|
||||
commands[cmd]["func"](client, *args)
|
||||
|
||||
LOG.info("Command execution completed")
|
||||
|
@ -20,8 +20,25 @@ use = egg:Paste#urlmap
|
||||
/v1.0: quantumapi
|
||||
|
||||
[pipeline:quantumapi]
|
||||
# To enable keystone integration uncomment the following line and
|
||||
# comment the next one
|
||||
#pipeline = authN authZ extensions quantumapiapp
|
||||
pipeline = extensions quantumapiapp
|
||||
|
||||
|
||||
[filter:authN]
|
||||
paste.filter_factory = quantum.common.authentication:filter_factory
|
||||
auth_host = 127.0.0.1
|
||||
auth_port = 5001
|
||||
auth_protocol = http
|
||||
auth_version = 2.0
|
||||
#auth_admin_token = 9a82c95a-99e9-4c3a-b5ee-199f6ba7ff04
|
||||
auth_admin_user = admin
|
||||
auth_admin_password = secrete
|
||||
|
||||
[filter:authZ]
|
||||
paste.filter_factory = quantum.common.authorization:filter_factory
|
||||
|
||||
[filter:extensions]
|
||||
paste.filter_factory = quantum.common.extensions:plugin_aware_extension_middleware_factory
|
||||
|
||||
|
116
extensions/multiport.py
Normal file
116
extensions/multiport.py
Normal file
@ -0,0 +1,116 @@
|
||||
"""
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright 2011 Cisco Systems, Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# @author: Ying Liu, Cisco Systems, Inc.
|
||||
#
|
||||
"""
|
||||
import logging
|
||||
|
||||
from webob import exc
|
||||
|
||||
from quantum.api import api_common as common
|
||||
from quantum.api.views import ports as port_view
|
||||
from quantum.common import extensions
|
||||
from quantum.manager import QuantumManager
|
||||
from quantum.plugins.cisco.common import cisco_exceptions as exception
|
||||
from quantum.plugins.cisco.common import cisco_faults as faults
|
||||
|
||||
LOG = logging.getLogger('quantum.api.multiports')
|
||||
|
||||
|
||||
class Multiport(object):
|
||||
"""extension class multiport"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
""" Returns Ext Resource Name """
|
||||
return "Cisco Multiport"
|
||||
|
||||
@classmethod
|
||||
def get_alias(cls):
|
||||
""" Returns Ext Resource Alias """
|
||||
return "Cisco Multiport"
|
||||
|
||||
@classmethod
|
||||
def get_description(cls):
|
||||
""" Returns Ext Resource Description """
|
||||
return "handle multiple ports in one call"
|
||||
|
||||
@classmethod
|
||||
def get_namespace(cls):
|
||||
""" Returns Ext Resource Namespace """
|
||||
return "http://docs.ciscocloud.com/api/ext/multiport/v1.0"
|
||||
|
||||
@classmethod
|
||||
def get_updated(cls):
|
||||
""" Returns Ext Resource Update Time """
|
||||
return "2011-08-25T13:25:27-06:00"
|
||||
|
||||
@classmethod
|
||||
def get_resources(cls):
|
||||
""" Returns Ext Resources """
|
||||
parent_resource = dict(member_name="tenant",
|
||||
collection_name="extensions/csco/tenants")
|
||||
controller = MultiportController(QuantumManager.get_plugin())
|
||||
return [extensions.ResourceExtension('multiport', controller,
|
||||
parent=parent_resource)]
|
||||
|
||||
|
||||
class MultiportController(common.QuantumController):
|
||||
""" multiport API controller
|
||||
based on QuantumController """
|
||||
|
||||
_multiport_ops_param_list = [{
|
||||
'param-name': 'net_id_list',
|
||||
'required': True}, {
|
||||
'param-name': 'status',
|
||||
'required': True}, {
|
||||
'param-name': 'ports_desc',
|
||||
'required': True}]
|
||||
|
||||
_serialization_metadata = {
|
||||
"application/xml": {
|
||||
"attributes": {
|
||||
"multiport": ["id", "name"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, plugin):
|
||||
self._resource_name = 'multiport'
|
||||
self._plugin = plugin
|
||||
|
||||
# pylint: disable-msg=E1101,W0613
|
||||
def create(self, request, tenant_id):
|
||||
""" Creates a new multiport for a given tenant """
|
||||
try:
|
||||
req_params = \
|
||||
self._parse_request_params(request,
|
||||
self._multiport_ops_param_list)
|
||||
except exc.HTTPError as exp:
|
||||
return faults.Fault(exp)
|
||||
multiports = self._plugin.\
|
||||
create_multiport(tenant_id,
|
||||
req_params['net_id_list'],
|
||||
req_params['status'],
|
||||
req_params['ports_desc'])
|
||||
builder = port_view.get_view_builder(request)
|
||||
result = [builder.build(port)['port']
|
||||
for port in multiports]
|
||||
return dict(ports=result)
|
@ -16,6 +16,7 @@
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import webob
|
||||
|
||||
from webob import exc
|
||||
|
||||
@ -35,28 +36,48 @@ class QuantumController(wsgi.Controller):
|
||||
|
||||
def _parse_request_params(self, req, params):
|
||||
results = {}
|
||||
data = {}
|
||||
# Parameters are expected to be in request body only
|
||||
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)
|
||||
if not data:
|
||||
msg = ("Failed to parse request. Resource: " +
|
||||
self._resource_name + " not found in request body")
|
||||
for line in msg.split('\n'):
|
||||
LOG.error(line)
|
||||
raise exc.HTTPBadRequest(msg)
|
||||
|
||||
for param in params:
|
||||
param_name = param['param-name']
|
||||
param_value = None
|
||||
# Parameters are expected to be in request body only
|
||||
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)
|
||||
if not data:
|
||||
msg = ("Failed to parse request. Resource: " +
|
||||
self._resource_name + " not found in request body")
|
||||
for line in msg.split('\n'):
|
||||
LOG.error(line)
|
||||
raise exc.HTTPBadRequest(msg)
|
||||
param_value = data.get(param_name, None)
|
||||
|
||||
param_value = data.get(param_name, None)
|
||||
# If the parameter wasn't found and it was required, return 400
|
||||
if not param_value and param['required']:
|
||||
if param_value is None and param['required']:
|
||||
msg = ("Failed to parse request. " +
|
||||
"Parameter: " + param_name + " not specified")
|
||||
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 _build_response(self, req, res_data, status_code=200):
|
||||
""" A function which builds an HTTP response
|
||||
given a status code and a dictionary containing
|
||||
the response body to be serialized
|
||||
|
||||
"""
|
||||
content_type = req.best_match_content_type()
|
||||
default_xmlns = self.get_default_xmlns(req)
|
||||
body = self._serialize(res_data, content_type, default_xmlns)
|
||||
|
||||
response = webob.Response()
|
||||
response.status = status_code
|
||||
response.headers['Content-Type'] = content_type
|
||||
response.body = body
|
||||
msg_dict = dict(url=req.url, status=response.status_int)
|
||||
msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
|
||||
LOG.debug(msg)
|
||||
return response
|
||||
|
@ -107,8 +107,10 @@ class Controller(common.QuantumController):
|
||||
request_params['name'])
|
||||
builder = networks_view.get_view_builder(request)
|
||||
result = builder.build(network)['network']
|
||||
#MUST RETURN 202???
|
||||
return dict(network=result)
|
||||
# Wsgi middleware allows us to build the response
|
||||
# before returning the call.
|
||||
# This will allow us to return a 202 status code.
|
||||
return self._build_response(request, dict(network=result), 202)
|
||||
|
||||
def update(self, request, tenant_id, id):
|
||||
""" Updates the name for the network with the given id """
|
||||
|
@ -116,7 +116,10 @@ class Controller(common.QuantumController):
|
||||
request_params['state'])
|
||||
builder = ports_view.get_view_builder(request)
|
||||
result = builder.build(port)['port']
|
||||
return dict(port=result)
|
||||
# Wsgi middleware allows us to build the response
|
||||
# before returning the call.
|
||||
# This will allow us to return a 202 status code.
|
||||
return self._build_response(request, dict(port=result), 202)
|
||||
except exception.NetworkNotFound as e:
|
||||
return faults.Fault(faults.NetworkNotFound(e))
|
||||
except exception.StateInvalid as e:
|
||||
|
@ -34,6 +34,7 @@ EXCEPTIONS = {
|
||||
431: exceptions.StateInvalid,
|
||||
432: exceptions.PortInUseClient,
|
||||
440: exceptions.AlreadyAttachedClient}
|
||||
AUTH_TOKEN_HEADER = "X-Auth-Token"
|
||||
|
||||
|
||||
class ApiCall(object):
|
||||
@ -83,7 +84,8 @@ class Client(object):
|
||||
|
||||
def __init__(self, host="127.0.0.1", port=9696, use_ssl=False, tenant=None,
|
||||
format="xml", testingStub=None, key_file=None, cert_file=None,
|
||||
logger=None, action_prefix="/v1.0/tenants/{tenant_id}"):
|
||||
auth_token=None, logger=None,
|
||||
action_prefix="/v1.0/tenants/{tenant_id}"):
|
||||
"""
|
||||
Creates a new client to some service.
|
||||
|
||||
@ -95,6 +97,9 @@ class Client(object):
|
||||
:param testingStub: A class that stubs basic server methods for tests
|
||||
:param key_file: The SSL key file to use if use_ssl is true
|
||||
:param cert_file: The SSL cert file to use if use_ssl is true
|
||||
:param auth_token: authentication token to be passed to server
|
||||
:param logger: Logger object for the client library
|
||||
:param action_prefix: prefix for request URIs
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
@ -106,6 +111,7 @@ class Client(object):
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
self.logger = logger
|
||||
self.auth_token = auth_token
|
||||
self.action_prefix = action_prefix
|
||||
|
||||
def get_connection_type(self):
|
||||
@ -163,6 +169,9 @@ class Client(object):
|
||||
connection_type = self.get_connection_type()
|
||||
headers = headers or {"Content-Type":
|
||||
"application/%s" % self.format}
|
||||
# if available, add authentication token
|
||||
if self.auth_token:
|
||||
headers[AUTH_TOKEN_HEADER] = self.auth_token
|
||||
# Open connection and send request, handling SSL certs
|
||||
certs = {'key_file': self.key_file, 'cert_file': self.cert_file}
|
||||
certs = dict((x, certs[x]) for x in certs if certs[x] != None)
|
||||
@ -178,7 +187,6 @@ class Client(object):
|
||||
if self.logger:
|
||||
self.logger.debug("Quantum Client Reply (code = %s) :\n %s" \
|
||||
% (str(status_code), data))
|
||||
|
||||
if status_code in (httplib.OK,
|
||||
httplib.CREATED,
|
||||
httplib.ACCEPTED,
|
||||
@ -228,7 +236,7 @@ class Client(object):
|
||||
"""
|
||||
Deserializes a an xml or json string into a dictionary
|
||||
"""
|
||||
if status_code in (202, 204):
|
||||
if status_code == 204:
|
||||
return data
|
||||
return Serializer(self._serialization_metadata).\
|
||||
deserialize(data, self.content_type())
|
||||
|
374
quantum/common/authentication.py
Executable file
374
quantum/common/authentication.py
Executable file
@ -0,0 +1,374 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright (c) 2010-2011 OpenStack, LLC.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
"""
|
||||
TOKEN-BASED AUTH MIDDLEWARE
|
||||
|
||||
This WSGI component performs multiple jobs:
|
||||
- it verifies that incoming client requests have valid tokens by verifying
|
||||
tokens with the auth service.
|
||||
- it will reject unauthenticated requests UNLESS it is in 'delay_auth_decision'
|
||||
mode, which means the final decision is delegated to the downstream WSGI
|
||||
component (usually the OpenStack service)
|
||||
- it will collect and forward identity information from a valid token
|
||||
such as user name, groups, etc...
|
||||
|
||||
Refer to: http://wiki.openstack.org/openstack-authn
|
||||
|
||||
This WSGI component has been derived from Keystone's auth_token
|
||||
middleware module. It contains some specialization for Quantum.
|
||||
|
||||
HEADERS
|
||||
-------
|
||||
Headers starting with HTTP_ is a standard http header
|
||||
Headers starting with HTTP_X is an extended http header
|
||||
|
||||
> Coming in from initial call from client or customer
|
||||
HTTP_X_AUTH_TOKEN : the client token being passed in
|
||||
HTTP_X_STORAGE_TOKEN: the client token being passed in (legacy Rackspace use)
|
||||
to support cloud files
|
||||
> Used for communication between components
|
||||
www-authenticate : only used if this component is being used remotely
|
||||
HTTP_AUTHORIZATION : basic auth password used to validate the connection
|
||||
|
||||
> What we add to the request for use by the OpenStack service
|
||||
HTTP_X_AUTHORIZATION: the client identity being passed in
|
||||
|
||||
"""
|
||||
|
||||
import eventlet
|
||||
from eventlet import wsgi
|
||||
import httplib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from paste.deploy import loadapp
|
||||
from urlparse import urlparse
|
||||
from webob.exc import HTTPUnauthorized, HTTPUseProxy
|
||||
from webob.exc import Request, Response
|
||||
|
||||
from quantum.common.bufferedhttp import http_connect_raw as http_connect
|
||||
|
||||
PROTOCOL_NAME = "Token Authentication"
|
||||
LOG = logging.getLogger('quantum.common.authentication')
|
||||
|
||||
|
||||
class AuthProtocol(object):
|
||||
"""Auth Middleware that handles authenticating client calls"""
|
||||
|
||||
def _init_protocol_common(self, app, conf):
|
||||
""" Common initialization code"""
|
||||
LOG.info("Starting the %s component", PROTOCOL_NAME)
|
||||
|
||||
self.conf = conf
|
||||
self.app = app
|
||||
#if app is set, then we are in a WSGI pipeline and requests get passed
|
||||
# on to app. If it is not set, this component should forward requests
|
||||
|
||||
# where to find the Quantum service (if not in local WSGI chain)
|
||||
# these settings are only used if this component is acting as a proxy
|
||||
# and the OpenSTack service is running remotely
|
||||
if not self.app:
|
||||
self.service_protocol = conf.get('quantum_protocol', 'https')
|
||||
self.service_host = conf.get('quantum_host')
|
||||
self.service_port = int(conf.get('quantum_port'))
|
||||
self.service_url = '%s://%s:%s' % (self.service_protocol,
|
||||
self.service_host,
|
||||
self.service_port)
|
||||
|
||||
# delay_auth_decision means we still allow unauthenticated requests
|
||||
# through and we let the downstream service make the final decision
|
||||
self.delay_auth_decision = int(conf.get('delay_auth_decision', 0))
|
||||
|
||||
def _init_protocol(self, app, conf):
|
||||
""" Protocol specific initialization """
|
||||
|
||||
# where to find the auth service (we use this to validate tokens)
|
||||
self.auth_host = conf.get('auth_host')
|
||||
self.auth_port = int(conf.get('auth_port'))
|
||||
self.auth_protocol = conf.get('auth_protocol', 'https')
|
||||
self.auth_api_version = conf.get('auth_version', '2.0')
|
||||
self.auth_location = "%s://%s:%s" % (self.auth_protocol,
|
||||
self.auth_host,
|
||||
self.auth_port)
|
||||
LOG.debug("AUTH SERVICE LOCATION:%s", self.auth_location)
|
||||
# Credentials used to verify this component with the Auth service since
|
||||
# validating tokens is a priviledged call
|
||||
self.admin_user = conf.get('auth_admin_user')
|
||||
self.admin_password = conf.get('auth_admin_password')
|
||||
self.admin_token = conf.get('auth_admin_token')
|
||||
|
||||
def _build_token_uri(self, claims=None):
|
||||
uri = "/v" + self.auth_api_version + "/tokens" + \
|
||||
(claims and '/' + claims or '')
|
||||
return uri
|
||||
|
||||
def __init__(self, app, conf):
|
||||
""" Common initialization code """
|
||||
#TODO(ziad): maybe we rafactor this into a superclass
|
||||
self._init_protocol_common(app, conf) # Applies to all protocols
|
||||
self._init_protocol(app, conf) # Specific to this protocol
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
""" Handle incoming request. Authenticate. And send downstream. """
|
||||
LOG.debug("entering AuthProtocol.__call__")
|
||||
LOG.debug("start response:%s", start_response)
|
||||
self.start_response = start_response
|
||||
self.env = env
|
||||
|
||||
#Prep headers to forward request to local or remote downstream service
|
||||
self.proxy_headers = env.copy()
|
||||
for header in self.proxy_headers.iterkeys():
|
||||
if header[0:5] == 'HTTP_':
|
||||
self.proxy_headers[header[5:]] = self.proxy_headers[header]
|
||||
del self.proxy_headers[header]
|
||||
|
||||
#Look for authentication claims
|
||||
LOG.debug("Looking for authentication claims")
|
||||
self.claims = self._get_claims(env)
|
||||
if not self.claims:
|
||||
#No claim(s) provided
|
||||
LOG.debug("No claims provided")
|
||||
if self.delay_auth_decision:
|
||||
#Configured to allow downstream service to make final decision.
|
||||
#So mark status as Invalid and forward the request downstream
|
||||
self._decorate_request("X_IDENTITY_STATUS", "Invalid")
|
||||
else:
|
||||
#Respond to client as appropriate for this auth protocol
|
||||
return self._reject_request()
|
||||
else:
|
||||
# this request is presenting claims. Let's validate them
|
||||
LOG.debug("Claims found. Validating.")
|
||||
valid = self._validate_claims(self.claims)
|
||||
if not valid:
|
||||
# Keystone rejected claim
|
||||
if self.delay_auth_decision:
|
||||
# Downstream service will receive call still and decide
|
||||
self._decorate_request("X_IDENTITY_STATUS", "Invalid")
|
||||
else:
|
||||
#Respond to client as appropriate for this auth protocol
|
||||
return self._reject_claims()
|
||||
else:
|
||||
self._decorate_request("X_IDENTITY_STATUS", "Confirmed")
|
||||
|
||||
#Collect information about valid claims
|
||||
if valid:
|
||||
LOG.debug("Validation successful")
|
||||
claims = self._expound_claims()
|
||||
|
||||
# Store authentication data
|
||||
if claims:
|
||||
# TODO(Ziad): add additional details we may need,
|
||||
# like tenant and group info
|
||||
self._decorate_request('X_AUTHORIZATION', "Proxy %s" %
|
||||
claims['user'])
|
||||
self._decorate_request('X_TENANT', claims['tenant'])
|
||||
self._decorate_request('X_USER', claims['user'])
|
||||
if 'group' in claims:
|
||||
self._decorate_request('X_GROUP', claims['group'])
|
||||
if 'roles' in claims and len(claims['roles']) > 0:
|
||||
if claims['roles'] != None:
|
||||
roles = ''
|
||||
for role in claims['roles']:
|
||||
if len(roles) > 0:
|
||||
roles += ','
|
||||
roles += role
|
||||
self._decorate_request('X_ROLE', roles)
|
||||
|
||||
# NOTE(todd): unused
|
||||
self.expanded = True
|
||||
LOG.debug("About to forward request")
|
||||
#Send request downstream
|
||||
return self._forward_request()
|
||||
|
||||
# NOTE(todd): unused
|
||||
# NOTE(salvatore-orlando): temporarily used again
|
||||
def get_admin_auth_token(self, username, password):
|
||||
"""
|
||||
This function gets an admin auth token to be used by this service to
|
||||
validate a user's token. Validate_token is a priviledged call so
|
||||
it needs to be authenticated by a service that is calling it
|
||||
"""
|
||||
headers = {"Content-type": "application/json", "Accept": "text/json"}
|
||||
params = {"passwordCredentials": {"username": username,
|
||||
"password": password}}
|
||||
conn = httplib.HTTPConnection("%s:%s" \
|
||||
% (self.auth_host, self.auth_port))
|
||||
conn.request("POST", self._build_token_uri(), json.dumps(params), \
|
||||
headers=headers)
|
||||
response = conn.getresponse()
|
||||
data = response.read()
|
||||
return data
|
||||
|
||||
def _get_claims(self, env):
|
||||
"""Get claims from request"""
|
||||
claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
|
||||
return claims
|
||||
|
||||
def _reject_request(self):
|
||||
"""Redirect client to auth server"""
|
||||
return HTTPUnauthorized()(self.env, self.start_response)
|
||||
|
||||
def _reject_claims(self):
|
||||
"""Client sent bad claims"""
|
||||
return HTTPUnauthorized()(self.env, self.start_response)
|
||||
|
||||
def _validate_claims(self, claims, retry=False):
|
||||
"""Validate claims, and provide identity information if applicable """
|
||||
|
||||
# Step 1: We need to auth with the keystone service, so get an
|
||||
# admin token
|
||||
#TODO(ziad): Need to properly implement this, where to store creds
|
||||
# for now using token from ini
|
||||
#TODO(salvatore-orlando): Temporarily restoring auth token retrieval,
|
||||
# with credentials in configuration file
|
||||
if not self.admin_token:
|
||||
auth = self.get_admin_auth_token(self.admin_user,
|
||||
self.admin_password)
|
||||
self.admin_token = json.loads(auth)["auth"]["token"]["id"]
|
||||
|
||||
# Step 2: validate the user's token with the auth service
|
||||
# since this is a priviledged op,m we need to auth ourselves
|
||||
# by using an admin token
|
||||
headers = {"Content-type": "application/json",
|
||||
"Accept": "text/json",
|
||||
"X-Auth-Token": self.admin_token}
|
||||
##TODO(ziad):we need to figure out how to auth to keystone
|
||||
#since validate_token is a priviledged call
|
||||
#Khaled's version uses creds to get a token
|
||||
# "X-Auth-Token": admin_token}
|
||||
# we're using a test token from the ini file for now
|
||||
conn = http_connect(self.auth_host, self.auth_port, 'GET',
|
||||
self._build_token_uri(claims), headers=headers)
|
||||
resp = conn.getresponse()
|
||||
conn.close()
|
||||
|
||||
if not str(resp.status).startswith('20'):
|
||||
# Keystone rejected claim
|
||||
# In case a 404 error it might just be that the token has expired
|
||||
# Therefore try and get a new token
|
||||
# of course assuming admin credentials have been specified
|
||||
# Note(salvatore-orlando): the 404 here is not really
|
||||
# what should be returned
|
||||
if self.admin_user and self.admin_password and \
|
||||
not retry and str(resp.status) == '404':
|
||||
LOG.warn("Unable to validate token." +
|
||||
"Admin token possibly expired.")
|
||||
self.admin_token = None
|
||||
return self._validate_claims(claims, True)
|
||||
return False
|
||||
else:
|
||||
#TODO(Ziad): there is an optimization we can do here. We have just
|
||||
#received data from Keystone that we can use instead of making
|
||||
#another call in _expound_claims
|
||||
LOG.info("Claims successfully validated")
|
||||
return True
|
||||
|
||||
def _expound_claims(self):
|
||||
# Valid token. Get user data and put it in to the call
|
||||
# so the downstream service can use it
|
||||
headers = {"Content-type": "application/json",
|
||||
"Accept": "text/json",
|
||||
"X-Auth-Token": self.admin_token}
|
||||
##TODO(ziad):we need to figure out how to auth to keystone
|
||||
#since validate_token is a priviledged call
|
||||
#Khaled's version uses creds to get a token
|
||||
# "X-Auth-Token": admin_token}
|
||||
# we're using a test token from the ini file for now
|
||||
conn = http_connect(self.auth_host, self.auth_port, 'GET',
|
||||
self._build_token_uri(self.claims),
|
||||
headers=headers)
|
||||
resp = conn.getresponse()
|
||||
data = resp.read()
|
||||
conn.close()
|
||||
|
||||
if not str(resp.status).startswith('20'):
|
||||
raise LookupError('Unable to locate claims: %s' % resp.status)
|
||||
|
||||
token_info = json.loads(data)
|
||||
#TODO(Ziad): make this more robust
|
||||
#first_group = token_info['auth']['user']['groups']['group'][0]
|
||||
roles = []
|
||||
role_refs = token_info["auth"]["user"]["roleRefs"]
|
||||
if role_refs != None:
|
||||
for role_ref in role_refs:
|
||||
roles.append(role_ref["roleId"])
|
||||
|
||||
verified_claims = {'user': token_info['auth']['user']['username'],
|
||||
'tenant': token_info['auth']['user']['tenantId'],
|
||||
'roles': roles}
|
||||
|
||||
# TODO(Ziad): removed groups for now
|
||||
# ,'group': '%s/%s' % (first_group['id'],
|
||||
# first_group['tenantId'])}
|
||||
return verified_claims
|
||||
|
||||
def _decorate_request(self, index, value):
|
||||
"""Add headers to request"""
|
||||
self.proxy_headers[index] = value
|
||||
self.env["HTTP_%s" % index] = value
|
||||
|
||||
def _forward_request(self):
|
||||
"""Token/Auth processed & claims added to headers"""
|
||||
#now decide how to pass on the call
|
||||
if self.app:
|
||||
# Pass to downstream WSGI component
|
||||
return self.app(self.env, self.start_response)
|
||||
#.custom_start_response)
|
||||
else:
|
||||
# We are forwarding to a remote service (no downstream WSGI app)
|
||||
req = Request(self.proxy_headers)
|
||||
parsed = urlparse(req.url)
|
||||
conn = http_connect(self.service_host,
|
||||
self.service_port,
|
||||
req.method,
|
||||
parsed.path,
|
||||
self.proxy_headers,
|
||||
ssl=(self.service_protocol == 'https'))
|
||||
resp = conn.getresponse()
|
||||
data = resp.read()
|
||||
#TODO(ziad): use a more sophisticated proxy
|
||||
# we are rewriting the headers now
|
||||
return Response(status=resp.status, body=data)(self.proxy_headers,
|
||||
self.start_response)
|
||||
|
||||
|
||||
def filter_factory(global_conf, **local_conf):
|
||||
"""Returns a WSGI filter app for use with paste.deploy."""
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
||||
def auth_filter(app):
|
||||
return AuthProtocol(app, conf)
|
||||
return auth_filter
|
||||
|
||||
|
||||
def app_factory(global_conf, **local_conf):
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
return AuthProtocol(None, conf)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = loadapp("config:" + \
|
||||
os.path.join(os.path.abspath(os.path.dirname(__file__)),
|
||||
os.pardir,
|
||||
os.pardir,
|
||||
"examples/paste/auth_token.ini"),
|
||||
global_conf={"log_name": "auth_token.log"})
|
||||
wsgi.server(eventlet.listen(('', 8090)), app)
|
107
quantum/common/authorization.py
Normal file
107
quantum/common/authorization.py
Normal file
@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright (c) 2010-2011 OpenStack, LLC.
|
||||
#
|
||||
# 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.
|
||||
|
||||
""" Middleware for authorizing Quantum Operations
|
||||
This is a first and very trivial implementation of a middleware
|
||||
for authorizing requests to Quantum API.
|
||||
It only verifies that the tenant requesting the operation owns the
|
||||
network (and thus the port) on which it is going to operate.
|
||||
It also verifies, if the operation involves an interface, that
|
||||
the tenant owns that interface by querying an API on the service
|
||||
where the interface is defined.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from webob.exc import HTTPUnauthorized, HTTPForbidden
|
||||
|
||||
LOG = logging.getLogger('quantum.common.authorization')
|
||||
TENANT_HEADER = "HTTP_X_TENANT"
|
||||
ROLE_HEADER = "HTTP_X_ROLE"
|
||||
ADMIN_ROLE = "Quantum:NetworkAdmin"
|
||||
|
||||
|
||||
#TODO(salvatore-orlando): This class should extend Middleware class
|
||||
# defined in common/wsgi.py
|
||||
class QuantumAuthorization(object):
|
||||
""" Authorizes an operation before it reaches the API WSGI app"""
|
||||
|
||||
def __init__(self, app, conf):
|
||||
""" Common initialization code """
|
||||
LOG.info("Starting the Authorization component")
|
||||
self.conf = conf
|
||||
self.app = app
|
||||
|
||||
def __call__(self, req, start_response):
|
||||
""" Handle incoming request. Authorize. And send downstream. """
|
||||
LOG.debug("entering QuantumAuthorization.__call__")
|
||||
self.start_response = start_response
|
||||
self.req = req
|
||||
|
||||
# Retrieves TENANT ID from headers as the request
|
||||
# should already have been authenticated with Keystone
|
||||
self.headers = req.copy()
|
||||
LOG.debug("Looking for X_TENANT header")
|
||||
if not TENANT_HEADER in self.headers:
|
||||
# This is bad, very bad
|
||||
return self._reject()
|
||||
LOG.debug("X_TENANT header found:%s", self.headers[TENANT_HEADER])
|
||||
auth_tenant_id = self.headers[TENANT_HEADER]
|
||||
path = self.req['PATH_INFO']
|
||||
parts = path.split('/')
|
||||
LOG.debug("Request parts:%s", parts)
|
||||
#TODO (salvatore-orlando): need bound checking here
|
||||
idx = parts.index('tenants') + 1
|
||||
req_tenant_id = parts[idx]
|
||||
LOG.debug("Tenant ID from request:%s", req_tenant_id)
|
||||
if auth_tenant_id != req_tenant_id:
|
||||
# This is bad, very bad
|
||||
return self._forbid()
|
||||
# Are you trying to operate on an attachment?
|
||||
# If yes, you must be Quantum:NetworkAdmin
|
||||
if parts[len(parts) - 1] == "attachment":
|
||||
LOG.debug("Looking for X_ROLE header")
|
||||
LOG.debug("Headers:%s", self.headers)
|
||||
if not ROLE_HEADER in self.headers:
|
||||
#This is bad as you definetely are not an administrator
|
||||
return self._forbid()
|
||||
LOG.debug("X_ROLE header found:%s", self.headers[ROLE_HEADER])
|
||||
roles = self.headers[ROLE_HEADER].split(',')
|
||||
if not ADMIN_ROLE in roles:
|
||||
# Sorry, you're not and admin
|
||||
return self._forbid()
|
||||
# Okay, authorize it - pass downstream
|
||||
return self.app(self.req, self.start_response)
|
||||
|
||||
def _reject(self):
|
||||
"""Apparently the request has not been authenticated """
|
||||
return HTTPUnauthorized()(self.req, self.start_response)
|
||||
|
||||
def _forbid(self):
|
||||
"""Cannot authorize. Operating on non-owned resources"""
|
||||
return HTTPForbidden()(self.req, self.start_response)
|
||||
|
||||
|
||||
def filter_factory(global_conf, **local_conf):
|
||||
"""Returns a WSGI filter app for use with paste.deploy."""
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
||||
def authz_filter(app):
|
||||
return QuantumAuthorization(app, conf)
|
||||
return authz_filter
|
165
quantum/common/bufferedhttp.py
Normal file
165
quantum/common/bufferedhttp.py
Normal file
@ -0,0 +1,165 @@
|
||||
# Copyright (c) 2010-2011 OpenStack, LLC.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Monkey Patch httplib.HTTPResponse to buffer reads of headers. This can improve
|
||||
performance when making large numbers of small HTTP requests. This module
|
||||
also provides helper functions to make HTTP connections using
|
||||
BufferedHTTPResponse.
|
||||
|
||||
.. warning::
|
||||
|
||||
If you use this, be sure that the libraries you are using do not access
|
||||
the socket directly (xmlrpclib, I'm looking at you :/), and instead
|
||||
make all calls through httplib.
|
||||
"""
|
||||
|
||||
from urllib import quote
|
||||
import logging
|
||||
import time
|
||||
|
||||
from eventlet.green.httplib import CONTINUE, HTTPConnection, HTTPMessage, \
|
||||
HTTPResponse, HTTPSConnection, _UNKNOWN
|
||||
|
||||
|
||||
class BufferedHTTPResponse(HTTPResponse):
|
||||
"""HTTPResponse class that buffers reading of headers"""
|
||||
|
||||
def __init__(self, sock, debuglevel=0, strict=0,
|
||||
method=None): # pragma: no cover
|
||||
self.sock = sock
|
||||
self.fp = sock.makefile('rb')
|
||||
self.debuglevel = debuglevel
|
||||
self.strict = strict
|
||||
self._method = method
|
||||
|
||||
self.msg = None
|
||||
|
||||
# from the Status-Line of the response
|
||||
self.version = _UNKNOWN # HTTP-Version
|
||||
self.status = _UNKNOWN # Status-Code
|
||||
self.reason = _UNKNOWN # Reason-Phrase
|
||||
|
||||
self.chunked = _UNKNOWN # is "chunked" being used?
|
||||
self.chunk_left = _UNKNOWN # bytes left to read in current chunk
|
||||
self.length = _UNKNOWN # number of bytes left in response
|
||||
self.will_close = _UNKNOWN # conn will close at end of response
|
||||
|
||||
def expect_response(self):
|
||||
self.fp = self.sock.makefile('rb', 0)
|
||||
version, status, reason = self._read_status()
|
||||
if status != CONTINUE:
|
||||
self._read_status = lambda: (version, status, reason)
|
||||
self.begin()
|
||||
else:
|
||||
self.status = status
|
||||
self.reason = reason.strip()
|
||||
self.version = 11
|
||||
self.msg = HTTPMessage(self.fp, 0)
|
||||
self.msg.fp = None
|
||||
|
||||
|
||||
class BufferedHTTPConnection(HTTPConnection):
|
||||
"""HTTPConnection class that uses BufferedHTTPResponse"""
|
||||
response_class = BufferedHTTPResponse
|
||||
|
||||
def connect(self):
|
||||
self._connected_time = time.time()
|
||||
return HTTPConnection.connect(self)
|
||||
|
||||
def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0):
|
||||
self._method = method
|
||||
self._path = url
|
||||
return HTTPConnection.putrequest(self, method, url, skip_host,
|
||||
skip_accept_encoding)
|
||||
|
||||
def getexpect(self):
|
||||
response = BufferedHTTPResponse(self.sock, strict=self.strict,
|
||||
method=self._method)
|
||||
response.expect_response()
|
||||
return response
|
||||
|
||||
def getresponse(self):
|
||||
response = HTTPConnection.getresponse(self)
|
||||
logging.debug(("HTTP PERF: %(time).5f seconds to %(method)s "
|
||||
"%(host)s:%(port)s %(path)s)"),
|
||||
{'time': time.time() - self._connected_time, 'method': self._method,
|
||||
'host': self.host, 'port': self.port, 'path': self._path})
|
||||
return response
|
||||
|
||||
|
||||
def http_connect(ipaddr, port, device, partition, method, path,
|
||||
headers=None, query_string=None, ssl=False):
|
||||
"""
|
||||
Helper function to create an HTTPConnection object. If ssl is set True,
|
||||
HTTPSConnection will be used. However, if ssl=False, BufferedHTTPConnection
|
||||
will be used, which is buffered for backend Swift services.
|
||||
|
||||
:param ipaddr: IPv4 address to connect to
|
||||
:param port: port to connect to
|
||||
:param device: device of the node to query
|
||||
:param partition: partition on the device
|
||||
:param method: HTTP method to request ('GET', 'PUT', 'POST', etc.)
|
||||
:param path: request path
|
||||
:param headers: dictionary of headers
|
||||
:param query_string: request query string
|
||||
:param ssl: set True if SSL should be used (default: False)
|
||||
:returns: HTTPConnection object
|
||||
"""
|
||||
if ssl:
|
||||
conn = HTTPSConnection('%s:%s' % (ipaddr, port))
|
||||
else:
|
||||
conn = BufferedHTTPConnection('%s:%s' % (ipaddr, port))
|
||||
path = quote('/' + device + '/' + str(partition) + path)
|
||||
if query_string:
|
||||
path += '?' + query_string
|
||||
conn.path = path
|
||||
conn.putrequest(method, path)
|
||||
if headers:
|
||||
for header, value in headers.iteritems():
|
||||
conn.putheader(header, value)
|
||||
conn.endheaders()
|
||||
return conn
|
||||
|
||||
|
||||
def http_connect_raw(ipaddr, port, method, path, headers=None,
|
||||
query_string=None, ssl=False):
|
||||
"""
|
||||
Helper function to create an HTTPConnection object. If ssl is set True,
|
||||
HTTPSConnection will be used. However, if ssl=False, BufferedHTTPConnection
|
||||
will be used, which is buffered for backend Swift services.
|
||||
|
||||
:param ipaddr: IPv4 address to connect to
|
||||
:param port: port to connect to
|
||||
:param method: HTTP method to request ('GET', 'PUT', 'POST', etc.)
|
||||
:param path: request path
|
||||
:param headers: dictionary of headers
|
||||
:param query_string: request query string
|
||||
:param ssl: set True if SSL should be used (default: False)
|
||||
:returns: HTTPConnection object
|
||||
"""
|
||||
if ssl:
|
||||
conn = HTTPSConnection('%s:%s' % (ipaddr, port))
|
||||
else:
|
||||
conn = BufferedHTTPConnection('%s:%s' % (ipaddr, port))
|
||||
if query_string:
|
||||
path += '?' + query_string
|
||||
conn.path = path
|
||||
conn.putrequest(method, path)
|
||||
if headers:
|
||||
for header, value in headers.iteritems():
|
||||
conn.putheader(header, value)
|
||||
conn.endheaders()
|
||||
return conn
|
@ -170,6 +170,8 @@ def port_create(net_id, state=None):
|
||||
|
||||
|
||||
def port_list(net_id):
|
||||
# confirm network exists
|
||||
network_get(net_id)
|
||||
session = get_session()
|
||||
return session.query(models.Port).\
|
||||
filter_by(network_id=net_id).\
|
||||
|
@ -40,8 +40,7 @@ If you plan to just leverage the plugin framework, you do not need these.)
|
||||
* One or more UCS B200 series blade servers with M81KR VIC (aka
|
||||
Palo adapters) installed.
|
||||
* UCSM 2.0 (Capitola) Build 230 or above.
|
||||
* OpenStack Cactus release installation (additional patch is required,
|
||||
details follow in this document)
|
||||
* OpenStack Diablo D3 or later (should have VIF-driver support)
|
||||
* RHEL 6.1 (as of this writing, UCS only officially supports RHEL, but
|
||||
it should be noted that Ubuntu support is planned in coming releases as well)
|
||||
** Package: python-configobj-4.6.0-3.el6.noarch (or newer)
|
||||
@ -84,11 +83,13 @@ Then run "yum install python-routes".
|
||||
Module Structure:
|
||||
-----------------
|
||||
* quantum/plugins/cisco/ - Contains the L2-Network Plugin Framework
|
||||
/client - CLI module for core and extensions API
|
||||
/common - Modules common to the entire plugin
|
||||
/conf - All configuration files
|
||||
/db - Persistence framework
|
||||
/models - Class(es) which tie the logical abstractions
|
||||
to the physical topology
|
||||
/nova - Scheduler and VIF-driver to be used by Nova
|
||||
/nexus - Nexus-specific modules
|
||||
/segmentation - Implementation of segmentation manager,
|
||||
e.g. VLAN Manager
|
||||
@ -109,20 +110,37 @@ provider = quantum.plugins.cisco.l2network_plugin.L2Network
|
||||
Quantum-aware scheduler by editing the /etc/nova/nova.conf file with the
|
||||
following entries:
|
||||
|
||||
--scheduler_driver=quantum.plugins.cisco.nova.quantum_aware_scheduler.QuantumScheduler
|
||||
--scheduler_driver=quantum.plugins.cisco.nova.quantum_port_aware_scheduler.QuantumPortAwareScheduler
|
||||
--quantum_host=127.0.0.1
|
||||
--quantum_port=9696
|
||||
--libvirt_vif_driver=quantum.plugins.cisco.nova.vifdirect.Libvirt802dot1QbhDriver
|
||||
--libvirt_vif_type=802.1Qbh
|
||||
|
||||
|
||||
4. If you want to turn on support for Cisco Nexus switches:
|
||||
4a. Uncomment the nexus_plugin property in
|
||||
Note: To be able to bring up a VM on a UCS blade, you should first create a
|
||||
port for that VM using the Quantum create port API. VM creation will
|
||||
fail if an unused port is not available. If you have configured your
|
||||
Nova project with more than one network, Nova will attempt to instantiate
|
||||
the VM with one network interface (VIF) per configured network. To provide
|
||||
plugin points for each of these VIFs, you will need to create multiple
|
||||
Quantum ports, one for each of the networks, prior to starting the VM.
|
||||
However, in this case you will need to use the Cisco multiport extension
|
||||
API instead of the Quantum create port API. More details on using the
|
||||
multiport extension follow in the section on multi NIC support.
|
||||
|
||||
4. To support the above configuration, you will need some Quantum modules. It's easiest
|
||||
to copy the entire quantum directory from your quantum installation into:
|
||||
|
||||
/usr/lib/python2.6/site-packages/
|
||||
|
||||
This needs to be done for each nova compute node.
|
||||
|
||||
5. If you want to turn on support for Cisco Nexus switches:
|
||||
5a. Uncomment the nexus_plugin property in
|
||||
quantum/plugins/cisco/conf/plugins.ini to read:
|
||||
|
||||
nexus_plugin=quantum.plugins.cisco.nexus.cisco_nexus_plugin.NexusPlugin
|
||||
|
||||
4b. Enter the relevant configuration in the
|
||||
5b. Enter the relevant configuration in the
|
||||
quantum/plugins/cisco/conf/nexus.ini file. Example:
|
||||
|
||||
[SWITCH]
|
||||
@ -141,7 +159,7 @@ nexus_ssh_port=22
|
||||
[DRIVER]
|
||||
name=quantum.plugins.cisco.nexus.cisco_nexus_network_driver.CiscoNEXUSDriver
|
||||
|
||||
4c. Make sure that SSH host key of the Nexus switch is known to the
|
||||
5c. Make sure that SSH host key of the Nexus switch is known to the
|
||||
host on which you are running the Quantum service. You can do
|
||||
this simply by logging in to your Quantum host as the user that
|
||||
Quantum runs as and SSHing to the switch at least once. If the
|
||||
@ -149,47 +167,45 @@ name=quantum.plugins.cisco.nexus.cisco_nexus_network_driver.CiscoNEXUSDriver
|
||||
clearing of the SSH config on the switch), you may need to repeat
|
||||
this step and remove the old hostkey from ~/.ssh/known_hosts.
|
||||
|
||||
5. Plugin Persistence framework setup:
|
||||
5a. Create quantum_l2network database in mysql with the following command -
|
||||
6. Plugin Persistence framework setup:
|
||||
6a. Create quantum_l2network database in mysql with the following command -
|
||||
|
||||
mysql -u<mysqlusername> -p<mysqlpassword> -e "create database quantum_l2network"
|
||||
|
||||
5b. Enter the quantum_l2network database configuration info in the
|
||||
6b. Enter the quantum_l2network database configuration info in the
|
||||
quantum/plugins/cisco/conf/db_conn.ini file.
|
||||
|
||||
5c. If there is a change in the plugin configuration, service would need
|
||||
6c. If there is a change in the plugin configuration, service would need
|
||||
to be restarted after dropping and re-creating the database using
|
||||
the following commands -
|
||||
|
||||
mysql -u<mysqlusername> -p<mysqlpassword> -e "drop database quantum_l2network"
|
||||
mysql -u<mysqlusername> -p<mysqlpassword> -e "create database quantum_l2network"
|
||||
|
||||
6. Verify that you have the correct credentials for each IP address listed
|
||||
7. Verify that you have the correct credentials for each IP address listed
|
||||
in quantum/plugins/cisco/conf/credentials.ini. Example:
|
||||
|
||||
# Provide the UCSM credentials
|
||||
# Provide the UCSM credentials, create a separte entry for each UCSM used in your system
|
||||
# UCSM IP address, username and password.
|
||||
[10.0.0.2]
|
||||
username=admin
|
||||
password=mySecretPasswordForUCSM
|
||||
|
||||
# Provide the Nova DB credentials.
|
||||
# The IP address should be the same as in nova.ini.
|
||||
[10.0.0.3]
|
||||
username=nova
|
||||
password=mySecretPasswordForNova
|
||||
|
||||
# Provide the Nexus credentials, if you are using Nexus switches.
|
||||
# If not this will be ignored.
|
||||
[10.0.0.1]
|
||||
username=admin
|
||||
password=mySecretPasswordForNexus
|
||||
|
||||
7. Configure the UCS systems' information in your deployment by editing the
|
||||
In general, make sure that every UCSM and Nexus switch used in your system,
|
||||
has a credential entry in the above file. This is required for the system to
|
||||
be able to communicate with those switches.
|
||||
|
||||
8. Configure the UCS systems' information in your deployment by editing the
|
||||
quantum/plugins/cisco/conf/ucs_inventory.ini file. You can configure multiple
|
||||
UCSMs per deployment, multiple chasses per UCSM, and multiple blades per
|
||||
UCSMs per deployment, multiple chassis per UCSM, and multiple blades per
|
||||
chassis. Chassis ID and blade ID can be obtained from the UCSM (they will
|
||||
typically numbers like 1, 2, 3, etc.
|
||||
typically be numbers like 1, 2, 3, etc.)
|
||||
|
||||
[ucsm-1]
|
||||
ip_address = <put_ucsm_ip_address_here>
|
||||
@ -217,11 +233,132 @@ blade_id = <put_blade_id_here>
|
||||
host_name = <put_hostname_here>
|
||||
|
||||
|
||||
8. Start the Quantum service. If something doesn't work, verify that
|
||||
9. Start the Quantum service. If something doesn't work, verify that
|
||||
your configuration of each of the above files hasn't gone a little kaka.
|
||||
Once you've put right what once went wrong, leap on.
|
||||
|
||||
|
||||
Multi NIC support for VMs
|
||||
-------------------------
|
||||
As indicated earlier, if your Nova setup has a project with more than one network,
|
||||
Nova will try to create a vritual network interface (VIF) on the VM for each of those
|
||||
networks. That implies that,
|
||||
|
||||
(1) You should create the same number of networks in Quantum as in your Nova
|
||||
project.
|
||||
|
||||
(2) Before each VM is instantiated, you should create Quantum ports on each of those
|
||||
networks. These ports need to be created using the following rest call:
|
||||
|
||||
POST /v1.0/extensions/csco/tenants/{tenant_id}/multiport/
|
||||
|
||||
with request body:
|
||||
|
||||
{'multiport':
|
||||
{'status': 'ACTIVE',
|
||||
'net_id_list': net_id_list,
|
||||
'ports_desc': {'key': 'value'}}}
|
||||
|
||||
where,
|
||||
|
||||
net_id_list is a list of network IDs: [netid1, netid2, ...]. The "ports_desc" dictionary
|
||||
is reserved for later use. For now, the same structure in terms of the dictionary name, key
|
||||
and value should be used.
|
||||
|
||||
The corresponding CLI for this operation is as follows:
|
||||
|
||||
PYTHONPATH=. python quantum/plugins/cisco/client/cli.py create_multiport <tenant_id> <net_id1,net_id2,...>
|
||||
|
||||
(Note that you should not be using the create port core API in the above case.)
|
||||
|
||||
|
||||
Using the Command Line Client to work with this Plugin
|
||||
------------------------------------------------------
|
||||
A command line client is packaged with this plugin. This module can be used
|
||||
to invoke the core API as well as the extensions API, so that you don't have
|
||||
to switch between different CLI modules (it internally invokes the Quantum
|
||||
CLI module for the core APIs to ensure consistency when using either). This
|
||||
command line client can be invoked as follows:
|
||||
|
||||
PYTHONPATH=. python quantum/plugins/cisco/client/cli.py
|
||||
|
||||
1. Creating the network
|
||||
|
||||
# PYTHONPATH=. python quantum/plugins/cisco/client/cli.py create_net -H 10.10.2.6 demo net1
|
||||
Created a new Virtual Network with ID: c4a2bea7-a528-4caf-b16e-80397cd1663a
|
||||
for Tenant demo
|
||||
|
||||
|
||||
2. Listing the networks
|
||||
|
||||
# PYTHONPATH=. python quantum/plugins/cisco/client/cli.py list_nets -H 10.10.2.6 demo
|
||||
Virtual Networks for Tenant demo
|
||||
Network ID: 0e85e924-6ef6-40c1-9f7a-3520ac6888b3
|
||||
Network ID: c4a2bea7-a528-4caf-b16e-80397cd1663a
|
||||
|
||||
|
||||
3. Creating one port on each of the networks
|
||||
|
||||
# PYTHONPATH=. python quantum/plugins/cisco/client/cli.py create_multiport -H 10.10.2.6 demo c4a2bea7-a528-4caf-b16e-80397cd1663a,0e85e924-6ef6-40c1-9f7a-3520ac6888b3
|
||||
Created ports: {u'ports': [{u'id': u'118ac473-294d-480e-8f6d-425acbbe81ae'}, {u'id': u'996e84b8-2ed3-40cf-be75-de17ff1214c4'}]}
|
||||
|
||||
|
||||
4. List all the ports on a network
|
||||
|
||||
# PYTHONPATH=. python quantum/plugins/cisco/client/cli.py list_ports -H 10.10.2.6 demo c4a2bea7-a528-4caf-b16e-80397cd1663a
|
||||
Ports on Virtual Network: c4a2bea7-a528-4caf-b16e-80397cd1663a
|
||||
for Tenant: demo
|
||||
Logical Port: 118ac473-294d-480e-8f6d-425acbbe81ae
|
||||
|
||||
|
||||
5. Show the details of a port
|
||||
|
||||
# PYTHONPATH=. python quantum/plugins/cisco/client/cli.py show_port -H 10.10.2.6 demo c4a2bea7-a528-4caf-b16e-80397cd1663a 118ac473-294d-480e-8f6d-425acbbe81ae
|
||||
Logical Port ID: 118ac473-294d-480e-8f6d-425acbbe81ae
|
||||
administrative State: ACTIVE
|
||||
interface: <none>
|
||||
on Virtual Network: c4a2bea7-a528-4caf-b16e-80397cd1663a
|
||||
for Tenant: demo
|
||||
|
||||
|
||||
6. Start the VM instance using Nova
|
||||
Note that when using UCS and the 802.1Qbh features, the association of the
|
||||
VIF-ID (also referred to as interface ID) on the VM's NIC with a port will
|
||||
happen automatically when the VM is instantiated. At this point, doing a
|
||||
show_port will reveal the VIF-ID associated with the port.
|
||||
|
||||
# PYTHONPATH=. python quantum/plugins/cisco/client/cli.py show_port demo c4a2bea7-a528-4caf-b16e-80397cd1663a 118ac473-294d-480e-8f6d-425acbbe81ae
|
||||
Logical Port ID: 118ac473-294d-480e-8f6d-425acbbe81ae
|
||||
administrative State: ACTIVE
|
||||
interface: b73e3585-d074-4379-8dde-931c0fc4db0e
|
||||
on Virtual Network: c4a2bea7-a528-4caf-b16e-80397cd1663a
|
||||
for Tenant: demo
|
||||
|
||||
|
||||
7. Plug interface and port into the network
|
||||
Use the interface information obtained in step 6 to plug the interface into
|
||||
the network.
|
||||
|
||||
# PYTHONPATH=. python quantum/plugins/cisco/client/cli.py plug_iface demo c4a2bea7-a528-4caf-b16e-80397cd1663a 118ac473-294d-480e-8f6d-425acbbe81ae b73e3585-d074-4379-8dde-931c0fc4db0e
|
||||
Plugged interface b73e3585-d074-4379-8dde-931c0fc4db0e
|
||||
into Logical Port: 118ac473-294d-480e-8f6d-425acbbe81ae
|
||||
on Virtual Network: c4a2bea7-a528-4caf-b16e-80397cd1663a
|
||||
for Tenant: demo
|
||||
|
||||
|
||||
8. Unplug an interface and port from the network
|
||||
Note: Before unplugging, make a note of the interface ID (you can use the
|
||||
show_port CLI as before). While the VM, which has a VIF with this interface
|
||||
ID still exists, you can only plug that same interface back into this port.
|
||||
So the subsequent plug interface operation on this port will have to make
|
||||
use of the same interface ID.
|
||||
|
||||
# PYTHONPATH=. python quantum/plugins/cisco/client/cli.py unplug_iface demo c4a2bea7-a528-4caf-b16e-80397cd1663a 118ac473-294d-480e-8f6d-425acbbe81ae
|
||||
Unplugged interface from Logical Port: 118ac473-294d-480e-8f6d-425acbbe81ae
|
||||
on Virtual Network: c4a2bea7-a528-4caf-b16e-80397cd1663a
|
||||
for Tenant: demo
|
||||
|
||||
|
||||
How to test the installation
|
||||
----------------------------
|
||||
The unit tests are located at quantum/plugins/cisco/tests/unit. They can be
|
||||
|
225
quantum/plugins/cisco/client/cli.py
Normal file
225
quantum/plugins/cisco/client/cli.py
Normal file
@ -0,0 +1,225 @@
|
||||
"""
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright 2011 Cisco Systems, Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Initial structure and framework of this CLI has been borrowed from Quantum,
|
||||
# written by the following authors
|
||||
# @author: Somik Behera, Nicira Networks, Inc.
|
||||
# @author: Brad Hall, Nicira Networks, Inc.
|
||||
# @author: Salvatore Orlando, Citrix
|
||||
#
|
||||
# Cisco adaptation for extensions
|
||||
# @author: Sumit Naiksatam, Cisco Systems, Inc.
|
||||
# @author: Ying Liu, Cisco Systems, Inc.
|
||||
#
|
||||
"""
|
||||
|
||||
import gettext
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from optparse import OptionParser
|
||||
|
||||
POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'quantum', '__init__.py')):
|
||||
sys.path.insert(0, POSSIBLE_TOPDIR)
|
||||
|
||||
gettext.install('quantum', unicode=1)
|
||||
|
||||
from quantum.client import Client
|
||||
|
||||
from quantum.plugins.cisco.common import cisco_constants as const
|
||||
|
||||
LOG = logging.getLogger('quantum')
|
||||
FORMAT = 'json'
|
||||
ACTION_PREFIX_EXT = '/v1.0'
|
||||
ACTION_PREFIX_CSCO = ACTION_PREFIX_EXT + \
|
||||
'/extensions/csco/tenants/{tenant_id}'
|
||||
TENANT_ID = 'nova'
|
||||
CSCO_EXT_NAME = 'Cisco Nova Tenant'
|
||||
|
||||
|
||||
def help():
|
||||
"""Help for CLI"""
|
||||
print "\nCisco Extension Commands:"
|
||||
for key in COMMANDS.keys():
|
||||
print " %s %s" % (key,
|
||||
" ".join(["<%s>" % y for y in COMMANDS[key]["args"]]))
|
||||
|
||||
|
||||
def build_args(cmd, cmdargs, arglist):
|
||||
"""Building the list of args for a particular CLI"""
|
||||
args = []
|
||||
orig_arglist = arglist[:]
|
||||
try:
|
||||
for cmdarg in cmdargs:
|
||||
args.append(arglist[0])
|
||||
del arglist[0]
|
||||
except:
|
||||
LOG.error("Not enough arguments for \"%s\" (expected: %d, got: %d)" % (
|
||||
cmd, len(cmdargs), len(orig_arglist)))
|
||||
print "Usage:\n %s %s" % (cmd,
|
||||
" ".join(["<%s>" % y for y in COMMANDS[cmd]["args"]]))
|
||||
sys.exit()
|
||||
if len(arglist) > 0:
|
||||
LOG.error("Too many arguments for \"%s\" (expected: %d, got: %d)" % (
|
||||
cmd, len(cmdargs), len(orig_arglist)))
|
||||
print "Usage:\n %s %s" % (cmd,
|
||||
" ".join(["<%s>" % y for y in COMMANDS[cmd]["args"]]))
|
||||
sys.exit()
|
||||
return args
|
||||
|
||||
|
||||
def list_extensions(*args):
|
||||
"""Invoking the action to get the supported extensions"""
|
||||
request_url = "/extensions"
|
||||
client = Client(HOST, PORT, USE_SSL, format='json',
|
||||
action_prefix=ACTION_PREFIX_EXT, tenant="dummy")
|
||||
data = client.do_request('GET', request_url)
|
||||
print("Obtained supported extensions from Quantum: %s" % data)
|
||||
|
||||
|
||||
def schedule_host(tenant_id, instance_id, user_id=None):
|
||||
"""Gets the host name from the Quantum service"""
|
||||
project_id = tenant_id
|
||||
|
||||
instance_data_dict = \
|
||||
{'novatenant': \
|
||||
{'instance_id': instance_id,
|
||||
'instance_desc': \
|
||||
{'user_id': user_id,
|
||||
'project_id': project_id}}}
|
||||
|
||||
request_url = "/novatenants/" + project_id + "/schedule_host"
|
||||
client = Client(HOST, PORT, USE_SSL, format='json', tenant=TENANT_ID,
|
||||
action_prefix=ACTION_PREFIX_CSCO)
|
||||
data = client.do_request('PUT', request_url, body=instance_data_dict)
|
||||
|
||||
hostname = data["host_list"]["host_1"]
|
||||
if not hostname:
|
||||
print("Scheduler was unable to locate a host" + \
|
||||
" for this request. Is the appropriate" + \
|
||||
" service running?")
|
||||
|
||||
print("Quantum service returned host: %s" % hostname)
|
||||
|
||||
|
||||
def create_multiport(tenant_id, net_id_list, *args):
|
||||
"""Creates ports on a single host"""
|
||||
net_list = net_id_list.split(",")
|
||||
ports_info = {'multiport': \
|
||||
{'status': 'ACTIVE',
|
||||
'net_id_list': net_list,
|
||||
'ports_desc': {'key': 'value'}}}
|
||||
|
||||
request_url = "/multiport"
|
||||
client = Client(HOST, PORT, USE_SSL, format='json', tenant=tenant_id,
|
||||
action_prefix=ACTION_PREFIX_CSCO)
|
||||
data = client.do_request('POST', request_url, body=ports_info)
|
||||
|
||||
print("Created ports: %s" % data)
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
"create_multiport": {
|
||||
"func": create_multiport,
|
||||
"args": ["tenant-id",
|
||||
"net-id-list (comma separated list of netword IDs)"]},
|
||||
"list_extensions": {
|
||||
"func": list_extensions,
|
||||
"args": []},
|
||||
"schedule_host": {
|
||||
"func": schedule_host,
|
||||
"args": ["tenant-id", "instance-id"]}, }
|
||||
|
||||
|
||||
class _DynamicModule(object):
|
||||
"""Loading a string as python module"""
|
||||
def load(self, code):
|
||||
execdict = {}
|
||||
exec code in execdict
|
||||
for key in execdict:
|
||||
if not key.startswith('_'):
|
||||
setattr(self, key, execdict[key])
|
||||
|
||||
|
||||
import sys as _sys
|
||||
_ref, _sys.modules[__name__] = _sys.modules[__name__], _DynamicModule()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import cli
|
||||
FILE_NAME = os.path.join("bin/", "cli")
|
||||
MODULE_CODE = open(FILE_NAME).read()
|
||||
cli.load(MODULE_CODE)
|
||||
usagestr = "Usage: %prog [OPTIONS] <command> [args]"
|
||||
PARSER = OptionParser(usage=usagestr)
|
||||
PARSER.add_option("-H", "--host", dest="host",
|
||||
type="string", default="127.0.0.1", help="ip address of api host")
|
||||
PARSER.add_option("-p", "--port", dest="port",
|
||||
type="int", default=9696, help="api poort")
|
||||
PARSER.add_option("-s", "--ssl", dest="ssl",
|
||||
action="store_true", default=False, help="use ssl")
|
||||
PARSER.add_option("-v", "--verbose", dest="verbose",
|
||||
action="store_true", default=False, help="turn on verbose logging")
|
||||
PARSER.add_option("-f", "--logfile", dest="logfile",
|
||||
type="string", default="syslog", help="log file path")
|
||||
options, args = PARSER.parse_args()
|
||||
|
||||
if options.verbose:
|
||||
LOG.setLevel(logging.DEBUG)
|
||||
else:
|
||||
LOG.setLevel(logging.WARN)
|
||||
|
||||
if options.logfile == "syslog":
|
||||
LOG.addHandler(logging.handlers.SysLogHandler(address='/dev/log'))
|
||||
else:
|
||||
LOG.addHandler(logging.handlers.WatchedFileHandler(options.logfile))
|
||||
os.chmod(options.logfile, 0644)
|
||||
|
||||
if len(args) < 1:
|
||||
PARSER.print_help()
|
||||
cli.help()
|
||||
help()
|
||||
sys.exit(1)
|
||||
|
||||
CMD = args[0]
|
||||
if CMD in cli.commands.keys():
|
||||
args.insert(0, FILE_NAME)
|
||||
subprocess.call(args)
|
||||
sys.exit(1)
|
||||
if CMD not in COMMANDS.keys():
|
||||
LOG.error("Unknown command: %s" % CMD)
|
||||
cli.help()
|
||||
help()
|
||||
sys.exit(1)
|
||||
|
||||
args = build_args(CMD, COMMANDS[CMD]["args"], args[1:])
|
||||
|
||||
LOG.info("Executing command \"%s\" with args: %s" % (CMD, args))
|
||||
|
||||
HOST = options.host
|
||||
PORT = options.port
|
||||
USE_SSL = options.ssl
|
||||
COMMANDS[CMD]["func"](*args)
|
||||
|
||||
LOG.info("Command execution completed")
|
||||
sys.exit(0)
|
@ -149,3 +149,7 @@ LEAST_RSVD_BLADE_DICT = 'least_rsvd_blade_dict'
|
||||
UCSM_IP = 'ucsm_ip_address'
|
||||
|
||||
NETWORK_ADMIN = 'network_admin'
|
||||
|
||||
NETID_LIST = 'net_id_list'
|
||||
|
||||
DELIMITERS = "[,;:\b\s]"
|
||||
|
@ -55,6 +55,12 @@ class PortProfileNotFound(exceptions.QuantumException):
|
||||
"for tenant %(tenant_id)s")
|
||||
|
||||
|
||||
class MultiportNotFound(exceptions.QuantumException):
|
||||
"""Multiport cannot be found"""
|
||||
message = _("Multiports %(port_id)s could not be found " \
|
||||
"for tenant %(tenant_id)s")
|
||||
|
||||
|
||||
class PortProfileInvalidDelete(exceptions.QuantumException):
|
||||
"""Port profile cannot be deleted since its being used"""
|
||||
message = _("Port profile %(profile_id)s could not be deleted " \
|
||||
|
@ -34,6 +34,7 @@ class Fault(webob.exc.HTTPException):
|
||||
451: "CredentialNotFound",
|
||||
452: "QoSNotFound",
|
||||
453: "NovatenantNotFound",
|
||||
454: "MultiportNotFound",
|
||||
470: "serviceUnavailable",
|
||||
471: "pluginFault"}
|
||||
|
||||
@ -96,7 +97,7 @@ class CredentialNotFound(webob.exc.HTTPClientError):
|
||||
This indicates that the server did not find the Credential specified
|
||||
in the HTTP request
|
||||
|
||||
code: 460, title: Credential not Found
|
||||
code: 451, title: Credential not Found
|
||||
"""
|
||||
code = 451
|
||||
title = 'Credential Not Found'
|
||||
@ -111,7 +112,7 @@ class QosNotFound(webob.exc.HTTPClientError):
|
||||
This indicates that the server did not find the QoS specified
|
||||
in the HTTP request
|
||||
|
||||
code: 480, title: QoS not Found
|
||||
code: 452, title: QoS not Found
|
||||
"""
|
||||
code = 452
|
||||
title = 'QoS Not Found'
|
||||
@ -126,7 +127,7 @@ class NovatenantNotFound(webob.exc.HTTPClientError):
|
||||
This indicates that the server did not find the Novatenant specified
|
||||
in the HTTP request
|
||||
|
||||
code: 480, title: Nova tenant not Found
|
||||
code: 453, title: Nova tenant not Found
|
||||
"""
|
||||
code = 453
|
||||
title = 'Nova tenant Not Found'
|
||||
@ -134,6 +135,21 @@ class NovatenantNotFound(webob.exc.HTTPClientError):
|
||||
+ ' the specified identifier.')
|
||||
|
||||
|
||||
class MultiportNotFound(webob.exc.HTTPClientError):
|
||||
"""
|
||||
subclass of :class:`~HTTPClientError`
|
||||
|
||||
This indicates that the server did not find the Multiport specified
|
||||
in the HTTP request
|
||||
|
||||
code: 454, title: Multiport not Found
|
||||
"""
|
||||
code = 454
|
||||
title = 'Multiport Not Found'
|
||||
explanation = ('Unable to find Multiport with'
|
||||
+ ' the specified identifier.')
|
||||
|
||||
|
||||
class RequestedStateInvalid(webob.exc.HTTPClientError):
|
||||
"""
|
||||
subclass of :class:`~HTTPClientError`
|
||||
|
@ -1,37 +0,0 @@
|
||||
"""
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright 2011 Cisco Systems, Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# @author: Sumit Naiksatam, Cisco Systems, Inc.
|
||||
#
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from quantum.plugins.cisco.common import cisco_configparser as confp
|
||||
|
||||
CONF_FILE = "../conf/nova.ini"
|
||||
|
||||
CP = confp.CiscoConfigParser(os.path.dirname(os.path.realpath(__file__)) \
|
||||
+ "/" + CONF_FILE)
|
||||
|
||||
SECTION = CP['NOVA']
|
||||
DB_SERVER_IP = SECTION['db_server_ip']
|
||||
DB_NAME = SECTION['db_name']
|
||||
DB_USERNAME = SECTION['db_username']
|
||||
DB_PASSWORD = SECTION['db_password']
|
||||
NOVA_HOST_NAME = SECTION['nova_host_name']
|
||||
NOVA_PROJ_NAME = SECTION['nova_proj_name']
|
@ -25,7 +25,6 @@ import MySQLdb
|
||||
import traceback
|
||||
|
||||
from quantum.plugins.cisco.common import cisco_constants as const
|
||||
from quantum.plugins.cisco.common import cisco_nova_configuration as conf
|
||||
from quantum.plugins.cisco.db import api as db
|
||||
from quantum.plugins.cisco.db import l2network_db as cdb
|
||||
|
||||
@ -77,35 +76,3 @@ def make_portprofile_assc_list(tenant_id, profile_id):
|
||||
assc_list.append(port[const.PORTID])
|
||||
|
||||
return assc_list
|
||||
|
||||
|
||||
class DBUtils(object):
|
||||
"""Utilities to use connect to MySQL DB and execute queries"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _get_db_connection(self):
|
||||
"""Get a connection to the DB"""
|
||||
db_ip = conf.DB_SERVER_IP
|
||||
db_username = conf.DB_USERNAME
|
||||
db_password = conf.DB_PASSWORD
|
||||
self.db = MySQLdb.connect(db_ip, db_username, db_password,
|
||||
conf.DB_NAME)
|
||||
return self.db
|
||||
|
||||
def execute_db_query(self, sql_query):
|
||||
"""Execute a DB query"""
|
||||
db = self._get_db_connection()
|
||||
cursor = db.cursor()
|
||||
try:
|
||||
cursor.execute(sql_query)
|
||||
results = cursor.fetchall()
|
||||
db.commit()
|
||||
LOG.debug("DB query execution succeeded: %s" % sql_query)
|
||||
db.close()
|
||||
except:
|
||||
db.rollback()
|
||||
LOG.debug("DB query execution failed: %s" % sql_query)
|
||||
traceback.print_exc()
|
||||
db.close()
|
||||
|
@ -1,13 +1,8 @@
|
||||
#Provide the UCSM credentials
|
||||
#Provide the UCSM credentials, make sure you have a separate entry for every UCSM in your deployment
|
||||
[<put_ucsm_ip_address_here>]
|
||||
username=<put_user_name_here>
|
||||
password=<put_password_here>
|
||||
|
||||
#Provide the Nova DB credentials, the IP address should be the same as in nova.ini
|
||||
[<put_nova_db_ip_here>]
|
||||
username=<put_user_name_here>
|
||||
password=<put_password_here>
|
||||
|
||||
#Provide the Nexus credentials, if you are using Nexus
|
||||
[<put_nexus_ip_address_here>]
|
||||
username=<put_user_name_here>
|
||||
|
@ -1,8 +0,0 @@
|
||||
[NOVA]
|
||||
#Change the following details to reflect your OpenStack Nova configuration. If you are running this service on the same machine as the Nova DB, you do not have to change the IP address.
|
||||
db_server_ip=127.0.0.1
|
||||
db_name=nova
|
||||
db_username=<put_db_user_name_here>
|
||||
db_password=<put_db_password_here>
|
||||
nova_host_name=<put_openstack_cloud_controller_hostname_here>
|
||||
nova_proj_name=<put_openstack_project_name_here>
|
@ -1,5 +1,6 @@
|
||||
[UCSM]
|
||||
#change the following to the appropriate UCSM IP address
|
||||
#if you have more than one UCSM, enter info from any one
|
||||
ip_address=<put_ucsm_ip_address_here>
|
||||
default_vlan_name=default
|
||||
default_vlan_id=1
|
||||
|
@ -21,7 +21,7 @@
|
||||
|
||||
import inspect
|
||||
import logging as LOG
|
||||
import platform
|
||||
import re
|
||||
|
||||
from quantum.common import exceptions as exc
|
||||
from quantum.common import utils
|
||||
@ -41,8 +41,9 @@ LOG.getLogger(const.LOGGER_COMPONENT_NAME)
|
||||
|
||||
class L2Network(QuantumPluginBase):
|
||||
""" L2 Network Framework Plugin """
|
||||
supported_extension_aliases = ["Cisco Credential", "Cisco Port Profile",
|
||||
"Cisco qos", "Cisco Nova Tenant"]
|
||||
supported_extension_aliases = ["Cisco Multiport", "Cisco Credential",
|
||||
"Cisco Port Profile", "Cisco qos",
|
||||
"Cisco Nova Tenant"]
|
||||
|
||||
def __init__(self):
|
||||
cdb.initialize()
|
||||
@ -178,6 +179,7 @@ class L2Network(QuantumPluginBase):
|
||||
Creates a port on the specified Virtual Network.
|
||||
"""
|
||||
LOG.debug("create_port() called\n")
|
||||
|
||||
port = db.port_create(net_id, port_state)
|
||||
unique_port_id_string = port[const.UUID]
|
||||
self._invoke_device_plugins(self._func_name(), [tenant_id, net_id,
|
||||
@ -249,10 +251,19 @@ class L2Network(QuantumPluginBase):
|
||||
"""
|
||||
LOG.debug("plug_interface() called\n")
|
||||
network = db.network_get(net_id)
|
||||
self._invoke_device_plugins(self._func_name(), [tenant_id, net_id,
|
||||
port_id,
|
||||
remote_interface_id])
|
||||
db.port_set_attachment(net_id, port_id, remote_interface_id)
|
||||
port = db.port_get(net_id, port_id)
|
||||
attachment_id = port[const.INTERFACEID]
|
||||
if attachment_id and remote_interface_id != attachment_id:
|
||||
raise exc.PortInUse(port_id=port_id, net_id=net_id,
|
||||
att_id=attachment_id)
|
||||
self._invoke_device_plugins(self._func_name(), [tenant_id,
|
||||
net_id, port_id,
|
||||
remote_interface_id])
|
||||
if attachment_id == None:
|
||||
db.port_set_attachment(net_id, port_id, remote_interface_id)
|
||||
#Note: The remote_interface_id gets associated with the port
|
||||
# when the VM is instantiated. The plug interface call results
|
||||
# in putting the port on the VLAN associated with this network
|
||||
|
||||
def unplug_interface(self, tenant_id, net_id, port_id):
|
||||
"""
|
||||
@ -287,7 +298,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("get_portprofile_details() called\n")
|
||||
try:
|
||||
portprofile = cdb.get_portprofile(tenant_id, profile_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.PortProfileNotFound(tenant_id=tenant_id,
|
||||
portprofile_id=profile_id)
|
||||
|
||||
@ -313,7 +324,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("delete_portprofile() called\n")
|
||||
try:
|
||||
portprofile = cdb.get_portprofile(tenant_id, profile_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.PortProfileNotFound(tenant_id=tenant_id,
|
||||
portprofile_id=profile_id)
|
||||
|
||||
@ -329,7 +340,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("rename_portprofile() called\n")
|
||||
try:
|
||||
portprofile = cdb.get_portprofile(tenant_id, profile_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.PortProfileNotFound(tenant_id=tenant_id,
|
||||
portprofile_id=profile_id)
|
||||
portprofile = cdb.update_portprofile(tenant_id, profile_id, new_name)
|
||||
@ -345,7 +356,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("associate_portprofile() called\n")
|
||||
try:
|
||||
portprofile = cdb.get_portprofile(tenant_id, portprofile_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.PortProfileNotFound(tenant_id=tenant_id,
|
||||
portprofile_id=portprofile_id)
|
||||
|
||||
@ -357,7 +368,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("disassociate_portprofile() called\n")
|
||||
try:
|
||||
portprofile = cdb.get_portprofile(tenant_id, portprofile_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.PortProfileNotFound(tenant_id=tenant_id,
|
||||
portprofile_id=portprofile_id)
|
||||
|
||||
@ -374,7 +385,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("get_qos_details() called\n")
|
||||
try:
|
||||
qos_level = cdb.get_qos(tenant_id, qos_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.QosNotFound(tenant_id=tenant_id,
|
||||
qos_id=qos_id)
|
||||
return qos_level
|
||||
@ -390,7 +401,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("delete_qos() called\n")
|
||||
try:
|
||||
qos_level = cdb.get_qos(tenant_id, qos_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.QosNotFound(tenant_id=tenant_id,
|
||||
qos_id=qos_id)
|
||||
return cdb.remove_qos(tenant_id, qos_id)
|
||||
@ -400,7 +411,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("rename_qos() called\n")
|
||||
try:
|
||||
qos_level = cdb.get_qos(tenant_id, qos_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.QosNotFound(tenant_id=tenant_id,
|
||||
qos_id=qos_id)
|
||||
qos = cdb.update_qos(tenant_id, qos_id, new_name)
|
||||
@ -417,7 +428,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("get_credential_details() called\n")
|
||||
try:
|
||||
credential = cdb.get_credential(tenant_id, credential_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.CredentialNotFound(tenant_id=tenant_id,
|
||||
credential_id=credential_id)
|
||||
return credential
|
||||
@ -435,7 +446,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("delete_credential() called\n")
|
||||
try:
|
||||
credential = cdb.get_credential(tenant_id, credential_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.CredentialNotFound(tenant_id=tenant_id,
|
||||
credential_id=credential_id)
|
||||
credential = cdb.remove_credential(tenant_id, credential_id)
|
||||
@ -446,7 +457,7 @@ class L2Network(QuantumPluginBase):
|
||||
LOG.debug("rename_credential() called\n")
|
||||
try:
|
||||
credential = cdb.get_credential(tenant_id, credential_id)
|
||||
except Exception, excp:
|
||||
except Exception:
|
||||
raise cexc.CredentialNotFound(tenant_id=tenant_id,
|
||||
credential_id=credential_id)
|
||||
credential = cdb.update_credential(tenant_id, credential_id, new_name)
|
||||
@ -469,6 +480,27 @@ class L2Network(QuantumPluginBase):
|
||||
instance_id,
|
||||
instance_desc])
|
||||
|
||||
def create_multiport(self, tenant_id, net_id_list, port_state, ports_desc):
|
||||
"""
|
||||
Creates multiple ports on the specified Virtual Network.
|
||||
"""
|
||||
LOG.debug("create_ports() called\n")
|
||||
ports_num = len(net_id_list)
|
||||
ports_id_list = []
|
||||
ports_dict_list = []
|
||||
|
||||
for net_id in net_id_list:
|
||||
port = db.port_create(net_id, port_state)
|
||||
ports_id_list.append(port[const.UUID])
|
||||
port_dict = {const.PORT_ID: port[const.UUID]}
|
||||
ports_dict_list.append(port_dict)
|
||||
|
||||
self._invoke_device_plugins(self._func_name(), [tenant_id,
|
||||
net_id_list,
|
||||
ports_num,
|
||||
ports_id_list])
|
||||
return ports_dict_list
|
||||
|
||||
"""
|
||||
Private functions
|
||||
"""
|
||||
|
@ -171,3 +171,8 @@ class L2NetworkMultiBlade(L2NetworkModelBase):
|
||||
LOG.debug("associate_port() called\n")
|
||||
return self._invoke_inventory(const.UCS_PLUGIN, self._func_name(),
|
||||
args)
|
||||
|
||||
def create_multiport(self, args):
|
||||
"""Support for extension API call"""
|
||||
self._invoke_plugin_per_device(const.UCS_PLUGIN, self._func_name(),
|
||||
args)
|
||||
|
@ -162,3 +162,8 @@ class L2NetworkSingleBlade(L2NetworkModelBase):
|
||||
LOG.debug("associate_port() called\n")
|
||||
return self._invoke_inventory(const.UCS_PLUGIN, self._func_name(),
|
||||
args)
|
||||
|
||||
def create_multiport(self, args):
|
||||
"""Support for extension API call"""
|
||||
self._invoke_plugin_per_device(const.UCS_PLUGIN, self._func_name(),
|
||||
args)
|
||||
|
@ -43,7 +43,7 @@ CSCO_EXT_NAME = 'Cisco Nova Tenant'
|
||||
ACTION = '/schedule_host'
|
||||
|
||||
|
||||
class QuantumScheduler(driver.Scheduler):
|
||||
class QuantumPortAwareScheduler(driver.Scheduler):
|
||||
"""
|
||||
Quantum network service dependent scheduler
|
||||
Obtains the hostname from Quantum using an extension API
|
@ -28,6 +28,7 @@ from extensions import credential
|
||||
from extensions import portprofile
|
||||
from extensions import novatenant
|
||||
from extensions import qos
|
||||
from extensions import multiport
|
||||
from quantum.plugins.cisco.db import api as db
|
||||
from quantum.common import wsgi
|
||||
from quantum.common import config
|
||||
@ -1022,6 +1023,122 @@ class CredentialExtensionTest(unittest.TestCase):
|
||||
db.clear_db()
|
||||
|
||||
|
||||
class MultiPortExtensionTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
""" Set up function """
|
||||
|
||||
parent_resource = dict(member_name="tenant",
|
||||
collection_name="extensions/csco/tenants")
|
||||
controller = multiport.MultiportController(
|
||||
QuantumManager.get_plugin())
|
||||
res_ext = extensions.ResourceExtension('multiport', controller,
|
||||
parent=parent_resource)
|
||||
self.test_app = setup_extensions_test_app(
|
||||
SimpleExtensionManager(res_ext))
|
||||
self.contenttype = 'application/json'
|
||||
self.multiport_path = '/extensions/csco/tenants/tt/multiport'
|
||||
self.multiport_path2 = '/extensions/csco/tenants/tt/multiport/'
|
||||
self.test_multi_port = {'multiport':
|
||||
{'net_id_list': '1',
|
||||
'status': 'test-qos1',
|
||||
'ports_desc': 'Port Descr'}}
|
||||
self.tenant_id = "test_tenant"
|
||||
self.network_name = "test_network"
|
||||
options = {}
|
||||
options['plugin_provider'] = 'quantum.plugins.cisco.l2network_plugin'\
|
||||
'.L2Network'
|
||||
self.api = server.APIRouterV1(options)
|
||||
self._l2network_plugin = l2network_plugin.L2Network()
|
||||
|
||||
def create_request(self, path, body, content_type, method='GET'):
|
||||
|
||||
""" Test create request"""
|
||||
|
||||
LOG.debug("test_create_request - START")
|
||||
req = webob.Request.blank(path)
|
||||
req.method = method
|
||||
req.headers = {}
|
||||
req.headers['Accept'] = content_type
|
||||
req.body = body
|
||||
LOG.debug("test_create_request - END")
|
||||
return req
|
||||
|
||||
def _create_network(self, name=None):
|
||||
|
||||
""" Test create network"""
|
||||
|
||||
LOG.debug("Creating network - START")
|
||||
if name:
|
||||
net_name = name
|
||||
else:
|
||||
net_name = self.network_name
|
||||
net_path = "/tenants/tt/networks"
|
||||
net_data = {'network': {'name': '%s' % net_name}}
|
||||
req_body = wsgi.Serializer().serialize(net_data, self.contenttype)
|
||||
network_req = self.create_request(net_path, req_body,
|
||||
self.contenttype, 'POST')
|
||||
network_res = network_req.get_response(self.api)
|
||||
network_data = wsgi.Serializer().deserialize(network_res.body,
|
||||
self.contenttype)
|
||||
LOG.debug("Creating network - END")
|
||||
return network_data['network']['id']
|
||||
|
||||
def _delete_network(self, network_id):
|
||||
""" Delete network """
|
||||
LOG.debug("Deleting network %s - START", network_id)
|
||||
network_path = "/tenants/tt/networks/%s" % network_id
|
||||
network_req = self.create_request(network_path, None,
|
||||
self.contenttype, 'DELETE')
|
||||
network_req.get_response(self.api)
|
||||
LOG.debug("Deleting network - END")
|
||||
|
||||
def test_create_multiport(self):
|
||||
|
||||
""" Test create MultiPort"""
|
||||
|
||||
LOG.debug("test_create_multiport - START")
|
||||
|
||||
net_id = self._create_network('net1')
|
||||
net_id2 = self._create_network('net2')
|
||||
test_multi_port = {'multiport':
|
||||
{'net_id_list': [net_id, net_id2],
|
||||
'status': 'ACTIVE',
|
||||
'ports_desc': {'key': 'value'}}}
|
||||
req_body = json.dumps(test_multi_port)
|
||||
index_response = self.test_app.post(self.multiport_path, req_body,
|
||||
content_type=self.contenttype)
|
||||
resp_body = wsgi.Serializer().deserialize(index_response.body,
|
||||
self.contenttype)
|
||||
self.assertEqual(200, index_response.status_int)
|
||||
self.assertEqual(len(test_multi_port['multiport']['net_id_list']),
|
||||
len(resp_body['ports']))
|
||||
# Clean Up - Delete the Port Profile
|
||||
self._delete_network(net_id)
|
||||
self._delete_network(net_id2)
|
||||
LOG.debug("test_create_multiport - END")
|
||||
|
||||
def test_create_multiportBADRequest(self):
|
||||
|
||||
""" Test create MultiPort Bad Request"""
|
||||
|
||||
LOG.debug("test_create_multiportBADRequest - START")
|
||||
net_id = self._create_network('net1')
|
||||
net_id2 = self._create_network('net2')
|
||||
index_response = self.test_app.post(self.multiport_path, 'BAD_REQUEST',
|
||||
content_type=self.contenttype,
|
||||
status='*')
|
||||
self.assertEqual(400, index_response.status_int)
|
||||
# Clean Up - Delete the Port Profile
|
||||
self._delete_network(net_id)
|
||||
self._delete_network(net_id2)
|
||||
LOG.debug("test_create_multiportBADRequest - END")
|
||||
|
||||
def tearDown(self):
|
||||
db.clear_db()
|
||||
|
||||
|
||||
def app_factory(global_conf, **local_conf):
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
@ -506,13 +506,14 @@ class CoreAPITestFunc(unittest.TestCase):
|
||||
"""
|
||||
|
||||
LOG.debug("test_plug_interface_portInUse - START")
|
||||
current_interface = "current_interface"
|
||||
new_net_dict = self._l2network_plugin.create_network(
|
||||
tenant_id, self.network_name)
|
||||
port_dict = self._l2network_plugin.create_port(
|
||||
tenant_id, new_net_dict[const.NET_ID], self.port_state)
|
||||
self._l2network_plugin.plug_interface(
|
||||
tenant_id, new_net_dict[const.NET_ID],
|
||||
port_dict[const.PORT_ID], remote_interface)
|
||||
port_dict[const.PORT_ID], current_interface)
|
||||
self.assertRaises(exc.PortInUse,
|
||||
self._l2network_plugin.plug_interface, tenant_id,
|
||||
new_net_dict[const.NET_ID],
|
||||
|
@ -121,7 +121,30 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
chassis_dict[chassis_id] = blade_list
|
||||
self._inventory[ucsm_ip] = chassis_dict
|
||||
|
||||
self.build_inventory_state()
|
||||
self._build_inventory_state()
|
||||
|
||||
def _build_inventory_state(self):
|
||||
"""Populate the state of all the blades"""
|
||||
for ucsm_ip in self._inventory.keys():
|
||||
self._inventory_state[ucsm_ip] = {ucsm_ip: {}}
|
||||
ucsm_username = cred.Store.getUsername(ucsm_ip)
|
||||
ucsm_password = cred.Store.getPassword(ucsm_ip)
|
||||
chasses_state = {}
|
||||
self._inventory_state[ucsm_ip] = chasses_state
|
||||
ucsm = self._inventory[ucsm_ip]
|
||||
for chassis_id in ucsm.keys():
|
||||
blades_dict = {}
|
||||
chasses_state[chassis_id] = blades_dict
|
||||
for blade_id in ucsm[chassis_id]:
|
||||
blade_data = self._get_initial_blade_state(chassis_id,
|
||||
blade_id,
|
||||
ucsm_ip,
|
||||
ucsm_username,
|
||||
ucsm_password)
|
||||
blades_dict[blade_id] = blade_data
|
||||
|
||||
LOG.debug("UCS Inventory state is: %s\n" % self._inventory_state)
|
||||
return True
|
||||
|
||||
def _get_host_name(self, ucsm_ip, chassis_id, blade_id):
|
||||
"""Get the hostname based on the blade info"""
|
||||
@ -187,8 +210,10 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
blade_intf_data[blade_intf][const.VIF_ID] = \
|
||||
port_binding[const.VIF_ID]
|
||||
|
||||
host_name = self._get_host_name(ucsm_ip, chassis_id, blade_id)
|
||||
blade_data = {const.BLADE_INTF_DATA: blade_intf_data,
|
||||
const.BLADE_UNRESERVED_INTF_COUNT: unreserved_counter}
|
||||
const.BLADE_UNRESERVED_INTF_COUNT: unreserved_counter,
|
||||
const.HOST_NAME: host_name}
|
||||
return blade_data
|
||||
|
||||
def _get_blade_state(self, chassis_id, blade_id, ucsm_ip,
|
||||
@ -229,7 +254,7 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
tenant_id = args[0]
|
||||
net_id = args[1]
|
||||
port_id = args[2]
|
||||
rsvd_info = self.get_rsvd_blade_intf_by_port(tenant_id, port_id)
|
||||
rsvd_info = self._get_rsvd_blade_intf_by_port(tenant_id, port_id)
|
||||
if not rsvd_info:
|
||||
raise exc.PortNotFound(net_id=net_id, port_id=port_id)
|
||||
device_params = {const.DEVICE_IP: [rsvd_info[const.UCSM_IP]]}
|
||||
@ -361,39 +386,46 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
(vif_id, port_id,
|
||||
blade_intf_data[blade_intf]))
|
||||
return
|
||||
LOG.warn("Disassociating VIF-ID %s in UCS inventory failed. " \
|
||||
LOG.warn("Disassociating VIF-ID in UCS inventory failed. " \
|
||||
"Could not find a reserved dynamic nic for tenant: %s" %
|
||||
(vif_id, tenant_id))
|
||||
tenant_id)
|
||||
return None
|
||||
|
||||
def reload_inventory(self):
|
||||
"""Reload the inventory from a conf file"""
|
||||
self._load_inventory()
|
||||
|
||||
def build_inventory_state(self):
|
||||
"""Populate the state of all the blades"""
|
||||
for ucsm_ip in self._inventory.keys():
|
||||
self._inventory_state[ucsm_ip] = {ucsm_ip: {}}
|
||||
ucsm_username = cred.Store.getUsername(ucsm_ip)
|
||||
ucsm_password = cred.Store.getPassword(ucsm_ip)
|
||||
chasses_state = {}
|
||||
self._inventory_state[ucsm_ip] = chasses_state
|
||||
ucsm = self._inventory[ucsm_ip]
|
||||
def _get_rsvd_blade_intf_by_port(self, tenant_id, port_id):
|
||||
"""
|
||||
Lookup a reserved blade interface based on tenant_id and port_id
|
||||
and return the blade interface info
|
||||
"""
|
||||
for ucsm_ip in self._inventory_state.keys():
|
||||
ucsm = self._inventory_state[ucsm_ip]
|
||||
for chassis_id in ucsm.keys():
|
||||
blades_dict = {}
|
||||
chasses_state[chassis_id] = blades_dict
|
||||
for blade_id in ucsm[chassis_id]:
|
||||
blade_data = self._get_initial_blade_state(chassis_id,
|
||||
blade_id,
|
||||
ucsm_ip,
|
||||
ucsm_username,
|
||||
ucsm_password)
|
||||
blades_dict[blade_id] = blade_data
|
||||
blade_data = ucsm[chassis_id][blade_id]
|
||||
blade_intf_data = blade_data[const.BLADE_INTF_DATA]
|
||||
for blade_intf in blade_intf_data.keys():
|
||||
if not blade_intf_data[blade_intf][const.PORTID] or \
|
||||
not blade_intf_data[blade_intf][const.TENANTID]:
|
||||
continue
|
||||
if blade_intf_data[blade_intf]\
|
||||
[const.BLADE_INTF_RESERVATION] == \
|
||||
const.BLADE_INTF_RESERVED and \
|
||||
blade_intf_data[blade_intf]\
|
||||
[const.TENANTID] == tenant_id and \
|
||||
blade_intf_data[blade_intf]\
|
||||
[const.PORTID] == port_id:
|
||||
interface_dn = blade_intf_data[blade_intf]\
|
||||
[const.BLADE_INTF_DN]
|
||||
blade_intf_info = {const.UCSM_IP: ucsm_ip,
|
||||
const.CHASSIS_ID: chassis_id,
|
||||
const.BLADE_ID: blade_id,
|
||||
const.BLADE_INTF_DN:
|
||||
interface_dn}
|
||||
return blade_intf_info
|
||||
LOG.warn("Could not find a reserved nic for tenant: %s port: %s" %
|
||||
(tenant_id, port_id))
|
||||
return None
|
||||
|
||||
LOG.debug("UCS Inventory state is: %s\n" % self._inventory_state)
|
||||
return True
|
||||
|
||||
def get_least_reserved_blade(self):
|
||||
def _get_least_reserved_blade(self, intf_count=1):
|
||||
"""Return the blade with least number of dynamic nics reserved"""
|
||||
unreserved_interface_count = 0
|
||||
least_reserved_blade_ucsm = None
|
||||
@ -415,8 +447,10 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
least_reserved_blade_id = blade_id
|
||||
least_reserved_blade_data = blade_data
|
||||
|
||||
if unreserved_interface_count == 0:
|
||||
LOG.warn("No more dynamic nics available for reservation")
|
||||
if unreserved_interface_count < intf_count:
|
||||
LOG.warn("Not enough dynamic nics available on a single host." \
|
||||
" Requested: %s, Maximum available: %s" %
|
||||
(intf_count, unreserved_interface_count))
|
||||
return False
|
||||
|
||||
least_reserved_blade_dict = \
|
||||
@ -428,6 +462,10 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
least_reserved_blade_dict)
|
||||
return least_reserved_blade_dict
|
||||
|
||||
def reload_inventory(self):
|
||||
"""Reload the inventory from a conf file"""
|
||||
self._load_inventory()
|
||||
|
||||
def reserve_blade_interface(self, ucsm_ip, chassis_id, blade_id,
|
||||
blade_data_dict, tenant_id, port_id,
|
||||
portprofile_name):
|
||||
@ -435,15 +473,19 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
ucsm_username = cred.Store.getUsername(ucsm_ip)
|
||||
ucsm_password = cred.Store.getPassword(ucsm_ip)
|
||||
"""
|
||||
We are first getting the updated blade interface state
|
||||
We are first getting the updated UCSM-specific blade
|
||||
interface state
|
||||
"""
|
||||
blade_data = self._get_blade_state(chassis_id, blade_id, ucsm_ip,
|
||||
ucsm_username, ucsm_password)
|
||||
blade_intf_data = blade_data[const.BLADE_INTF_DATA]
|
||||
old_blade_intf_data = blade_data_dict[const.BLADE_INTF_DATA]
|
||||
old_blade_intf_data = \
|
||||
self._inventory_state[ucsm_ip][chassis_id]\
|
||||
[blade_id][const.BLADE_INTF_DATA]
|
||||
|
||||
"""
|
||||
We will now copy the older blade interface state
|
||||
We will now copy the older non-UCSM-specific blade
|
||||
interface state
|
||||
"""
|
||||
for blade_intf in blade_intf_data.keys():
|
||||
blade_intf_data[blade_intf][const.BLADE_INTF_RESERVATION] = \
|
||||
@ -461,7 +503,8 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
old_blade_intf_data[blade_intf][const.VIF_ID]
|
||||
|
||||
blade_data[const.BLADE_UNRESERVED_INTF_COUNT] = \
|
||||
blade_data_dict[const.BLADE_UNRESERVED_INTF_COUNT]
|
||||
self._inventory_state[ucsm_ip][chassis_id]\
|
||||
[blade_id][const.BLADE_UNRESERVED_INTF_COUNT]
|
||||
"""
|
||||
Now we will reserve an interface if its available
|
||||
"""
|
||||
@ -498,7 +541,7 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
return reserved_nic_dict
|
||||
|
||||
LOG.warn("Dynamic nic %s could not be reserved for port-id: %s" %
|
||||
(blade_data_dict, port_id))
|
||||
(blade_data, port_id))
|
||||
return False
|
||||
|
||||
def unreserve_blade_interface(self, ucsm_ip, chassis_id, blade_id,
|
||||
@ -518,40 +561,6 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
blade_intf[const.VIF_ID] = None
|
||||
LOG.debug("Unreserved blade interface %s\n" % interface_dn)
|
||||
|
||||
def get_rsvd_blade_intf_by_port(self, tenant_id, port_id):
|
||||
"""
|
||||
Lookup a reserved blade interface based on tenant_id and port_id
|
||||
and return the blade interface info
|
||||
"""
|
||||
for ucsm_ip in self._inventory_state.keys():
|
||||
ucsm = self._inventory_state[ucsm_ip]
|
||||
for chassis_id in ucsm.keys():
|
||||
for blade_id in ucsm[chassis_id]:
|
||||
blade_data = ucsm[chassis_id][blade_id]
|
||||
blade_intf_data = blade_data[const.BLADE_INTF_DATA]
|
||||
for blade_intf in blade_intf_data.keys():
|
||||
if not blade_intf_data[blade_intf][const.PORTID] or \
|
||||
not blade_intf_data[blade_intf][const.TENANTID]:
|
||||
continue
|
||||
if blade_intf_data[blade_intf]\
|
||||
[const.BLADE_INTF_RESERVATION] == \
|
||||
const.BLADE_INTF_RESERVED and \
|
||||
blade_intf_data[blade_intf]\
|
||||
[const.TENANTID] == tenant_id and \
|
||||
blade_intf_data[blade_intf]\
|
||||
[const.PORTID] == port_id:
|
||||
interface_dn = blade_intf_data[blade_intf]\
|
||||
[const.BLADE_INTF_DN]
|
||||
blade_intf_info = {const.UCSM_IP: ucsm_ip,
|
||||
const.CHASSIS_ID: chassis_id,
|
||||
const.BLADE_ID: blade_id,
|
||||
const.BLADE_INTF_DN:
|
||||
interface_dn}
|
||||
return blade_intf_info
|
||||
LOG.warn("Could not find a reserved nic for tenant: %s port: %s" %
|
||||
(tenant_id, port_id))
|
||||
return None
|
||||
|
||||
def add_blade(self, ucsm_ip, chassis_id, blade_id):
|
||||
"""Add a blade to the inventory"""
|
||||
# TODO (Sumit)
|
||||
@ -593,7 +602,7 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
on which a dynamic vnic is available
|
||||
"""
|
||||
LOG.debug("create_port() called\n")
|
||||
least_reserved_blade_dict = self.get_least_reserved_blade()
|
||||
least_reserved_blade_dict = self._get_least_reserved_blade()
|
||||
if not least_reserved_blade_dict:
|
||||
raise cexc.NoMoreNics()
|
||||
ucsm_ip = least_reserved_blade_dict[const.LEAST_RSVD_BLADE_UCSM]
|
||||
@ -612,9 +621,11 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
tenant_id = args[0]
|
||||
net_id = args[1]
|
||||
port_id = args[2]
|
||||
rsvd_info = self.get_rsvd_blade_intf_by_port(tenant_id, port_id)
|
||||
rsvd_info = self._get_rsvd_blade_intf_by_port(tenant_id, port_id)
|
||||
if not rsvd_info:
|
||||
raise exc.PortNotFound(net_id=net_id, port_id=port_id)
|
||||
LOG.warn("UCSInventory: Port not found: net_id: %s, port_id: %s" %
|
||||
(net_id, port_id))
|
||||
return {const.DEVICE_IP: []}
|
||||
device_params = \
|
||||
{const.DEVICE_IP: [rsvd_info[const.UCSM_IP]],
|
||||
const.UCS_INVENTORY: self,
|
||||
@ -680,3 +691,20 @@ class UCSInventory(L2NetworkDeviceInventoryBase):
|
||||
vif_desc = {const.VIF_DESC: vif_info}
|
||||
LOG.debug("vif_desc is: %s" % vif_desc)
|
||||
return vif_desc
|
||||
|
||||
def create_multiport(self, args):
|
||||
"""
|
||||
Create multiple ports for a VM
|
||||
"""
|
||||
LOG.debug("create_ports() called\n")
|
||||
tenant_id = args[0]
|
||||
ports_num = args[2]
|
||||
least_reserved_blade_dict = self._get_least_reserved_blade(ports_num)
|
||||
if not least_reserved_blade_dict:
|
||||
raise cexc.NoMoreNics()
|
||||
ucsm_ip = least_reserved_blade_dict[const.LEAST_RSVD_BLADE_UCSM]
|
||||
device_params = {const.DEVICE_IP: [ucsm_ip],
|
||||
const.UCS_INVENTORY: self,
|
||||
const.LEAST_RSVD_BLADE_DICT:\
|
||||
least_reserved_blade_dict}
|
||||
return device_params
|
||||
|
@ -145,7 +145,7 @@ class CiscoUCSMDriver():
|
||||
|
||||
def _post_data(self, ucsm_ip, ucsm_username, ucsm_password, data):
|
||||
"""Send command to UCSM in http request"""
|
||||
conn = httplib.HTTPConnection(ucsm_ip)
|
||||
conn = httplib.HTTPSConnection(ucsm_ip)
|
||||
login_data = "<aaaLogin inName=\"" + ucsm_username + \
|
||||
"\" inPassword=\"" + ucsm_password + "\" />"
|
||||
conn.request(METHOD, URL, login_data, HEADERS)
|
||||
|
@ -296,3 +296,36 @@ class UCSVICPlugin(L2DevicePluginBase):
|
||||
self._ucsm_ip = ucsm_ip
|
||||
self._ucsm_username = cred.Store.getUsername(conf.UCSM_IP_ADDRESS)
|
||||
self._ucsm_password = cred.Store.getPassword(conf.UCSM_IP_ADDRESS)
|
||||
|
||||
def create_multiport(self, tenant_id, net_id_list, ports_num, port_id_list,
|
||||
**kwargs):
|
||||
"""
|
||||
Creates a port on the specified Virtual Network.
|
||||
"""
|
||||
LOG.debug("UCSVICPlugin:create_multiport() called\n")
|
||||
self._set_ucsm(kwargs[const.DEVICE_IP])
|
||||
qos = None
|
||||
ucs_inventory = kwargs[const.UCS_INVENTORY]
|
||||
least_rsvd_blade_dict = kwargs[const.LEAST_RSVD_BLADE_DICT]
|
||||
chassis_id = least_rsvd_blade_dict[const.LEAST_RSVD_BLADE_CHASSIS]
|
||||
blade_id = least_rsvd_blade_dict[const.LEAST_RSVD_BLADE_ID]
|
||||
blade_data_dict = least_rsvd_blade_dict[const.LEAST_RSVD_BLADE_DATA]
|
||||
port_binding_list = []
|
||||
for port_id, net_id in zip(port_id_list, net_id_list):
|
||||
new_port_profile = \
|
||||
self._create_port_profile(tenant_id, net_id, port_id,
|
||||
conf.DEFAULT_VLAN_NAME,
|
||||
conf.DEFAULT_VLAN_ID)
|
||||
profile_name = new_port_profile[const.PROFILE_NAME]
|
||||
rsvd_nic_dict = ucs_inventory.\
|
||||
reserve_blade_interface(self._ucsm_ip, chassis_id,
|
||||
blade_id, blade_data_dict,
|
||||
tenant_id, port_id,
|
||||
profile_name)
|
||||
port_binding = udb.update_portbinding(port_id,
|
||||
portprofile_name=profile_name,
|
||||
vlan_name=conf.DEFAULT_VLAN_NAME,
|
||||
vlan_id=conf.DEFAULT_VLAN_ID,
|
||||
qos=qos)
|
||||
port_binding_list.append(port_binding)
|
||||
return port_binding_list
|
||||
|
190
tests/unit/database_stubs.py
Normal file
190
tests/unit/database_stubs.py
Normal file
@ -0,0 +1,190 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011, Cisco Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# @author: Rohit Agarwalla, Cisco Systems, Inc.
|
||||
|
||||
"""
|
||||
stubs.py provides interface methods for
|
||||
the database test cases
|
||||
"""
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
from quantum.db import api as db
|
||||
|
||||
|
||||
LOG = logging.getLogger('quantum.tests.database_stubs')
|
||||
|
||||
|
||||
class QuantumDB(object):
|
||||
"""Class conisting of methods to call Quantum db methods"""
|
||||
def get_all_networks(self, tenant_id):
|
||||
"""Get all networks"""
|
||||
nets = []
|
||||
try:
|
||||
for net in db.network_list(tenant_id):
|
||||
LOG.debug("Getting network: %s", net.uuid)
|
||||
net_dict = {}
|
||||
net_dict["tenant_id"] = net.tenant_id
|
||||
net_dict["id"] = str(net.uuid)
|
||||
net_dict["name"] = net.name
|
||||
nets.append(net_dict)
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to get all networks: %s", str(exc))
|
||||
return nets
|
||||
|
||||
def get_network(self, network_id):
|
||||
"""Get a network"""
|
||||
net = []
|
||||
try:
|
||||
for net in db.network_get(network_id):
|
||||
LOG.debug("Getting network: %s", net.uuid)
|
||||
net_dict = {}
|
||||
net_dict["tenant_id"] = net.tenant_id
|
||||
net_dict["id"] = str(net.uuid)
|
||||
net_dict["name"] = net.name
|
||||
net.append(net_dict)
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to get network: %s", str(exc))
|
||||
return net
|
||||
|
||||
def create_network(self, tenant_id, net_name):
|
||||
"""Create a network"""
|
||||
net_dict = {}
|
||||
try:
|
||||
res = db.network_create(tenant_id, net_name)
|
||||
LOG.debug("Created network: %s", res.uuid)
|
||||
net_dict["tenant_id"] = res.tenant_id
|
||||
net_dict["id"] = str(res.uuid)
|
||||
net_dict["name"] = res.name
|
||||
return net_dict
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to create network: %s", str(exc))
|
||||
|
||||
def delete_network(self, net_id):
|
||||
"""Delete a network"""
|
||||
try:
|
||||
net = db.network_destroy(net_id)
|
||||
LOG.debug("Deleted network: %s", net.uuid)
|
||||
net_dict = {}
|
||||
net_dict["id"] = str(net.uuid)
|
||||
return net_dict
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to delete network: %s", str(exc))
|
||||
|
||||
def rename_network(self, tenant_id, net_id, new_name):
|
||||
"""Rename a network"""
|
||||
try:
|
||||
net = db.network_rename(net_id, tenant_id, new_name)
|
||||
LOG.debug("Renamed network: %s", net.uuid)
|
||||
net_dict = {}
|
||||
net_dict["id"] = str(net.uuid)
|
||||
net_dict["name"] = net.name
|
||||
return net_dict
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to rename network: %s", str(exc))
|
||||
|
||||
def get_all_ports(self, net_id):
|
||||
"""Get all ports"""
|
||||
ports = []
|
||||
try:
|
||||
for port in db.port_list(net_id):
|
||||
LOG.debug("Getting port: %s", port.uuid)
|
||||
port_dict = {}
|
||||
port_dict["id"] = str(port.uuid)
|
||||
port_dict["net-id"] = str(port.network_id)
|
||||
port_dict["attachment"] = port.interface_id
|
||||
port_dict["state"] = port.state
|
||||
ports.append(port_dict)
|
||||
return ports
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to get all ports: %s", str(exc))
|
||||
|
||||
def get_port(self, net_id, port_id):
|
||||
"""Get a port"""
|
||||
port_list = []
|
||||
port = db.port_get(port_id, net_id)
|
||||
try:
|
||||
LOG.debug("Getting port: %s", port.uuid)
|
||||
port_dict = {}
|
||||
port_dict["id"] = str(port.uuid)
|
||||
port_dict["net-id"] = str(port.network_id)
|
||||
port_dict["attachment"] = port.interface_id
|
||||
port_dict["state"] = port.state
|
||||
port_list.append(port_dict)
|
||||
return port_list
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to get port: %s", str(exc))
|
||||
|
||||
def create_port(self, net_id):
|
||||
"""Add a port"""
|
||||
port_dict = {}
|
||||
try:
|
||||
port = db.port_create(net_id)
|
||||
LOG.debug("Creating port %s", port.uuid)
|
||||
port_dict["id"] = str(port.uuid)
|
||||
port_dict["net-id"] = str(port.network_id)
|
||||
port_dict["attachment"] = port.interface_id
|
||||
port_dict["state"] = port.state
|
||||
return port_dict
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to create port: %s", str(exc))
|
||||
|
||||
def delete_port(self, net_id, port_id):
|
||||
"""Delete a port"""
|
||||
try:
|
||||
port = db.port_destroy(port_id, net_id)
|
||||
LOG.debug("Deleted port %s", port.uuid)
|
||||
port_dict = {}
|
||||
port_dict["id"] = str(port.uuid)
|
||||
return port_dict
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to delete port: %s", str(exc))
|
||||
|
||||
def update_port(self, net_id, port_id, port_state):
|
||||
"""Update a port"""
|
||||
try:
|
||||
port = db.port_set_state(net_id, port_id, port_state)
|
||||
LOG.debug("Updated port %s", port.uuid)
|
||||
port_dict = {}
|
||||
port_dict["id"] = str(port.uuid)
|
||||
port_dict["net-id"] = str(port.network_id)
|
||||
port_dict["attachment"] = port.interface_id
|
||||
port_dict["state"] = port.state
|
||||
return port_dict
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to update port state: %s", str(exc))
|
||||
|
||||
def plug_interface(self, net_id, port_id, int_id):
|
||||
"""Plug interface to a port"""
|
||||
try:
|
||||
port = db.port_set_attachment(port_id, net_id, int_id)
|
||||
LOG.debug("Attached interface to port %s", port.uuid)
|
||||
port_dict = {}
|
||||
port_dict["id"] = str(port.uuid)
|
||||
port_dict["net-id"] = str(port.network_id)
|
||||
port_dict["attachment"] = port.interface_id
|
||||
port_dict["state"] = port.state
|
||||
return port_dict
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to plug interface: %s", str(exc))
|
||||
|
||||
def unplug_interface(self, net_id, port_id):
|
||||
"""Unplug interface to a port"""
|
||||
try:
|
||||
db.port_unset_attachment(port_id, net_id)
|
||||
LOG.debug("Detached interface from port %s", port_id)
|
||||
except Exception, exc:
|
||||
LOG.error("Failed to unplug interface: %s", str(exc))
|
@ -33,7 +33,7 @@ LOG = logging.getLogger('quantum.tests.test_api')
|
||||
class APITest(unittest.TestCase):
|
||||
|
||||
def _create_network(self, format, name=None, custom_req_body=None,
|
||||
expected_res_status=200):
|
||||
expected_res_status=202):
|
||||
LOG.debug("Creating network")
|
||||
content_type = "application/" + format
|
||||
if name:
|
||||
@ -45,13 +45,13 @@ class APITest(unittest.TestCase):
|
||||
custom_req_body)
|
||||
network_res = network_req.get_response(self.api)
|
||||
self.assertEqual(network_res.status_int, expected_res_status)
|
||||
if expected_res_status == 200:
|
||||
if expected_res_status in (200, 202):
|
||||
network_data = Serializer().deserialize(network_res.body,
|
||||
content_type)
|
||||
return network_data['network']['id']
|
||||
|
||||
def _create_port(self, network_id, port_state, format,
|
||||
custom_req_body=None, expected_res_status=200):
|
||||
custom_req_body=None, expected_res_status=202):
|
||||
LOG.debug("Creating port for network %s", network_id)
|
||||
content_type = "application/%s" % format
|
||||
port_req = testlib.new_port_request(self.tenant_id, network_id,
|
||||
@ -59,7 +59,7 @@ class APITest(unittest.TestCase):
|
||||
custom_req_body)
|
||||
port_res = port_req.get_response(self.api)
|
||||
self.assertEqual(port_res.status_int, expected_res_status)
|
||||
if expected_res_status == 200:
|
||||
if expected_res_status in (200, 202):
|
||||
port_data = Serializer().deserialize(port_res.body, content_type)
|
||||
return port_data['port']['id']
|
||||
|
||||
@ -298,6 +298,15 @@ class APITest(unittest.TestCase):
|
||||
self.assertEqual(len(port_data['ports']), 2)
|
||||
LOG.debug("_test_list_ports - format:%s - END", format)
|
||||
|
||||
def _test_list_ports_networknotfound(self, format):
|
||||
LOG.debug("_test_list_ports_networknotfound"
|
||||
" - format:%s - START", format)
|
||||
list_port_req = testlib.port_list_request(self.tenant_id,
|
||||
"A_BAD_ID", format)
|
||||
list_port_res = list_port_req.get_response(self.api)
|
||||
self.assertEqual(list_port_res.status_int, 420)
|
||||
LOG.debug("_test_list_ports_networknotfound - format:%s - END", format)
|
||||
|
||||
def _test_list_ports_detail(self, format):
|
||||
LOG.debug("_test_list_ports_detail - format:%s - START", format)
|
||||
content_type = "application/%s" % format
|
||||
@ -882,6 +891,12 @@ class APITest(unittest.TestCase):
|
||||
def test_list_ports_xml(self):
|
||||
self._test_list_ports('xml')
|
||||
|
||||
def test_list_ports_networknotfound_json(self):
|
||||
self._test_list_ports_networknotfound('json')
|
||||
|
||||
def test_list_ports_networknotfound_xml(self):
|
||||
self._test_list_ports_networknotfound('xml')
|
||||
|
||||
def test_list_ports_detail_json(self):
|
||||
self._test_list_ports_detail('json')
|
||||
|
||||
|
117
tests/unit/test_database.py
Normal file
117
tests/unit/test_database.py
Normal file
@ -0,0 +1,117 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011, Cisco Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# @author: Rohit Agarwalla, Cisco Systems, Inc.
|
||||
|
||||
"""
|
||||
test_database.py is an independent test suite
|
||||
that tests the database api method calls
|
||||
"""
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
|
||||
from quantum.db import api as db
|
||||
from tests.unit import database_stubs as db_stubs
|
||||
|
||||
|
||||
LOG = logging.getLogger('quantum.tests.test_database')
|
||||
|
||||
|
||||
class QuantumDBTest(unittest.TestCase):
|
||||
"""Class consisting of Quantum DB unit tests"""
|
||||
def setUp(self):
|
||||
"""Setup for tests"""
|
||||
db.configure_db({'sql_connection': 'sqlite:///:memory:'})
|
||||
self.dbtest = db_stubs.QuantumDB()
|
||||
self.tenant_id = "t1"
|
||||
LOG.debug("Setup")
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear Down"""
|
||||
db.clear_db()
|
||||
|
||||
def testa_create_network(self):
|
||||
"""test to create network"""
|
||||
net1 = self.dbtest.create_network(self.tenant_id, "plugin_test1")
|
||||
self.assertTrue(net1["name"] == "plugin_test1")
|
||||
|
||||
def testb_get_networks(self):
|
||||
"""test to get all networks"""
|
||||
net1 = self.dbtest.create_network(self.tenant_id, "plugin_test1")
|
||||
self.assertTrue(net1["name"] == "plugin_test1")
|
||||
net2 = self.dbtest.create_network(self.tenant_id, "plugin_test2")
|
||||
self.assertTrue(net2["name"] == "plugin_test2")
|
||||
nets = self.dbtest.get_all_networks(self.tenant_id)
|
||||
count = 0
|
||||
for net in nets:
|
||||
if "plugin_test" in net["name"]:
|
||||
count += 1
|
||||
self.assertTrue(count == 2)
|
||||
|
||||
def testc_delete_network(self):
|
||||
"""test to delete network"""
|
||||
net1 = self.dbtest.create_network(self.tenant_id, "plugin_test1")
|
||||
self.assertTrue(net1["name"] == "plugin_test1")
|
||||
self.dbtest.delete_network(net1["id"])
|
||||
nets = self.dbtest.get_all_networks(self.tenant_id)
|
||||
count = len(nets)
|
||||
self.assertTrue(count == 0)
|
||||
|
||||
def testd_rename_network(self):
|
||||
"""test to rename network"""
|
||||
net1 = self.dbtest.create_network(self.tenant_id, "plugin_test1")
|
||||
self.assertTrue(net1["name"] == "plugin_test1")
|
||||
net = self.dbtest.rename_network(self.tenant_id, net1["id"],
|
||||
"plugin_test1_renamed")
|
||||
self.assertTrue(net["name"] == "plugin_test1_renamed")
|
||||
|
||||
def teste_create_port(self):
|
||||
"""test to create port"""
|
||||
net1 = self.dbtest.create_network(self.tenant_id, "plugin_test1")
|
||||
port = self.dbtest.create_port(net1["id"])
|
||||
self.assertTrue(port["net-id"] == net1["id"])
|
||||
|
||||
def testf_get_ports(self):
|
||||
"""test to get ports"""
|
||||
net1 = self.dbtest.create_network(self.tenant_id, "plugin_test1")
|
||||
port = self.dbtest.create_port(net1["id"])
|
||||
self.assertTrue(port["net-id"] == net1["id"])
|
||||
ports = self.dbtest.get_all_ports(net1["id"])
|
||||
count = len(ports)
|
||||
self.assertTrue(count == 1)
|
||||
|
||||
def testf_delete_port(self):
|
||||
"""test to delete port"""
|
||||
net1 = self.dbtest.create_network(self.tenant_id, "plugin_test1")
|
||||
port = self.dbtest.create_port(net1["id"])
|
||||
self.assertTrue(port["net-id"] == net1["id"])
|
||||
ports = self.dbtest.get_all_ports(net1["id"])
|
||||
for por in ports:
|
||||
self.dbtest.delete_port(net1["id"], por["id"])
|
||||
ports = self.dbtest.get_all_ports(net1["id"])
|
||||
count = len(ports)
|
||||
self.assertTrue(count == 0)
|
||||
|
||||
def testg_plug_unplug_interface(self):
|
||||
"""test to plug/unplug interface"""
|
||||
net1 = self.dbtest.create_network(self.tenant_id, "plugin_test1")
|
||||
port1 = self.dbtest.create_port(net1["id"])
|
||||
self.dbtest.plug_interface(net1["id"], port1["id"], "vif1.1")
|
||||
port = self.dbtest.get_port(net1["id"], port1["id"])
|
||||
self.assertTrue(port[0]["attachment"] == "vif1.1")
|
||||
self.dbtest.unplug_interface(net1["id"], port1["id"])
|
||||
port = self.dbtest.get_port(net1["id"], port1["id"])
|
||||
self.assertTrue(port[0]["attachment"] == None)
|
Loading…
x
Reference in New Issue
Block a user