Merge trunk

This commit is contained in:
Salvatore Orlando 2011-09-09 21:21:23 +01:00
commit 6554f92989
34 changed files with 1962 additions and 232 deletions

63
README
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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