diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..cd84ef96b --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = quantumclient +omit = quantumclient/openstack/* + +[report] +ignore-errors = True diff --git a/.gitignore b/.gitignore index 1fe7c256e..841355240 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,20 @@ *.pyc *.DS_Store +*.egg +AUTHORS +ChangeLog build/* build-stamp -.coverage cover/* -python_quantumclient.egg-info/* -quantum/vcsversion.py +doc/build/ +doc/source/api/ +python_neutronclient.egg-info/* +neutron/vcsversion.py +neutronclient/versioninfo run_tests.err.log run_tests.log -tests/ -.venv/ +.autogenerated +.coverage +.testrepository/ .tox/ +.venv/ diff --git a/.gitreview b/.gitreview index 40817febd..994b07a43 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] host=review.openstack.org port=29418 -project=openstack/python-quantumclient.git +project=openstack/python-neutronclient.git diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 000000000..2109af6ce --- /dev/null +++ b/.testr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 000000000..ea5e86b7b --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,206 @@ +Neutron Style Commandments +================================ + +- Step 1: Read http://www.python.org/dev/peps/pep-0008/ +- Step 2: Read http://www.python.org/dev/peps/pep-0008/ again +- Step 3: Read on + + +General +------- +- Put two newlines between top-level code (funcs, classes, etc) +- Put one newline between methods in classes and anywhere else +- Do not write "except:", use "except Exception:" at the very least +- Include your name with TODOs as in "#TODO(termie)" +- Do not shadow a built-in or reserved word. Example:: + + def list(): + return [1, 2, 3] + + mylist = list() # BAD, shadows `list` built-in + + class Foo(object): + def list(self): + return [1, 2, 3] + + mylist = Foo().list() # OKAY, does not shadow built-in + + +Imports +------- +- Do not make relative imports +- Order your imports by the full module path +- Organize your imports according to the following template + +Example:: + + # vim: tabstop=4 shiftwidth=4 softtabstop=4 + {{stdlib imports in human alphabetical order}} + \n + {{third-party lib imports in human alphabetical order}} + \n + {{neutron imports in human alphabetical order}} + \n + \n + {{begin your code}} + + +Human Alphabetical Order Examples +--------------------------------- +Example:: + + import httplib + import logging + import random + import StringIO + import time + import unittest + + import eventlet + import webob.exc + + import neutron.api.networks + from neutron.api import ports + from neutron.db import models + from neutron.extensions import multiport + import neutron.manager + from neutron import service + + +Docstrings +---------- +Example:: + + """A one line docstring looks like this and ends in a period.""" + + + """A multiline docstring has a one-line summary, less than 80 characters. + + Then a new paragraph after a newline that explains in more detail any + general information about the function, class or method. Example usages + are also great to have here if it is a complex class for function. + + When writing the docstring for a class, an extra line should be placed + after the closing quotations. For more in-depth explanations for these + decisions see http://www.python.org/dev/peps/pep-0257/ + + If you are going to describe parameters and return values, use Sphinx, the + appropriate syntax is as follows. + + :param foo: the foo parameter + :param bar: the bar parameter + :returns: return_type -- description of the return value + :returns: description of the return value + :raises: AttributeError, KeyError + """ + + +Dictionaries/Lists +------------------ +If a dictionary (dict) or list object is longer than 80 characters, its items +should be split with newlines. Embedded iterables should have their items +indented. Additionally, the last item in the dictionary should have a trailing +comma. This increases readability and simplifies future diffs. + +Example:: + + my_dictionary = { + "image": { + "name": "Just a Snapshot", + "size": 2749573, + "properties": { + "user_id": 12, + "arch": "x86_64", + }, + "things": [ + "thing_one", + "thing_two", + ], + "status": "ACTIVE", + }, + } + + +Calling Methods +--------------- +Calls to methods 80 characters or longer should format each argument with +newlines. This is not a requirement, but a guideline:: + + unnecessarily_long_function_name('string one', + 'string two', + kwarg1=constants.ACTIVE, + kwarg2=['a', 'b', 'c']) + + +Rather than constructing parameters inline, it is better to break things up:: + + list_of_strings = [ + 'what_a_long_string', + 'not as long', + ] + + dict_of_numbers = { + 'one': 1, + 'two': 2, + 'twenty four': 24, + } + + object_one.call_a_method('string three', + 'string four', + kwarg1=list_of_strings, + kwarg2=dict_of_numbers) + + +Internationalization (i18n) Strings +----------------------------------- +In order to support multiple languages, we have a mechanism to support +automatic translations of exception and log strings. + +Example:: + + msg = _("An error occurred") + raise HTTPBadRequest(explanation=msg) + +If you have a variable to place within the string, first internationalize the +template string then do the replacement. + +Example:: + + msg = _("Missing parameter: %s") % ("flavor",) + LOG.error(msg) + +If you have multiple variables to place in the string, use keyword parameters. +This helps our translators reorder parameters when needed. + +Example:: + + msg = _("The server with id %(s_id)s has no key %(m_key)s") + LOG.error(msg % {"s_id": "1234", "m_key": "imageId"}) + + +Creating Unit Tests +------------------- +For every new feature, unit tests should be created that both test and +(implicitly) document the usage of said feature. If submitting a patch for a +bug that had no unit test, a new passing unit test should be added. If a +submitted bug fix does have a unit test, be sure to add a new one that fails +without the patch and passes with the patch. + +Running Tests +------------- +The testing system is based on a combination of tox and testr. The canonical +approach to running tests is to simply run the command `tox`. This will +create virtual environments, populate them with depenedencies and run all of +the tests that OpenStack CI systems run. Behind the scenes, tox is running +`testr run --parallel`, but is set up such that you can supply any additional +testr arguments that are needed to tox. For example, you can run: +`tox -- --analyze-isolation` to cause tox to tell testr to add +--analyze-isolation to its argument list. + +It is also possible to run the tests inside of a virtual environment +you have created, or it is possible that you have all of the dependencies +installed locally already. In this case, you can interact with the testr +command directly. Running `testr run` will run the entire test suite. `testr +run --parallel` will run it in parallel (this is the default incantation tox +uses.) More information about testr can be found at: +http://wiki.openstack.org/testr diff --git a/MANIFEST.in b/MANIFEST.in index 8aa75aee5..c0f014e74 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include tox.ini -include LICENSE README -include version.py +include LICENSE README.rst HACKING.rst +include AUTHORS +include ChangeLog include tools/* -include quantum/client/tests/* -include quantum/client/tests/unit/* +recursive-include tests * diff --git a/README b/README deleted file mode 100644 index 20b7a8978..000000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -This is the client API library for Quantum. diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..ea8e44c88 --- /dev/null +++ b/README.rst @@ -0,0 +1 @@ +This is the client API library for Neutron. diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 000000000..1d6023e2d --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# + +import sys +import os + +project = 'python-neutronclient' + +# -- General configuration --------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +copyright = u'OpenStack LLC' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output --------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'nature' + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, +# documentclass [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack LLC', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 000000000..612a41ee1 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,63 @@ +Python bindings to the OpenStack Network API +============================================ + +In order to use the python neutron client directly, you must first obtain an auth token and identify which endpoint you wish to speak to. Once you have done so, you can use the API like so:: + + >>> import logging + >>> from neutronclient.neutron import client + >>> logging.basicConfig(level=logging.DEBUG) + >>> neutron = client.Client('2.0', endpoint_url=OS_URL, token=OS_TOKEN) + >>> neutron.format = 'json' + >>> network = {'name': 'mynetwork', 'admin_state_up': True} + >>> neutron.create_network({'network':network}) + >>> networks = neutron.list_networks(name='mynetwork') + >>> print networks + >>> network_id = networks['networks'][0]['id'] + >>> neutron.delete_network(network_id) + + +Command-line Tool +================= +In order to use the CLI, you must provide your OpenStack username, password, tenant, and auth endpoint. Use the corresponding configuration options (``--os-username``, ``--os-password``, ``--os-tenant-name``, and ``--os-auth-url``) or set them in environment variables:: + + export OS_USERNAME=user + export OS_PASSWORD=pass + export OS_TENANT_NAME=tenant + export OS_AUTH_URL=http://auth.example.com:5000/v2.0 + +The command line tool will attempt to reauthenticate using your provided credentials for every request. You can override this behavior by manually supplying an auth token using ``--os-url`` and ``--os-auth-token``. You can alternatively set these environment variables:: + + export OS_URL=http://neutron.example.org:9696/ + export OS_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 + +If neutron server does not require authentication, besides these two arguments or environment variables (We can use any value as token.), we need manually supply ``--os-auth-strategy`` or set the environment variable:: + + export OS_AUTH_STRATEGY=noauth + +Once you've configured your authentication parameters, you can run ``neutron -h`` to see a complete listing of available commands. + +Release Notes +============= + +2.0 +----- +* support Neutron API 2.0 + +2.2.0 +----- +* add security group commands +* add Lbaas commands +* allow options put after positional arguments +* add NVP queue and net gateway commands +* add commands for agent management extensions +* add commands for DHCP and L3 agents scheduling +* support XML request format +* support pagination options + +2.2.2 +----- +* improved support for listing a large number of filtered subnets +* add --endpoint-type and OS_ENDPOINT_TYPE to shell client +* made the publicURL the default endpoint instead of adminURL +* add ability to update security group name (requires 2013.2-Havana or later) +* add flake8 and pbr support for testing and building diff --git a/neutron_test.sh b/neutron_test.sh new file mode 100755 index 000000000..53aee7cf4 --- /dev/null +++ b/neutron_test.sh @@ -0,0 +1,128 @@ +#!/bin/bash +set -x +function die() { + local exitcode=$? + set +o xtrace + echo $@ + exit $exitcode +} + +noauth_tenant_id=me +if [ $1 == 'noauth' ]; then + NOAUTH="--tenant_id $noauth_tenant_id" +else + NOAUTH= +fi + +FORMAT=" --request-format xml" + +# test the CRUD of network +network=mynet1 +neutron net-create $FORMAT $NOAUTH $network || die "fail to create network $network" +temp=`neutron net-list $FORMAT -- --name $network --fields id | wc -l` +echo $temp +if [ $temp -ne 5 ]; then + die "networks with name $network is not unique or found" +fi +network_id=`neutron net-list -- --name $network --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` +echo "ID of network with name $network is $network_id" + +neutron net-show $FORMAT $network || die "fail to show network $network" +neutron net-show $FORMAT $network_id || die "fail to show network $network_id" + +neutron net-update $FORMAT $network --admin_state_up False || die "fail to update network $network" +neutron net-update $FORMAT $network_id --admin_state_up True || die "fail to update network $network_id" + +neutron net-list $FORMAT -c id -- --id fakeid || die "fail to list networks with column selection on empty list" + +# test the CRUD of subnet +subnet=mysubnet1 +cidr=10.0.1.3/24 +neutron subnet-create $FORMAT $NOAUTH $network $cidr --name $subnet || die "fail to create subnet $subnet" +tempsubnet=`neutron subnet-list $FORMAT -- --name $subnet --fields id | wc -l` +echo $tempsubnet +if [ $tempsubnet -ne 5 ]; then + die "subnets with name $subnet is not unique or found" +fi +subnet_id=`neutron subnet-list $FORMAT -- --name $subnet --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` +echo "ID of subnet with name $subnet is $subnet_id" +neutron subnet-show $FORMAT $subnet || die "fail to show subnet $subnet" +neutron subnet-show $FORMAT $subnet_id || die "fail to show subnet $subnet_id" + +neutron subnet-update $FORMAT $subnet --dns_namesevers host1 || die "fail to update subnet $subnet" +neutron subnet-update $FORMAT $subnet_id --dns_namesevers host2 || die "fail to update subnet $subnet_id" + +# test the crud of ports +port=myport1 +neutron port-create $FORMAT $NOAUTH $network --name $port || die "fail to create port $port" +tempport=`neutron port-list $FORMAT -- --name $port --fields id | wc -l` +echo $tempport +if [ $tempport -ne 5 ]; then + die "ports with name $port is not unique or found" +fi +port_id=`neutron port-list $FORMAT -- --name $port --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` +echo "ID of port with name $port is $port_id" +neutron port-show $FORMAT $port || die "fail to show port $port" +neutron port-show $FORMAT $port_id || die "fail to show port $port_id" + +neutron port-update $FORMAT $port --device_id deviceid1 || die "fail to update port $port" +neutron port-update $FORMAT $port_id --device_id deviceid2 || die "fail to update port $port_id" + +# test quota commands RUD +DEFAULT_NETWORKS=10 +DEFAULT_PORTS=50 +tenant_id=tenant_a +tenant_id_b=tenant_b +neutron quota-update $FORMAT --tenant_id $tenant_id --network 30 || die "fail to update quota for tenant $tenant_id" +neutron quota-update $FORMAT --tenant_id $tenant_id_b --network 20 || die "fail to update quota for tenant $tenant_id" +networks=`neutron quota-list $FORMAT -c network -c tenant_id | grep $tenant_id | awk '{print $2}'` +if [ $networks -ne 30 ]; then + die "networks quota should be 30" +fi +networks=`neutron quota-list $FORMAT -c network -c tenant_id | grep $tenant_id_b | awk '{print $2}'` +if [ $networks -ne 20 ]; then + die "networks quota should be 20" +fi +networks=`neutron quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` +if [ $networks -ne 30 ]; then + die "networks quota should be 30" +fi +neutron quota-delete $FORMAT --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id" +networks=`neutron quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` +if [ $networks -ne $DEFAULT_NETWORKS ]; then + die "networks quota should be $DEFAULT_NETWORKS" +fi +# update self +if [ "t$NOAUTH" = "t" ]; then + # with auth + neutron quota-update $FORMAT --port 99 || die "fail to update quota for self" + ports=`neutron quota-show $FORMAT | grep port | awk -F'|' '{print $3}'` + if [ $ports -ne 99 ]; then + die "ports quota should be 99" + fi + + ports=`neutron quota-list $FORMAT -c port | grep 99 | awk '{print $2}'` + if [ $ports -ne 99 ]; then + die "ports quota should be 99" + fi + neutron quota-delete $FORMAT || die "fail to delete quota for tenant self" + ports=`neutron quota-show $FORMAT | grep port | awk -F'|' '{print $3}'` + if [ $ports -ne $DEFAULT_PORTS ]; then + die "ports quota should be $DEFAULT_PORTS" + fi +else + # without auth + neutron quota-update $FORMAT --port 100 + if [ $? -eq 0 ]; then + die "without valid context on server, quota update command should fail." + fi + neutron quota-show $FORMAT + if [ $? -eq 0 ]; then + die "without valid context on server, quota show command should fail." + fi + neutron quota-delete $FORMAT + if [ $? -eq 0 ]; then + die "without valid context on server, quota delete command should fail." + fi + neutron quota-list $FORMAT || die "fail to update quota for self" +fi diff --git a/quantum/common/__init__.py b/neutronclient/__init__.py similarity index 88% rename from quantum/common/__init__.py rename to neutronclient/__init__.py index 7e695ff08..034d66eaf 100644 --- a/quantum/common/__init__.py +++ b/neutronclient/__init__.py @@ -1,5 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 Nicira Networks, Inc. + +# Copyright 2011 Citrix Systems # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,4 +14,4 @@ # 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: Somik Behera, Nicira Networks, Inc. +# @author: Tyler Smith, Cisco Systems diff --git a/neutronclient/client.py b/neutronclient/client.py new file mode 100644 index 000000000..fefa07b63 --- /dev/null +++ b/neutronclient/client.py @@ -0,0 +1,262 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +try: + import json +except ImportError: + import simplejson as json +import logging +import os +import urlparse +# Python 2.5 compat fix +if not hasattr(urlparse, 'parse_qsl'): + import cgi + urlparse.parse_qsl = cgi.parse_qsl + +import httplib2 + +from neutronclient.common import exceptions +from neutronclient.common import utils + +_logger = logging.getLogger(__name__) + + +if os.environ.get('NEUTRONCLIENT_DEBUG'): + ch = logging.StreamHandler() + _logger.setLevel(logging.DEBUG) + _logger.addHandler(ch) + + +class ServiceCatalog(object): + """Helper methods for dealing with a Keystone Service Catalog.""" + + def __init__(self, resource_dict): + self.catalog = resource_dict + + def get_token(self): + """Fetch token details fron service catalog.""" + token = {'id': self.catalog['access']['token']['id'], + 'expires': self.catalog['access']['token']['expires'], } + try: + token['user_id'] = self.catalog['access']['user']['id'] + token['tenant_id'] = ( + self.catalog['access']['token']['tenant']['id']) + except Exception: + # just leave the tenant and user out if it doesn't exist + pass + return token + + def url_for(self, attr=None, filter_value=None, + service_type='network', endpoint_type='publicURL'): + """Fetch the URL from the Neutron service for + a particular endpoint type. If none given, return + publicURL. + """ + + catalog = self.catalog['access'].get('serviceCatalog', []) + matching_endpoints = [] + for service in catalog: + if service['type'] != service_type: + continue + + endpoints = service['endpoints'] + for endpoint in endpoints: + if not filter_value or endpoint.get(attr) == filter_value: + matching_endpoints.append(endpoint) + + if not matching_endpoints: + raise exceptions.EndpointNotFound() + elif len(matching_endpoints) > 1: + raise exceptions.AmbiguousEndpoints(message=matching_endpoints) + else: + if endpoint_type not in matching_endpoints[0]: + raise exceptions.EndpointTypeNotFound(message=endpoint_type) + + return matching_endpoints[0][endpoint_type] + + +class HTTPClient(httplib2.Http): + """Handles the REST calls and responses, include authn.""" + + USER_AGENT = 'python-neutronclient' + + def __init__(self, username=None, tenant_name=None, tenant_id=None, + password=None, auth_url=None, + token=None, region_name=None, timeout=None, + endpoint_url=None, insecure=False, + endpoint_type='publicURL', + auth_strategy='keystone', **kwargs): + super(HTTPClient, self).__init__(timeout=timeout) + self.username = username + self.tenant_name = tenant_name + self.tenant_id = tenant_id + self.password = password + self.auth_url = auth_url.rstrip('/') if auth_url else None + self.endpoint_type = endpoint_type + self.region_name = region_name + self.auth_token = token + self.content_type = 'application/json' + self.endpoint_url = endpoint_url + self.auth_strategy = auth_strategy + # httplib2 overrides + self.disable_ssl_certificate_validation = insecure + + def _cs_request(self, *args, **kwargs): + kargs = {} + kargs.setdefault('headers', kwargs.get('headers', {})) + kargs['headers']['User-Agent'] = self.USER_AGENT + + if 'content_type' in kwargs: + kargs['headers']['Content-Type'] = kwargs['content_type'] + kargs['headers']['Accept'] = kwargs['content_type'] + else: + kargs['headers']['Content-Type'] = self.content_type + kargs['headers']['Accept'] = self.content_type + + if 'body' in kwargs: + kargs['body'] = kwargs['body'] + args = utils.safe_encode_list(args) + kargs = utils.safe_encode_dict(kargs) + utils.http_log_req(_logger, args, kargs) + try: + resp, body = self.request(*args, **kargs) + except Exception as e: + # Wrap the low-level connection error (socket timeout, redirect + # limit, decompression error, etc) into our custom high-level + # connection exception (it is excepted in the upper layers of code) + raise exceptions.ConnectionFailed(reason=e) + utils.http_log_resp(_logger, resp, body) + status_code = self.get_status_code(resp) + if status_code == 401: + raise exceptions.Unauthorized(message=body) + elif status_code == 403: + raise exceptions.Forbidden(message=body) + return resp, body + + def authenticate_and_fetch_endpoint_url(self): + if not self.auth_token: + self.authenticate() + elif not self.endpoint_url: + self.endpoint_url = self._get_endpoint_url() + + def do_request(self, url, method, **kwargs): + self.authenticate_and_fetch_endpoint_url() + # Perform the request once. If we get a 401 back then it + # might be because the auth token expired, so try to + # re-authenticate and try again. If it still fails, bail. + try: + kwargs.setdefault('headers', {}) + kwargs['headers']['X-Auth-Token'] = self.auth_token + resp, body = self._cs_request(self.endpoint_url + url, method, + **kwargs) + return resp, body + except exceptions.Unauthorized: + self.authenticate() + kwargs.setdefault('headers', {}) + kwargs['headers']['X-Auth-Token'] = self.auth_token + resp, body = self._cs_request( + self.endpoint_url + url, method, **kwargs) + return resp, body + + def _extract_service_catalog(self, body): + """Set the client's service catalog from the response data.""" + self.service_catalog = ServiceCatalog(body) + try: + sc = self.service_catalog.get_token() + self.auth_token = sc['id'] + self.auth_tenant_id = sc.get('tenant_id') + self.auth_user_id = sc.get('user_id') + except KeyError: + raise exceptions.Unauthorized() + self.endpoint_url = self.service_catalog.url_for( + attr='region', filter_value=self.region_name, + endpoint_type=self.endpoint_type) + + def authenticate(self): + if self.auth_strategy != 'keystone': + raise exceptions.Unauthorized(message='unknown auth strategy') + if self.tenant_id: + body = {'auth': {'passwordCredentials': + {'username': self.username, + 'password': self.password, }, + 'tenantId': self.tenant_id, }, } + else: + body = {'auth': {'passwordCredentials': + {'username': self.username, + 'password': self.password, }, + 'tenantName': self.tenant_name, }, } + + token_url = self.auth_url + "/tokens" + + # Make sure we follow redirects when trying to reach Keystone + tmp_follow_all_redirects = self.follow_all_redirects + self.follow_all_redirects = True + try: + resp, body = self._cs_request(token_url, "POST", + body=json.dumps(body), + content_type="application/json") + finally: + self.follow_all_redirects = tmp_follow_all_redirects + status_code = self.get_status_code(resp) + if status_code != 200: + raise exceptions.Unauthorized(message=body) + if body: + try: + body = json.loads(body) + except ValueError: + pass + else: + body = None + self._extract_service_catalog(body) + + def _get_endpoint_url(self): + url = self.auth_url + '/tokens/%s/endpoints' % self.auth_token + try: + resp, body = self._cs_request(url, "GET") + except exceptions.Unauthorized: + # rollback to authenticate() to handle case when neutron client + # is initialized just before the token is expired + self.authenticate() + return self.endpoint_url + + body = json.loads(body) + for endpoint in body.get('endpoints', []): + if (endpoint['type'] == 'network' and + endpoint.get('region') == self.region_name): + if self.endpoint_type not in endpoint: + raise exceptions.EndpointTypeNotFound( + message=self.endpoint_type) + return endpoint[self.endpoint_type] + + raise exceptions.EndpointNotFound() + + def get_auth_info(self): + return {'auth_token': self.auth_token, + 'auth_tenant_id': self.auth_tenant_id, + 'auth_user_id': self.auth_user_id, + 'endpoint_url': self.endpoint_url} + + def get_status_code(self, response): + """Returns the integer status code from the response. + + Either a Webob.Response (used in testing) or httplib.Response + is returned. + """ + if hasattr(response, 'status_int'): + return response.status_int + else: + return response.status diff --git a/neutronclient/common/__init__.py b/neutronclient/common/__init__.py new file mode 100644 index 000000000..47ca7088f --- /dev/null +++ b/neutronclient/common/__init__.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2011 Nicira Networks, 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: Somik Behera, Nicira Networks, Inc. + +import gettext + +t = gettext.translation('neutronclient', fallback=True) + + +def _(msg): + return t.ugettext(msg) diff --git a/neutronclient/common/clientmanager.py b/neutronclient/common/clientmanager.py new file mode 100644 index 000000000..f9e886e01 --- /dev/null +++ b/neutronclient/common/clientmanager.py @@ -0,0 +1,88 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +"""Manage access to the clients, including authenticating when needed. +""" + +import logging + +from neutronclient import client +from neutronclient.neutron import client as neutron_client + + +LOG = logging.getLogger(__name__) + + +class ClientCache(object): + """Descriptor class for caching created client handles. + """ + + def __init__(self, factory): + self.factory = factory + self._handle = None + + def __get__(self, instance, owner): + # Tell the ClientManager to login to keystone + if self._handle is None: + self._handle = self.factory(instance) + return self._handle + + +class ClientManager(object): + """Manages access to API clients, including authentication. + """ + neutron = ClientCache(neutron_client.make_client) + + def __init__(self, token=None, url=None, + auth_url=None, + endpoint_type=None, + tenant_name=None, tenant_id=None, + username=None, password=None, + region_name=None, + api_version=None, + auth_strategy=None, + insecure=False + ): + self._token = token + self._url = url + self._auth_url = auth_url + self._endpoint_type = endpoint_type + self._tenant_name = tenant_name + self._tenant_id = tenant_id + self._username = username + self._password = password + self._region_name = region_name + self._api_version = api_version + self._service_catalog = None + self._auth_strategy = auth_strategy + self._insecure = insecure + return + + def initialize(self): + if not self._url: + httpclient = client.HTTPClient(username=self._username, + tenant_name=self._tenant_name, + tenant_id=self._tenant_id, + password=self._password, + region_name=self._region_name, + auth_url=self._auth_url, + endpoint_type=self._endpoint_type, + insecure=self._insecure) + httpclient.authenticate() + # Populate other password flow attributes + self._token = httpclient.auth_token + self._url = httpclient.endpoint_url diff --git a/neutronclient/common/command.py b/neutronclient/common/command.py new file mode 100644 index 000000000..719143642 --- /dev/null +++ b/neutronclient/common/command.py @@ -0,0 +1,41 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +""" +OpenStack base command +""" + +from cliff import command + + +class OpenStackCommand(command.Command): + """Base class for OpenStack commands + """ + + api = None + + def run(self, parsed_args): + if not self.api: + return + else: + return super(OpenStackCommand, self).run(parsed_args) + + def get_data(self, parsed_args): + pass + + def take_action(self, parsed_args): + return self.get_data(parsed_args) diff --git a/neutronclient/common/constants.py b/neutronclient/common/constants.py new file mode 100644 index 000000000..a8e8276af --- /dev/null +++ b/neutronclient/common/constants.py @@ -0,0 +1,43 @@ +# Copyright (c) 2012 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. + + +EXT_NS = '_extension_ns' +XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0' +XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance" +XSI_ATTR = "xsi:nil" +XSI_NIL_ATTR = "xmlns:xsi" +TYPE_XMLNS = "xmlns:quantum" +TYPE_ATTR = "quantum:type" +VIRTUAL_ROOT_KEY = "_v_root" +ATOM_NAMESPACE = "http://www.w3.org/2005/Atom" +ATOM_XMLNS = "xmlns:atom" +ATOM_LINK_NOTATION = "{%s}link" % ATOM_NAMESPACE + +TYPE_BOOL = "bool" +TYPE_INT = "int" +TYPE_LONG = "long" +TYPE_FLOAT = "float" +TYPE_LIST = "list" +TYPE_DICT = "dict" + +PLURALS = {'networks': 'network', + 'ports': 'port', + 'subnets': 'subnet', + 'dns_nameservers': 'dns_nameserver', + 'host_routes': 'host_route', + 'allocation_pools': 'allocation_pool', + 'fixed_ips': 'fixed_ip', + 'extensions': 'extension'} diff --git a/neutronclient/common/exceptions.py b/neutronclient/common/exceptions.py new file mode 100644 index 000000000..e02be309c --- /dev/null +++ b/neutronclient/common/exceptions.py @@ -0,0 +1,169 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Nicira Networks, 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. + +from neutronclient.common import _ + +""" +Neutron base exception handling. +""" + + +class NeutronException(Exception): + """Base Neutron Exception + + Taken from nova.exception.NovaException + To correctly use this class, inherit from it and define + a 'message' property. That message will get printf'd + with the keyword arguments provided to the constructor. + + """ + message = _("An unknown exception occurred.") + + def __init__(self, **kwargs): + try: + self._error_string = self.message % kwargs + + except Exception: + # at least get the core message out if something happened + self._error_string = self.message + + def __str__(self): + return self._error_string + + +class NotFound(NeutronException): + pass + + +class NeutronClientException(NeutronException): + + def __init__(self, **kwargs): + message = kwargs.get('message') + self.status_code = kwargs.get('status_code', 0) + if message: + self.message = message + super(NeutronClientException, self).__init__(**kwargs) + + +# NOTE: on the client side, we use different exception types in order +# to allow client library users to handle server exceptions in try...except +# blocks. The actual error message is the one generated on the server side +class NetworkNotFoundClient(NeutronClientException): + pass + + +class PortNotFoundClient(NeutronClientException): + pass + + +class MalformedResponseBody(NeutronException): + message = _("Malformed response body: %(reason)s") + + +class StateInvalidClient(NeutronClientException): + pass + + +class NetworkInUseClient(NeutronClientException): + pass + + +class PortInUseClient(NeutronClientException): + pass + + +class AlreadyAttachedClient(NeutronClientException): + pass + + +class Unauthorized(NeutronClientException): + message = _("Unauthorized: bad credentials.") + + +class Forbidden(NeutronClientException): + message = _("Forbidden: your credentials don't give you access to this " + "resource.") + + +class EndpointNotFound(NeutronClientException): + """Could not find Service or Region in Service Catalog.""" + message = _("Could not find Service or Region in Service Catalog.") + + +class EndpointTypeNotFound(NeutronClientException): + """Could not find endpoint type in Service Catalog.""" + + def __str__(self): + msg = "Could not find endpoint type %s in Service Catalog." + return msg % repr(self.message) + + +class AmbiguousEndpoints(NeutronClientException): + """Found more than one matching endpoint in Service Catalog.""" + + def __str__(self): + return "AmbiguousEndpoints: %s" % repr(self.message) + + +class NeutronCLIError(NeutronClientException): + """Exception raised when command line parsing fails.""" + pass + + +class RequestURITooLong(NeutronClientException): + """Raised when a request fails with HTTP error 414.""" + + def __init__(self, **kwargs): + self.excess = kwargs.get('excess', 0) + super(RequestURITooLong, self).__init__(**kwargs) + + +class ConnectionFailed(NeutronClientException): + message = _("Connection to neutron failed: %(reason)s") + + +class BadInputError(Exception): + """Error resulting from a client sending bad input to a server.""" + pass + + +class Error(Exception): + def __init__(self, message=None): + super(Error, self).__init__(message) + + +class MalformedRequestBody(NeutronException): + message = _("Malformed request body: %(reason)s") + + +class Invalid(Error): + pass + + +class InvalidContentType(Invalid): + message = _("Invalid content type %(content_type)s.") + + +class UnsupportedVersion(Exception): + """Indicates that the user is trying to use an unsupported + version of the API + """ + pass + + +class CommandError(Exception): + pass diff --git a/neutronclient/common/serializer.py b/neutronclient/common/serializer.py new file mode 100644 index 000000000..b8f1c19ed --- /dev/null +++ b/neutronclient/common/serializer.py @@ -0,0 +1,410 @@ +# Copyright 2013 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +### +### Codes from neutron wsgi +### + +import logging + +from xml.etree import ElementTree as etree +from xml.parsers import expat + +from neutronclient.common import constants +from neutronclient.common import exceptions as exception +from neutronclient.openstack.common.gettextutils import _ +from neutronclient.openstack.common import jsonutils + +LOG = logging.getLogger(__name__) + + +class ActionDispatcher(object): + """Maps method name to local methods through action name.""" + + def dispatch(self, *args, **kwargs): + """Find and call local method.""" + action = kwargs.pop('action', 'default') + action_method = getattr(self, str(action), self.default) + return action_method(*args, **kwargs) + + def default(self, data): + raise NotImplementedError() + + +class DictSerializer(ActionDispatcher): + """Default request body serialization.""" + + def serialize(self, data, action='default'): + return self.dispatch(data, action=action) + + def default(self, data): + return "" + + +class JSONDictSerializer(DictSerializer): + """Default JSON request body serialization.""" + + def default(self, data): + def sanitizer(obj): + return unicode(obj) + return jsonutils.dumps(data, default=sanitizer) + + +class XMLDictSerializer(DictSerializer): + + def __init__(self, metadata=None, xmlns=None): + """XMLDictSerializer constructor. + + :param metadata: information needed to deserialize xml into + a dictionary. + :param xmlns: XML namespace to include with serialized xml + """ + super(XMLDictSerializer, self).__init__() + self.metadata = metadata or {} + if not xmlns: + xmlns = self.metadata.get('xmlns') + if not xmlns: + xmlns = constants.XML_NS_V20 + self.xmlns = xmlns + + def default(self, data): + """Default serializer of XMLDictSerializer. + + :param data: expect data to contain a single key as XML root, or + contain another '*_links' key as atom links. Other + case will use 'VIRTUAL_ROOT_KEY' as XML root. + """ + try: + links = None + has_atom = False + if data is None: + root_key = constants.VIRTUAL_ROOT_KEY + root_value = None + else: + link_keys = [k for k in data.iterkeys() or [] + if k.endswith('_links')] + if link_keys: + links = data.pop(link_keys[0], None) + has_atom = True + root_key = (len(data) == 1 and + data.keys()[0] or constants.VIRTUAL_ROOT_KEY) + root_value = data.get(root_key, data) + doc = etree.Element("_temp_root") + used_prefixes = [] + self._to_xml_node(doc, self.metadata, root_key, + root_value, used_prefixes) + if links: + self._create_link_nodes(list(doc)[0], links) + return self.to_xml_string(list(doc)[0], used_prefixes, has_atom) + except AttributeError as e: + LOG.exception(str(e)) + return '' + + def __call__(self, data): + # Provides a migration path to a cleaner WSGI layer, this + # "default" stuff and extreme extensibility isn't being used + # like originally intended + return self.default(data) + + def to_xml_string(self, node, used_prefixes, has_atom=False): + self._add_xmlns(node, used_prefixes, has_atom) + return etree.tostring(node, encoding='UTF-8') + + #NOTE (ameade): the has_atom should be removed after all of the + # xml serializers and view builders have been updated to the current + # spec that required all responses include the xmlns:atom, the has_atom + # flag is to prevent current tests from breaking + def _add_xmlns(self, node, used_prefixes, has_atom=False): + node.set('xmlns', self.xmlns) + node.set(constants.TYPE_XMLNS, self.xmlns) + if has_atom: + node.set(constants.ATOM_XMLNS, constants.ATOM_NAMESPACE) + node.set(constants.XSI_NIL_ATTR, constants.XSI_NAMESPACE) + ext_ns = self.metadata.get(constants.EXT_NS, {}) + for prefix in used_prefixes: + if prefix in ext_ns: + node.set('xmlns:' + prefix, ext_ns[prefix]) + + def _to_xml_node(self, parent, metadata, nodename, data, used_prefixes): + """Recursive method to convert data members to XML nodes.""" + result = etree.SubElement(parent, nodename) + if ":" in nodename: + used_prefixes.append(nodename.split(":", 1)[0]) + #TODO(bcwaldon): accomplish this without a type-check + if isinstance(data, list): + if not data: + result.set( + constants.TYPE_ATTR, + constants.TYPE_LIST) + return result + singular = metadata.get('plurals', {}).get(nodename, None) + if singular is None: + if nodename.endswith('s'): + singular = nodename[:-1] + else: + singular = 'item' + for item in data: + self._to_xml_node(result, metadata, singular, item, + used_prefixes) + #TODO(bcwaldon): accomplish this without a type-check + elif isinstance(data, dict): + if not data: + result.set( + constants.TYPE_ATTR, + constants.TYPE_DICT) + return result + attrs = metadata.get('attributes', {}).get(nodename, {}) + for k, v in data.items(): + if k in attrs: + result.set(k, str(v)) + else: + self._to_xml_node(result, metadata, k, v, + used_prefixes) + elif data is None: + result.set(constants.XSI_ATTR, 'true') + else: + if isinstance(data, bool): + result.set( + constants.TYPE_ATTR, + constants.TYPE_BOOL) + elif isinstance(data, int): + result.set( + constants.TYPE_ATTR, + constants.TYPE_INT) + elif isinstance(data, long): + result.set( + constants.TYPE_ATTR, + constants.TYPE_LONG) + elif isinstance(data, float): + result.set( + constants.TYPE_ATTR, + constants.TYPE_FLOAT) + LOG.debug(_("Data %(data)s type is %(type)s"), + {'data': data, + 'type': type(data)}) + if isinstance(data, str): + result.text = unicode(data, 'utf-8') + else: + result.text = unicode(data) + return result + + def _create_link_nodes(self, xml_doc, links): + for link in links: + link_node = etree.SubElement(xml_doc, 'atom:link') + link_node.set('rel', link['rel']) + link_node.set('href', link['href']) + + +class TextDeserializer(ActionDispatcher): + """Default request body deserialization.""" + + def deserialize(self, datastring, action='default'): + return self.dispatch(datastring, action=action) + + def default(self, datastring): + return {} + + +class JSONDeserializer(TextDeserializer): + + def _from_json(self, datastring): + try: + return jsonutils.loads(datastring) + except ValueError: + msg = _("Cannot understand JSON") + raise exception.MalformedRequestBody(reason=msg) + + def default(self, datastring): + return {'body': self._from_json(datastring)} + + +class XMLDeserializer(TextDeserializer): + + def __init__(self, metadata=None): + """XMLDeserializer constructor. + + :param metadata: information needed to deserialize xml into + a dictionary. + """ + super(XMLDeserializer, self).__init__() + self.metadata = metadata or {} + xmlns = self.metadata.get('xmlns') + if not xmlns: + xmlns = constants.XML_NS_V20 + self.xmlns = xmlns + + def _get_key(self, tag): + tags = tag.split("}", 1) + if len(tags) == 2: + ns = tags[0][1:] + bare_tag = tags[1] + ext_ns = self.metadata.get(constants.EXT_NS, {}) + if ns == self.xmlns: + return bare_tag + for prefix, _ns in ext_ns.items(): + if ns == _ns: + return prefix + ":" + bare_tag + else: + return tag + + def _get_links(self, root_tag, node): + link_nodes = node.findall(constants.ATOM_LINK_NOTATION) + root_tag = self._get_key(node.tag) + link_key = "%s_links" % root_tag + link_list = [] + for link in link_nodes: + link_list.append({'rel': link.get('rel'), + 'href': link.get('href')}) + # Remove link node in order to avoid link node being + # processed as an item in _from_xml_node + node.remove(link) + return link_list and {link_key: link_list} or {} + + def _from_xml(self, datastring): + if datastring is None: + return None + plurals = set(self.metadata.get('plurals', {})) + try: + node = etree.fromstring(datastring) + root_tag = self._get_key(node.tag) + links = self._get_links(root_tag, node) + result = self._from_xml_node(node, plurals) + # There is no case where root_tag = constants.VIRTUAL_ROOT_KEY + # and links is not None because of the way data are serialized + if root_tag == constants.VIRTUAL_ROOT_KEY: + return result + return dict({root_tag: result}, **links) + except Exception as e: + parseError = False + # Python2.7 + if (hasattr(etree, 'ParseError') and + isinstance(e, getattr(etree, 'ParseError'))): + parseError = True + # Python2.6 + elif isinstance(e, expat.ExpatError): + parseError = True + if parseError: + msg = _("Cannot understand XML") + raise exception.MalformedRequestBody(reason=msg) + else: + raise + + def _from_xml_node(self, node, listnames): + """Convert a minidom node to a simple Python type. + + :param listnames: list of XML node names whose subnodes should + be considered list items. + + """ + attrNil = node.get(str(etree.QName(constants.XSI_NAMESPACE, "nil"))) + attrType = node.get(str(etree.QName( + self.metadata.get('xmlns'), "type"))) + if (attrNil and attrNil.lower() == 'true'): + return None + elif not len(node) and not node.text: + if (attrType and attrType == constants.TYPE_DICT): + return {} + elif (attrType and attrType == constants.TYPE_LIST): + return [] + else: + return '' + elif (len(node) == 0 and node.text): + converters = {constants.TYPE_BOOL: + lambda x: x.lower() == 'true', + constants.TYPE_INT: + lambda x: int(x), + constants.TYPE_LONG: + lambda x: long(x), + constants.TYPE_FLOAT: + lambda x: float(x)} + if attrType and attrType in converters: + return converters[attrType](node.text) + else: + return node.text + elif self._get_key(node.tag) in listnames: + return [self._from_xml_node(n, listnames) for n in node] + else: + result = dict() + for attr in node.keys(): + if (attr == 'xmlns' or + attr.startswith('xmlns:') or + attr == constants.XSI_ATTR or + attr == constants.TYPE_ATTR): + continue + result[self._get_key(attr)] = node.get(attr) + children = list(node) + for child in children: + result[self._get_key(child.tag)] = self._from_xml_node( + child, listnames) + return result + + def default(self, datastring): + return {'body': self._from_xml(datastring)} + + def __call__(self, datastring): + # Adding a migration path to allow us to remove unncessary classes + return self.default(datastring) + + +# NOTE(maru): this class is duplicated from neutron.wsgi +class Serializer(object): + """Serializes and deserializes dictionaries to certain MIME types.""" + + def __init__(self, metadata=None, default_xmlns=None): + """Create a serializer based on the given WSGI environment. + + 'metadata' is an optional dict mapping MIME types to information + needed to serialize a dictionary to that type. + + """ + self.metadata = metadata or {} + self.default_xmlns = default_xmlns + + def _get_serialize_handler(self, content_type): + handlers = { + 'application/json': JSONDictSerializer(), + 'application/xml': XMLDictSerializer(self.metadata), + } + + try: + return handlers[content_type] + except Exception: + raise exception.InvalidContentType(content_type=content_type) + + def serialize(self, data, content_type): + """Serialize a dictionary into the specified content type.""" + return self._get_serialize_handler(content_type).serialize(data) + + def deserialize(self, datastring, content_type): + """Deserialize a string to a dictionary. + + The string must be in the format of a supported MIME type. + + """ + return self.get_deserialize_handler(content_type).deserialize( + datastring) + + def get_deserialize_handler(self, content_type): + handlers = { + 'application/json': JSONDeserializer(), + 'application/xml': XMLDeserializer(self.metadata), + } + + try: + return handlers[content_type] + except Exception: + raise exception.InvalidContentType(content_type=content_type) diff --git a/neutronclient/common/utils.py b/neutronclient/common/utils.py new file mode 100644 index 000000000..ca2e6ecc4 --- /dev/null +++ b/neutronclient/common/utils.py @@ -0,0 +1,200 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011, Nicira Networks, 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. +# +# Borrowed from nova code base, more utilities will be added/borrowed as and +# when needed. +# @author: Somik Behera, Nicira Networks, Inc. + +"""Utilities and helper functions.""" + +import datetime +import json +import logging +import os +import sys + +from neutronclient.common import exceptions +from neutronclient.openstack.common import strutils + + +def env(*vars, **kwargs): + """Returns the first environment variable set. + + if none are non-empty, defaults to '' or keyword arg default. + """ + for v in vars: + value = os.environ.get(v) + if value: + return value + return kwargs.get('default', '') + + +def to_primitive(value): + if isinstance(value, list) or isinstance(value, tuple): + o = [] + for v in value: + o.append(to_primitive(v)) + return o + elif isinstance(value, dict): + o = {} + for k, v in value.iteritems(): + o[k] = to_primitive(v) + return o + elif isinstance(value, datetime.datetime): + return str(value) + elif hasattr(value, 'iteritems'): + return to_primitive(dict(value.iteritems())) + elif hasattr(value, '__iter__'): + return to_primitive(list(value)) + else: + return value + + +def dumps(value, indent=None): + try: + return json.dumps(value, indent=indent) + except TypeError: + pass + return json.dumps(to_primitive(value)) + + +def loads(s): + return json.loads(s) + + +def import_class(import_str): + """Returns a class from a string including module and class. + + :param import_str: a string representation of the class name + :rtype: the requested class + """ + mod_str, _sep, class_str = import_str.rpartition('.') + __import__(mod_str) + return getattr(sys.modules[mod_str], class_str) + + +def get_client_class(api_name, version, version_map): + """Returns the client class for the requested API version + + :param api_name: the name of the API, e.g. 'compute', 'image', etc + :param version: the requested API version + :param version_map: a dict of client classes keyed by version + :rtype: a client class for the requested API version + """ + try: + client_path = version_map[str(version)] + except (KeyError, ValueError): + msg = "Invalid %s client version '%s'. must be one of: %s" % ( + (api_name, version, ', '.join(version_map.keys()))) + raise exceptions.UnsupportedVersion(msg) + + return import_class(client_path) + + +def get_item_properties(item, fields, mixed_case_fields=[], formatters={}): + """Return a tuple containing the item properties. + + :param item: a single item resource (e.g. Server, Tenant, etc) + :param fields: tuple of strings with the desired field names + :param mixed_case_fields: tuple of field names to preserve case + :param formatters: dictionary mapping field names to callables + to format the values + """ + row = [] + + for field in fields: + if field in formatters: + row.append(formatters[field](item)) + else: + if field in mixed_case_fields: + field_name = field.replace(' ', '_') + else: + field_name = field.lower().replace(' ', '_') + if not hasattr(item, field_name) and isinstance(item, dict): + data = item[field_name] + else: + data = getattr(item, field_name, '') + if data is None: + data = '' + row.append(data) + return tuple(row) + + +def str2bool(strbool): + if strbool is None: + return None + else: + return strbool.lower() == 'true' + + +def str2dict(strdict): + '''Convert key1=value1,key2=value2,... string into dictionary. + + :param strdict: key1=value1,key2=value2 + ''' + _info = {} + for kv_str in strdict.split(","): + k, v = kv_str.split("=", 1) + _info.update({k: v}) + return _info + + +def http_log_req(_logger, args, kwargs): + if not _logger.isEnabledFor(logging.DEBUG): + return + + string_parts = ['curl -i'] + for element in args: + if element in ('GET', 'POST', 'DELETE', 'PUT'): + string_parts.append(' -X %s' % element) + else: + string_parts.append(' %s' % element) + + for element in kwargs['headers']: + header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) + string_parts.append(header) + + if 'body' in kwargs and kwargs['body']: + string_parts.append(" -d '%s'" % (kwargs['body'])) + string_parts = safe_encode_list(string_parts) + _logger.debug("\nREQ: %s\n" % "".join(string_parts)) + + +def http_log_resp(_logger, resp, body): + if not _logger.isEnabledFor(logging.DEBUG): + return + _logger.debug("RESP:%s %s\n", resp, body) + + +def _safe_encode_without_obj(data): + if isinstance(data, basestring): + return strutils.safe_encode(data) + return data + + +def safe_encode_list(data): + return map(_safe_encode_without_obj, data) + + +def safe_encode_dict(data): + def _encode_item((k, v)): + if isinstance(v, list): + return (k, safe_encode_list(v)) + elif isinstance(v, dict): + return (k, safe_encode_dict(v)) + return (k, _safe_encode_without_obj(v)) + + return dict(map(_encode_item, data.items())) diff --git a/neutronclient/neutron/__init__.py b/neutronclient/neutron/__init__.py new file mode 100644 index 000000000..63c3905e2 --- /dev/null +++ b/neutronclient/neutron/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2012 OpenStack LLC. +# 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. diff --git a/neutronclient/neutron/client.py b/neutronclient/neutron/client.py new file mode 100644 index 000000000..daf04815a --- /dev/null +++ b/neutronclient/neutron/client.py @@ -0,0 +1,64 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +from neutronclient.common import exceptions +from neutronclient.common import utils + + +API_NAME = 'network' +API_VERSIONS = { + '2.0': 'neutronclient.v2_0.client.Client', +} + + +def make_client(instance): + """Returns an neutron client. + """ + neutron_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS, + ) + instance.initialize() + url = instance._url + url = url.rstrip("/") + if '2.0' == instance._api_version[API_NAME]: + client = neutron_client(username=instance._username, + tenant_name=instance._tenant_name, + password=instance._password, + region_name=instance._region_name, + auth_url=instance._auth_url, + endpoint_url=url, + token=instance._token, + auth_strategy=instance._auth_strategy, + insecure=instance._insecure) + return client + else: + raise exceptions.UnsupportedVersion("API version %s is not supported" % + instance._api_version[API_NAME]) + + +def Client(api_version, *args, **kwargs): + """Return an neutron client. + @param api_version: only 2.0 is supported now + """ + neutron_client = utils.get_client_class( + API_NAME, + api_version, + API_VERSIONS, + ) + return neutron_client(*args, **kwargs) diff --git a/neutronclient/neutron/v2_0/__init__.py b/neutronclient/neutron/v2_0/__init__.py new file mode 100644 index 000000000..9cb8cd1e2 --- /dev/null +++ b/neutronclient/neutron/v2_0/__init__.py @@ -0,0 +1,588 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import argparse +import logging +import re + +from cliff.formatters import table +from cliff import lister +from cliff import show + +from neutronclient.common import command +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.openstack.common.gettextutils import _ + +HEX_ELEM = '[0-9A-Fa-f]' +UUID_PATTERN = '-'.join([HEX_ELEM + '{8}', HEX_ELEM + '{4}', + HEX_ELEM + '{4}', HEX_ELEM + '{4}', + HEX_ELEM + '{12}']) + + +def find_resourceid_by_name_or_id(client, resource, name_or_id): + obj_lister = getattr(client, "list_%ss" % resource) + # perform search by id only if we are passing a valid UUID + match = re.match(UUID_PATTERN, name_or_id) + collection = resource + "s" + if match: + data = obj_lister(id=name_or_id, fields='id') + if data and data[collection]: + return data[collection][0]['id'] + return _find_resourceid_by_name(client, resource, name_or_id) + + +def _find_resourceid_by_name(client, resource, name): + obj_lister = getattr(client, "list_%ss" % resource) + data = obj_lister(name=name, fields='id') + collection = resource + "s" + info = data[collection] + if len(info) > 1: + msg = (_("Multiple %(resource)s matches found for name '%(name)s'," + " use an ID to be more specific.") % + {'resource': resource, 'name': name}) + raise exceptions.NeutronClientException( + message=msg) + elif len(info) == 0: + not_found_message = (_("Unable to find %(resource)s with name " + "'%(name)s'") % + {'resource': resource, 'name': name}) + # 404 is used to simulate server side behavior + raise exceptions.NeutronClientException( + message=not_found_message, status_code=404) + else: + return info[0]['id'] + + +def add_show_list_common_argument(parser): + parser.add_argument( + '-D', '--show-details', + help='show detailed info', + action='store_true', + default=False, ) + parser.add_argument( + '--show_details', + action='store_true', + help=argparse.SUPPRESS) + parser.add_argument( + '--fields', + help=argparse.SUPPRESS, + action='append', + default=[]) + parser.add_argument( + '-F', '--field', + dest='fields', metavar='FIELD', + help='specify the field(s) to be returned by server,' + ' can be repeated', + action='append', + default=[]) + + +def add_pagination_argument(parser): + parser.add_argument( + '-P', '--page-size', + dest='page_size', metavar='SIZE', type=int, + help=("specify retrieve unit of each request, then split one request " + "to several requests"), + default=None) + + +def add_sorting_argument(parser): + parser.add_argument( + '--sort-key', + dest='sort_key', metavar='FIELD', + action='append', + help=("sort list by specified fields (This option can be repeated), " + "The number of sort_dir and sort_key should match each other, " + "more sort_dir specified will be omitted, less will be filled " + "with asc as default direction "), + default=[]) + parser.add_argument( + '--sort-dir', + dest='sort_dir', metavar='{asc,desc}', + help=("sort list in specified directions " + "(This option can be repeated)"), + action='append', + default=[], + choices=['asc', 'desc']) + + +def is_number(s): + try: + float(s) # for int, long and float + except ValueError: + try: + complex(s) # for complex + except ValueError: + return False + + return True + + +def parse_args_to_dict(values_specs): + '''It is used to analyze the extra command options to command. + + Besides known options and arguments, our commands also support user to + put more options to the end of command line. For example, + list_nets -- --tag x y --key1 value1, where '-- --tag x y --key1 value1' + is extra options to our list_nets. This feature can support V2.0 API's + fields selection and filters. For example, to list networks which has name + 'test4', we can have list_nets -- --name=test4. + + value spec is: --key type=int|bool|... value. Type is one of Python + built-in types. By default, type is string. The key without value is + a bool option. Key with two values will be a list option. + + ''' + # -- is a pseudo argument + values_specs_copy = values_specs[:] + if values_specs_copy and values_specs_copy[0] == '--': + del values_specs_copy[0] + _options = {} + current_arg = None + _values_specs = [] + _value_number = 0 + _list_flag = False + current_item = None + for _item in values_specs_copy: + if _item.startswith('--'): + if current_arg is not None: + if _value_number > 1 or _list_flag: + current_arg.update({'nargs': '+'}) + elif _value_number == 0: + current_arg.update({'action': 'store_true'}) + _temp = _item + if "=" in _item: + _item = _item.split('=')[0] + if _item in _options: + raise exceptions.CommandError( + "duplicated options %s" % ' '.join(values_specs)) + else: + _options.update({_item: {}}) + current_arg = _options[_item] + _item = _temp + elif _item.startswith('type='): + if current_arg is None: + raise exceptions.CommandError( + "invalid values_specs %s" % ' '.join(values_specs)) + if 'type' not in current_arg: + _type_str = _item.split('=', 2)[1] + current_arg.update({'type': eval(_type_str)}) + if _type_str == 'bool': + current_arg.update({'type': utils.str2bool}) + elif _type_str == 'dict': + current_arg.update({'type': utils.str2dict}) + continue + elif _item == 'list=true': + _list_flag = True + continue + if not _item.startswith('--'): + if (not current_item or '=' in current_item or + _item.startswith('-') and not is_number(_item)): + raise exceptions.CommandError( + "Invalid values_specs %s" % ' '.join(values_specs)) + _value_number += 1 + elif _item.startswith('--'): + current_item = _item + if '=' in current_item: + _value_number = 1 + else: + _value_number = 0 + _list_flag = False + _values_specs.append(_item) + if current_arg is not None: + if _value_number > 1 or _list_flag: + current_arg.update({'nargs': '+'}) + elif _value_number == 0: + current_arg.update({'action': 'store_true'}) + _args = None + if _values_specs: + _parser = argparse.ArgumentParser(add_help=False) + for opt, optspec in _options.iteritems(): + _parser.add_argument(opt, **optspec) + _args = _parser.parse_args(_values_specs) + result_dict = {} + if _args: + for opt in _options.iterkeys(): + _opt = opt.split('--', 2)[1] + _opt = _opt.replace('-', '_') + _value = getattr(_args, _opt) + if _value is not None: + result_dict.update({_opt: _value}) + return result_dict + + +def _merge_args(qCmd, parsed_args, _extra_values, value_specs): + """Merge arguments from _extra_values into parsed_args. + + If an argument value are provided in both and it is a list, + the values in _extra_values will be merged into parsed_args. + + @param parsed_args: the parsed args from known options + @param _extra_values: the other parsed arguments in unknown parts + @param values_specs: the unparsed unknown parts + """ + temp_values = _extra_values.copy() + for key, value in temp_values.iteritems(): + if hasattr(parsed_args, key): + arg_value = getattr(parsed_args, key) + if arg_value is not None and value is not None: + if isinstance(arg_value, list): + if value and isinstance(value, list): + if type(arg_value[0]) == type(value[0]): + arg_value.extend(value) + _extra_values.pop(key) + + +def update_dict(obj, dict, attributes): + for attribute in attributes: + if hasattr(obj, attribute) and getattr(obj, attribute): + dict[attribute] = getattr(obj, attribute) + + +class TableFormater(table.TableFormatter): + """This class is used to keep consistency with prettytable 0.6. + + https://bugs.launchpad.net/python-neutronclient/+bug/1165962 + """ + def emit_list(self, column_names, data, stdout, parsed_args): + if column_names: + super(TableFormater, self).emit_list(column_names, data, stdout, + parsed_args) + else: + stdout.write('\n') + + +class NeutronCommand(command.OpenStackCommand): + api = 'network' + log = logging.getLogger(__name__ + '.NeutronCommand') + values_specs = [] + json_indent = None + + def __init__(self, app, app_args): + super(NeutronCommand, self).__init__(app, app_args) + if hasattr(self, 'formatters'): + self.formatters['table'] = TableFormater() + + def get_client(self): + return self.app.client_manager.neutron + + def get_parser(self, prog_name): + parser = super(NeutronCommand, self).get_parser(prog_name) + parser.add_argument( + '--request-format', + help=_('the xml or json request format'), + default='json', + choices=['json', 'xml', ], ) + parser.add_argument( + '--request_format', + choices=['json', 'xml', ], + help=argparse.SUPPRESS) + + return parser + + def format_output_data(self, data): + # Modify data to make it more readable + if self.resource in data: + for k, v in data[self.resource].iteritems(): + if isinstance(v, list): + value = '\n'.join(utils.dumps( + i, indent=self.json_indent) if isinstance(i, dict) + else str(i) for i in v) + data[self.resource][k] = value + elif isinstance(v, dict): + value = utils.dumps(v, indent=self.json_indent) + data[self.resource][k] = value + elif v is None: + data[self.resource][k] = '' + + def add_known_arguments(self, parser): + pass + + def args2body(self, parsed_args): + return {} + + +class CreateCommand(NeutronCommand, show.ShowOne): + """Create a resource for a given tenant + + """ + + api = 'network' + resource = None + log = None + + def get_parser(self, prog_name): + parser = super(CreateCommand, self).get_parser(prog_name) + parser.add_argument( + '--tenant-id', metavar='TENANT_ID', + help=_('the owner tenant ID'), ) + parser.add_argument( + '--tenant_id', + help=argparse.SUPPRESS) + self.add_known_arguments(parser) + return parser + + def get_data(self, parsed_args): + self.log.debug('get_data(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _extra_values = parse_args_to_dict(self.values_specs) + _merge_args(self, parsed_args, _extra_values, + self.values_specs) + body = self.args2body(parsed_args) + body[self.resource].update(_extra_values) + obj_creator = getattr(neutron_client, + "create_%s" % self.resource) + data = obj_creator(body) + self.format_output_data(data) + # {u'network': {u'id': u'e9424a76-6db4-4c93-97b6-ec311cd51f19'}} + info = self.resource in data and data[self.resource] or None + if info: + print >>self.app.stdout, _('Created a new %s:') % self.resource + else: + info = {'': ''} + return zip(*sorted(info.iteritems())) + + +class UpdateCommand(NeutronCommand): + """Update resource's information + """ + + api = 'network' + resource = None + log = None + + def get_parser(self, prog_name): + parser = super(UpdateCommand, self).get_parser(prog_name) + parser.add_argument( + 'id', metavar=self.resource.upper(), + help='ID or name of %s to update' % self.resource) + self.add_known_arguments(parser) + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _extra_values = parse_args_to_dict(self.values_specs) + _merge_args(self, parsed_args, _extra_values, + self.values_specs) + body = self.args2body(parsed_args) + if self.resource in body: + body[self.resource].update(_extra_values) + else: + body[self.resource] = _extra_values + if not body[self.resource]: + raise exceptions.CommandError( + "Must specify new values to update %s" % self.resource) + _id = find_resourceid_by_name_or_id(neutron_client, + self.resource, + parsed_args.id) + obj_updator = getattr(neutron_client, + "update_%s" % self.resource) + obj_updator(_id, body) + print >>self.app.stdout, ( + _('Updated %(resource)s: %(id)s') % + {'id': parsed_args.id, 'resource': self.resource}) + return + + +class DeleteCommand(NeutronCommand): + """Delete a given resource + + """ + + api = 'network' + resource = None + log = None + allow_names = True + + def get_parser(self, prog_name): + parser = super(DeleteCommand, self).get_parser(prog_name) + if self.allow_names: + help_str = 'ID or name of %s to delete' + else: + help_str = 'ID of %s to delete' + parser.add_argument( + 'id', metavar=self.resource.upper(), + help=help_str % self.resource) + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + obj_deleter = getattr(neutron_client, + "delete_%s" % self.resource) + if self.allow_names: + _id = find_resourceid_by_name_or_id(neutron_client, self.resource, + parsed_args.id) + else: + _id = parsed_args.id + obj_deleter(_id) + print >>self.app.stdout, (_('Deleted %(resource)s: %(id)s') + % {'id': parsed_args.id, + 'resource': self.resource}) + return + + +class ListCommand(NeutronCommand, lister.Lister): + """List resources that belong to a given tenant + + """ + + api = 'network' + resource = None + log = None + _formatters = {} + list_columns = [] + unknown_parts_flag = True + pagination_support = False + sorting_support = False + + def get_parser(self, prog_name): + parser = super(ListCommand, self).get_parser(prog_name) + add_show_list_common_argument(parser) + if self.pagination_support: + add_pagination_argument(parser) + if self.sorting_support: + add_sorting_argument(parser) + return parser + + def args2search_opts(self, parsed_args): + search_opts = {} + fields = parsed_args.fields + if parsed_args.fields: + search_opts.update({'fields': fields}) + if parsed_args.show_details: + search_opts.update({'verbose': 'True'}) + return search_opts + + def call_server(self, neutron_client, search_opts, parsed_args): + obj_lister = getattr(neutron_client, + "list_%ss" % self.resource) + data = obj_lister(**search_opts) + return data + + def retrieve_list(self, parsed_args): + """Retrieve a list of resources from Neutron server""" + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _extra_values = parse_args_to_dict(self.values_specs) + _merge_args(self, parsed_args, _extra_values, + self.values_specs) + search_opts = self.args2search_opts(parsed_args) + search_opts.update(_extra_values) + if self.pagination_support: + page_size = parsed_args.page_size + if page_size: + search_opts.update({'limit': page_size}) + if self.sorting_support: + keys = parsed_args.sort_key + if keys: + search_opts.update({'sort_key': keys}) + dirs = parsed_args.sort_dir + len_diff = len(keys) - len(dirs) + if len_diff > 0: + dirs += ['asc'] * len_diff + elif len_diff < 0: + dirs = dirs[:len(keys)] + if dirs: + search_opts.update({'sort_dir': dirs}) + data = self.call_server(neutron_client, search_opts, parsed_args) + collection = self.resource + "s" + return data.get(collection, []) + + def extend_list(self, data, parsed_args): + """Update a retrieved list. + + This method provides a way to modify a original list returned from + the neutron server. For example, you can add subnet cidr information + to a list network. + """ + pass + + def setup_columns(self, info, parsed_args): + _columns = len(info) > 0 and sorted(info[0].keys()) or [] + if not _columns: + # clean the parsed_args.columns so that cliff will not break + parsed_args.columns = [] + elif parsed_args.columns: + _columns = [x for x in parsed_args.columns if x in _columns] + elif self.list_columns: + # if no -c(s) by user and list_columns, we use columns in + # both list_columns and returned resource. + # Also Keep their order the same as in list_columns + _columns = [x for x in self.list_columns if x in _columns] + return (_columns, (utils.get_item_properties( + s, _columns, formatters=self._formatters, ) + for s in info), ) + + def get_data(self, parsed_args): + self.log.debug('get_data(%s)' % parsed_args) + data = self.retrieve_list(parsed_args) + self.extend_list(data, parsed_args) + return self.setup_columns(data, parsed_args) + + +class ShowCommand(NeutronCommand, show.ShowOne): + """Show information of a given resource + + """ + + api = 'network' + resource = None + log = None + allow_names = True + + def get_parser(self, prog_name): + parser = super(ShowCommand, self).get_parser(prog_name) + add_show_list_common_argument(parser) + if self.allow_names: + help_str = 'ID or name of %s to look up' + else: + help_str = 'ID of %s to look up' + parser.add_argument( + 'id', metavar=self.resource.upper(), + help=help_str % self.resource) + return parser + + def get_data(self, parsed_args): + self.log.debug('get_data(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + + params = {} + if parsed_args.show_details: + params = {'verbose': 'True'} + if parsed_args.fields: + params = {'fields': parsed_args.fields} + if self.allow_names: + _id = find_resourceid_by_name_or_id(neutron_client, self.resource, + parsed_args.id) + else: + _id = parsed_args.id + + obj_shower = getattr(neutron_client, "show_%s" % self.resource) + data = obj_shower(_id, **params) + self.format_output_data(data) + resource = data[self.resource] + if self.resource in data: + return zip(*sorted(resource.iteritems())) + else: + return None diff --git a/neutronclient/neutron/v2_0/agent.py b/neutronclient/neutron/v2_0/agent.py new file mode 100644 index 000000000..8c85f5441 --- /dev/null +++ b/neutronclient/neutron/v2_0/agent.py @@ -0,0 +1,65 @@ +# Copyright 2013 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import logging + +from neutronclient.neutron import v2_0 as neutronV20 + + +def _format_timestamp(component): + try: + return component['heartbeat_timestamp'].split(".", 2)[0] + except Exception: + return '' + + +class ListAgent(neutronV20.ListCommand): + """List agents.""" + + resource = 'agent' + log = logging.getLogger(__name__ + '.ListAgent') + list_columns = ['id', 'agent_type', 'host', 'alive', 'admin_state_up'] + _formatters = {'heartbeat_timestamp': _format_timestamp} + + def extend_list(self, data, parsed_args): + for agent in data: + agent['alive'] = ":-)" if agent['alive'] else 'xxx' + + +class ShowAgent(neutronV20.ShowCommand): + """Show information of a given agent.""" + + resource = 'agent' + log = logging.getLogger(__name__ + '.ShowAgent') + allow_names = False + json_indent = 5 + + +class DeleteAgent(neutronV20.DeleteCommand): + """Delete a given agent.""" + + log = logging.getLogger(__name__ + '.DeleteAgent') + resource = 'agent' + allow_names = False + + +class UpdateAgent(neutronV20.UpdateCommand): + """Update a given agent.""" + + log = logging.getLogger(__name__ + '.UpdateAgent') + resource = 'agent' + allow_names = False diff --git a/neutronclient/neutron/v2_0/agentscheduler.py b/neutronclient/neutron/v2_0/agentscheduler.py new file mode 100644 index 000000000..d76112551 --- /dev/null +++ b/neutronclient/neutron/v2_0/agentscheduler.py @@ -0,0 +1,234 @@ +# Copyright 2013 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import logging + +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.neutron.v2_0 import network +from neutronclient.neutron.v2_0 import router +from neutronclient.openstack.common.gettextutils import _ +PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" + + +class AddNetworkToDhcpAgent(neutronV20.NeutronCommand): + """Add a network to a DHCP agent.""" + + log = logging.getLogger(__name__ + '.AddNetworkToDhcpAgent') + + def get_parser(self, prog_name): + parser = super(AddNetworkToDhcpAgent, self).get_parser(prog_name) + parser.add_argument( + 'dhcp_agent', + help='ID of the DHCP agent') + parser.add_argument( + 'network', + help='network to add') + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _net_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'network', parsed_args.network) + neutron_client.add_network_to_dhcp_agent(parsed_args.dhcp_agent, + {'network_id': _net_id}) + print >>self.app.stdout, ( + _('Added network %s to DHCP agent') % parsed_args.network) + + +class RemoveNetworkFromDhcpAgent(neutronV20.NeutronCommand): + """Remove a network from a DHCP agent.""" + log = logging.getLogger(__name__ + '.RemoveNetworkFromDhcpAgent') + + def get_parser(self, prog_name): + parser = super(RemoveNetworkFromDhcpAgent, self).get_parser(prog_name) + parser.add_argument( + 'dhcp_agent', + help='ID of the DHCP agent') + parser.add_argument( + 'network', + help='network to remove') + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _net_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'network', parsed_args.network) + neutron_client.remove_network_from_dhcp_agent( + parsed_args.dhcp_agent, _net_id) + print >>self.app.stdout, ( + _('Removed network %s to DHCP agent') % parsed_args.network) + + +class ListNetworksOnDhcpAgent(network.ListNetwork): + """List the networks on a DHCP agent.""" + + log = logging.getLogger(__name__ + '.ListNetworksOnDhcpAgent') + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListNetworksOnDhcpAgent, + self).get_parser(prog_name) + parser.add_argument( + 'dhcp_agent', + help='ID of the DHCP agent') + return parser + + def call_server(self, neutron_client, search_opts, parsed_args): + data = neutron_client.list_networks_on_dhcp_agent( + parsed_args.dhcp_agent, **search_opts) + return data + + +class ListDhcpAgentsHostingNetwork(neutronV20.ListCommand): + """List DHCP agents hosting a network.""" + + resource = 'agent' + _formatters = {} + log = logging.getLogger(__name__ + '.ListDhcpAgentsHostingNetwork') + list_columns = ['id', 'host', 'admin_state_up', 'alive'] + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListDhcpAgentsHostingNetwork, + self).get_parser(prog_name) + parser.add_argument( + 'network', + help='network to query') + return parser + + def extend_list(self, data, parsed_args): + for agent in data: + agent['alive'] = ":-)" if agent['alive'] else 'xxx' + + def call_server(self, neutron_client, search_opts, parsed_args): + _id = neutronV20.find_resourceid_by_name_or_id(neutron_client, + 'network', + parsed_args.network) + search_opts['network'] = _id + data = neutron_client.list_dhcp_agent_hosting_networks(**search_opts) + return data + + +class AddRouterToL3Agent(neutronV20.NeutronCommand): + """Add a router to a L3 agent.""" + + log = logging.getLogger(__name__ + '.AddRouterToL3Agent') + + def get_parser(self, prog_name): + parser = super(AddRouterToL3Agent, self).get_parser(prog_name) + parser.add_argument( + 'l3_agent', + help='ID of the L3 agent') + parser.add_argument( + 'router', + help='router to add') + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'router', parsed_args.router) + neutron_client.add_router_to_l3_agent(parsed_args.l3_agent, + {'router_id': _id}) + print >>self.app.stdout, ( + _('Added router %s to L3 agent') % parsed_args.router) + + +class RemoveRouterFromL3Agent(neutronV20.NeutronCommand): + """Remove a router from a L3 agent.""" + + log = logging.getLogger(__name__ + '.RemoveRouterFromL3Agent') + + def get_parser(self, prog_name): + parser = super(RemoveRouterFromL3Agent, self).get_parser(prog_name) + parser.add_argument( + 'l3_agent', + help='ID of the L3 agent') + parser.add_argument( + 'router', + help='router to remove') + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'router', parsed_args.router) + neutron_client.remove_router_from_l3_agent( + parsed_args.l3_agent, _id) + print >>self.app.stdout, ( + _('Removed Router %s to L3 agent') % parsed_args.router) + + +class ListRoutersOnL3Agent(neutronV20.ListCommand): + """List the routers on a L3 agent.""" + + log = logging.getLogger(__name__ + '.ListRoutersOnL3Agent') + _formatters = {'external_gateway_info': + router._format_external_gateway_info} + list_columns = ['id', 'name', 'external_gateway_info'] + resource = 'router' + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListRoutersOnL3Agent, + self).get_parser(prog_name) + parser.add_argument( + 'l3_agent', + help='ID of the L3 agent to query') + return parser + + def call_server(self, neutron_client, search_opts, parsed_args): + data = neutron_client.list_routers_on_l3_agent( + parsed_args.l3_agent, **search_opts) + return data + + +class ListL3AgentsHostingRouter(neutronV20.ListCommand): + """List L3 agents hosting a router.""" + + resource = 'agent' + _formatters = {} + log = logging.getLogger(__name__ + '.ListL3AgentsHostingRouter') + list_columns = ['id', 'host', 'admin_state_up', 'alive', 'default'] + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListL3AgentsHostingRouter, + self).get_parser(prog_name) + parser.add_argument('router', + help='router to query') + return parser + + def extend_list(self, data, parsed_args): + for agent in data: + agent['alive'] = ":-)" if agent['alive'] else 'xxx' + + def call_server(self, neutron_client, search_opts, parsed_args): + _id = neutronV20.find_resourceid_by_name_or_id(neutron_client, + 'router', + parsed_args.router) + search_opts['router'] = _id + data = neutron_client.list_l3_agent_hosting_routers(**search_opts) + return data diff --git a/neutronclient/neutron/v2_0/extension.py b/neutronclient/neutron/v2_0/extension.py new file mode 100644 index 000000000..b2a641135 --- /dev/null +++ b/neutronclient/neutron/v2_0/extension.py @@ -0,0 +1,44 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import logging + +from neutronclient.neutron import v2_0 as cmd_base + + +class ListExt(cmd_base.ListCommand): + """List all extensions.""" + + resource = 'extension' + log = logging.getLogger(__name__ + '.ListExt') + list_columns = ['alias', 'name'] + + +class ShowExt(cmd_base.ShowCommand): + """Show information of a given resource.""" + + resource = "extension" + log = logging.getLogger(__name__ + '.ShowExt') + allow_names = False + + def get_parser(self, prog_name): + parser = super(cmd_base.ShowCommand, self).get_parser(prog_name) + cmd_base.add_show_list_common_argument(parser) + parser.add_argument( + 'id', metavar='EXT-ALIAS', + help='the extension alias') + return parser diff --git a/neutronclient/neutron/v2_0/floatingip.py b/neutronclient/neutron/v2_0/floatingip.py new file mode 100644 index 000000000..7931f31f3 --- /dev/null +++ b/neutronclient/neutron/v2_0/floatingip.py @@ -0,0 +1,151 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import argparse +import logging + +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.openstack.common.gettextutils import _ + + +class ListFloatingIP(neutronV20.ListCommand): + """List floating ips that belong to a given tenant.""" + + resource = 'floatingip' + log = logging.getLogger(__name__ + '.ListFloatingIP') + list_columns = ['id', 'fixed_ip_address', 'floating_ip_address', + 'port_id'] + pagination_support = True + sorting_support = True + + +class ShowFloatingIP(neutronV20.ShowCommand): + """Show information of a given floating ip.""" + + resource = 'floatingip' + log = logging.getLogger(__name__ + '.ShowFloatingIP') + allow_names = False + + +class CreateFloatingIP(neutronV20.CreateCommand): + """Create a floating ip for a given tenant.""" + + resource = 'floatingip' + log = logging.getLogger(__name__ + '.CreateFloatingIP') + + def add_known_arguments(self, parser): + parser.add_argument( + 'floating_network_id', metavar='FLOATING_NETWORK', + help='Network name or id to allocate floating IP from') + parser.add_argument( + '--port-id', + help='ID of the port to be associated with the floatingip') + parser.add_argument( + '--port_id', + help=argparse.SUPPRESS) + parser.add_argument( + '--fixed-ip-address', + help=('IP address on the port (only required if port has multiple' + 'IPs)')) + parser.add_argument( + '--fixed_ip_address', + help=argparse.SUPPRESS) + + def args2body(self, parsed_args): + _network_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'network', parsed_args.floating_network_id) + body = {self.resource: {'floating_network_id': _network_id}} + if parsed_args.port_id: + body[self.resource].update({'port_id': parsed_args.port_id}) + if parsed_args.tenant_id: + body[self.resource].update({'tenant_id': parsed_args.tenant_id}) + if parsed_args.fixed_ip_address: + body[self.resource].update({'fixed_ip_address': + parsed_args.fixed_ip_address}) + return body + + +class DeleteFloatingIP(neutronV20.DeleteCommand): + """Delete a given floating ip.""" + + log = logging.getLogger(__name__ + '.DeleteFloatingIP') + resource = 'floatingip' + allow_names = False + + +class AssociateFloatingIP(neutronV20.NeutronCommand): + """Create a mapping between a floating ip and a fixed ip.""" + + api = 'network' + log = logging.getLogger(__name__ + '.AssociateFloatingIP') + resource = 'floatingip' + + def get_parser(self, prog_name): + parser = super(AssociateFloatingIP, self).get_parser(prog_name) + parser.add_argument( + 'floatingip_id', metavar='FLOATINGIP_ID', + help='ID of the floating IP to associate') + parser.add_argument( + 'port_id', metavar='PORT', + help='ID or name of the port to be associated with the floatingip') + parser.add_argument( + '--fixed-ip-address', + help=('IP address on the port (only required if port has multiple' + 'IPs)')) + parser.add_argument( + '--fixed_ip_address', + help=argparse.SUPPRESS) + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + update_dict = {} + if parsed_args.port_id: + update_dict['port_id'] = parsed_args.port_id + if parsed_args.fixed_ip_address: + update_dict['fixed_ip_address'] = parsed_args.fixed_ip_address + neutron_client.update_floatingip(parsed_args.floatingip_id, + {'floatingip': update_dict}) + print >>self.app.stdout, ( + _('Associated floatingip %s') % parsed_args.floatingip_id) + + +class DisassociateFloatingIP(neutronV20.NeutronCommand): + """Remove a mapping from a floating ip to a fixed ip. + """ + + api = 'network' + log = logging.getLogger(__name__ + '.DisassociateFloatingIP') + resource = 'floatingip' + + def get_parser(self, prog_name): + parser = super(DisassociateFloatingIP, self).get_parser(prog_name) + parser.add_argument( + 'floatingip_id', metavar='FLOATINGIP_ID', + help='ID of the floating IP to associate') + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + neutron_client.update_floatingip(parsed_args.floatingip_id, + {'floatingip': {'port_id': None}}) + print >>self.app.stdout, ( + _('Disassociated floatingip %s') % parsed_args.floatingip_id) diff --git a/quantum/__init__.py b/neutronclient/neutron/v2_0/lb/__init__.py similarity index 82% rename from quantum/__init__.py rename to neutronclient/neutron/v2_0/lb/__init__.py index d5dcad659..1668497e7 100644 --- a/quantum/__init__.py +++ b/neutronclient/neutron/v2_0/lb/__init__.py @@ -1,7 +1,5 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC -# All Rights Reserved. +# Copyright 2013 OpenStack LLC. +# 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 @@ -14,6 +12,5 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - -from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 diff --git a/neutronclient/neutron/v2_0/lb/healthmonitor.py b/neutronclient/neutron/v2_0/lb/healthmonitor.py new file mode 100644 index 000000000..a6e847a9d --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/healthmonitor.py @@ -0,0 +1,174 @@ +# Copyright 2013 Mirantis 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: Ilya Shakhat, Mirantis Inc. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import logging + +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.openstack.common.gettextutils import _ + + +class ListHealthMonitor(neutronV20.ListCommand): + """List healthmonitors that belong to a given tenant.""" + + resource = 'health_monitor' + log = logging.getLogger(__name__ + '.ListHealthMonitor') + list_columns = ['id', 'type', 'admin_state_up', 'status'] + pagination_support = True + sorting_support = True + + +class ShowHealthMonitor(neutronV20.ShowCommand): + """Show information of a given healthmonitor.""" + + resource = 'health_monitor' + log = logging.getLogger(__name__ + '.ShowHealthMonitor') + + +class CreateHealthMonitor(neutronV20.CreateCommand): + """Create a healthmonitor.""" + + resource = 'health_monitor' + log = logging.getLogger(__name__ + '.CreateHealthMonitor') + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help='set admin state up to false') + parser.add_argument( + '--expected-codes', + help='the list of HTTP status codes expected in ' + 'response from the member to declare it healthy. This ' + 'attribute can contain one value, ' + 'or a list of values separated by comma, ' + 'or a range of values (e.g. "200-299"). If this attribute ' + 'is not specified, it defaults to "200". ') + parser.add_argument( + '--http-method', + help='the HTTP method used for requests by the monitor of type ' + 'HTTP.') + parser.add_argument( + '--url-path', + help='the HTTP path used in the HTTP request used by the monitor' + ' to test a member health. This must be a string ' + 'beginning with a / (forward slash)') + parser.add_argument( + '--delay', + required=True, + help='the minimum time in seconds between regular connections ' + 'of the member.') + parser.add_argument( + '--max-retries', + required=True, + help='number of permissible connection failures before changing ' + 'the member status to INACTIVE.') + parser.add_argument( + '--timeout', + required=True, + help='maximum number of seconds for a monitor to wait for a ' + 'connection to be established before it times out. The ' + 'value must be less than the delay value.') + parser.add_argument( + '--type', + required=True, + help='one of predefined health monitor types, e.g. RoundRobin') + + def args2body(self, parsed_args): + body = { + self.resource: { + 'admin_state_up': parsed_args.admin_state, + 'delay': parsed_args.delay, + 'max_retries': parsed_args.max_retries, + 'timeout': parsed_args.timeout, + 'type': parsed_args.type, + }, + } + neutronV20.update_dict(parsed_args, body[self.resource], + ['expected_codes', 'http_method', 'url_path', + 'tenant_id']) + return body + + +class UpdateHealthMonitor(neutronV20.UpdateCommand): + """Update a given healthmonitor.""" + + resource = 'health_monitor' + log = logging.getLogger(__name__ + '.UpdateHealthMonitor') + + +class DeleteHealthMonitor(neutronV20.DeleteCommand): + """Delete a given healthmonitor.""" + + resource = 'health_monitor' + log = logging.getLogger(__name__ + '.DeleteHealthMonitor') + + +class AssociateHealthMonitor(neutronV20.NeutronCommand): + """Create a mapping between a health monitor and a pool.""" + + log = logging.getLogger(__name__ + '.AssociateHealthMonitor') + resource = 'health_monitor' + + def get_parser(self, prog_name): + parser = super(AssociateHealthMonitor, self).get_parser(prog_name) + parser.add_argument( + 'health_monitor_id', + help='Health monitor to associate') + parser.add_argument( + 'pool_id', + help='ID of the pool to be associated with the health monitor') + return parser + + def run(self, parsed_args): + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + body = {'health_monitor': {'id': parsed_args.health_monitor_id}} + pool_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'pool', parsed_args.pool_id) + neutron_client.associate_health_monitor(pool_id, body) + print >>self.app.stdout, (_('Associated health monitor ' + '%s') % parsed_args.health_monitor_id) + + +class DisassociateHealthMonitor(neutronV20.NeutronCommand): + """Remove a mapping from a health monitor to a pool.""" + + log = logging.getLogger(__name__ + '.DisassociateHealthMonitor') + resource = 'health_monitor' + + def get_parser(self, prog_name): + parser = super(DisassociateHealthMonitor, self).get_parser(prog_name) + parser.add_argument( + 'health_monitor_id', + help='Health monitor to associate') + parser.add_argument( + 'pool_id', + help='ID of the pool to be associated with the health monitor') + return parser + + def run(self, parsed_args): + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + pool_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'pool', parsed_args.pool_id) + neutron_client.disassociate_health_monitor(pool_id, + parsed_args + .health_monitor_id) + print >>self.app.stdout, (_('Disassociated health monitor ' + '%s') % parsed_args.health_monitor_id) diff --git a/neutronclient/neutron/v2_0/lb/member.py b/neutronclient/neutron/v2_0/lb/member.py new file mode 100644 index 000000000..b13fdcd86 --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/member.py @@ -0,0 +1,99 @@ +# Copyright 2013 Mirantis 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: Ilya Shakhat, Mirantis Inc. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import logging + +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListMember(neutronV20.ListCommand): + """List members that belong to a given tenant.""" + + resource = 'member' + log = logging.getLogger(__name__ + '.ListMember') + list_columns = [ + 'id', 'address', 'protocol_port', 'admin_state_up', 'status' + ] + pagination_support = True + sorting_support = True + + +class ShowMember(neutronV20.ShowCommand): + """Show information of a given member.""" + + resource = 'member' + log = logging.getLogger(__name__ + '.ShowMember') + + +class CreateMember(neutronV20.CreateCommand): + """Create a member.""" + + resource = 'member' + log = logging.getLogger(__name__ + '.CreateMember') + + def add_known_arguments(self, parser): + parser.add_argument( + 'pool_id', metavar='pool', + help='Pool id or name this vip belongs to') + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help='set admin state up to false') + parser.add_argument( + '--weight', + help='weight of pool member in the pool') + parser.add_argument( + '--address', + required=True, + help='IP address of the pool member on the pool network. ') + parser.add_argument( + '--protocol-port', + required=True, + help='port on which the pool member listens for requests or ' + 'connections. ') + + def args2body(self, parsed_args): + _pool_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'pool', parsed_args.pool_id) + body = { + self.resource: { + 'pool_id': _pool_id, + 'admin_state_up': parsed_args.admin_state, + }, + } + neutronV20.update_dict( + parsed_args, + body[self.resource], + ['address', 'protocol_port', 'weight', 'tenant_id'] + ) + return body + + +class UpdateMember(neutronV20.UpdateCommand): + """Update a given member.""" + + resource = 'member' + log = logging.getLogger(__name__ + '.UpdateMember') + + +class DeleteMember(neutronV20.DeleteCommand): + """Delete a given member.""" + + resource = 'member' + log = logging.getLogger(__name__ + '.DeleteMember') diff --git a/neutronclient/neutron/v2_0/lb/pool.py b/neutronclient/neutron/v2_0/lb/pool.py new file mode 100644 index 000000000..4fb1e79a8 --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/pool.py @@ -0,0 +1,124 @@ +# Copyright 2013 Mirantis 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: Ilya Shakhat, Mirantis Inc. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import logging + +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListPool(neutronV20.ListCommand): + """List pools that belong to a given tenant.""" + + resource = 'pool' + log = logging.getLogger(__name__ + '.ListPool') + list_columns = ['id', 'name', 'lb_method', 'protocol', + 'admin_state_up', 'status'] + pagination_support = True + sorting_support = True + + +class ShowPool(neutronV20.ShowCommand): + """Show information of a given pool.""" + + resource = 'pool' + log = logging.getLogger(__name__ + '.ShowPool') + + +class CreatePool(neutronV20.CreateCommand): + """Create a pool.""" + + resource = 'pool' + log = logging.getLogger(__name__ + '.CreatePool') + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help='set admin state up to false') + parser.add_argument( + '--description', + help='description of the pool') + parser.add_argument( + '--lb-method', + required=True, + help='the algorithm used to distribute load between the members ' + 'of the pool') + parser.add_argument( + '--name', + required=True, + help='the name of the pool') + parser.add_argument( + '--protocol', + required=True, + help='protocol for balancing') + parser.add_argument( + '--subnet-id', + required=True, + help='the subnet on which the members of the pool will be located') + + def args2body(self, parsed_args): + _subnet_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'subnet', parsed_args.subnet_id) + body = { + self.resource: { + 'admin_state_up': parsed_args.admin_state, + 'subnet_id': _subnet_id, + }, + } + neutronV20.update_dict(parsed_args, body[self.resource], + ['description', 'lb_method', 'name', + 'protocol', 'tenant_id']) + return body + + +class UpdatePool(neutronV20.UpdateCommand): + """Update a given pool.""" + + resource = 'pool' + log = logging.getLogger(__name__ + '.UpdatePool') + + +class DeletePool(neutronV20.DeleteCommand): + """Delete a given pool.""" + + resource = 'pool' + log = logging.getLogger(__name__ + '.DeletePool') + + +class RetrievePoolStats(neutronV20.ShowCommand): + """Retrieve stats for a given pool.""" + + resource = 'pool' + log = logging.getLogger(__name__ + '.RetrievePoolStats') + + def get_data(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + params = {} + if parsed_args.fields: + params = {'fields': parsed_args.fields} + + data = neutron_client.retrieve_pool_stats(parsed_args.id, **params) + self.format_output_data(data) + stats = data['stats'] + if 'stats' in data: + return zip(*sorted(stats.iteritems())) + else: + return None diff --git a/neutronclient/neutron/v2_0/lb/vip.py b/neutronclient/neutron/v2_0/lb/vip.py new file mode 100644 index 000000000..f836862bf --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/vip.py @@ -0,0 +1,115 @@ +# Copyright 2013 Mirantis 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: Ilya Shakhat, Mirantis Inc. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import logging + +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListVip(neutronV20.ListCommand): + """List vips that belong to a given tenant.""" + + resource = 'vip' + log = logging.getLogger(__name__ + '.ListVip') + list_columns = ['id', 'name', 'algorithm', 'address', 'protocol', + 'admin_state_up', 'status'] + pagination_support = True + sorting_support = True + + +class ShowVip(neutronV20.ShowCommand): + """Show information of a given vip.""" + + resource = 'vip' + log = logging.getLogger(__name__ + '.ShowVip') + + +class CreateVip(neutronV20.CreateCommand): + """Create a vip.""" + + resource = 'vip' + log = logging.getLogger(__name__ + '.CreateVip') + + def add_known_arguments(self, parser): + parser.add_argument( + 'pool_id', metavar='pool', + help='Pool id or name this vip belongs to') + parser.add_argument( + '--address', + help='IP address of the vip') + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help='set admin state up to false') + parser.add_argument( + '--connection-limit', + help='the maximum number of connections per second allowed for ' + 'the vip') + parser.add_argument( + '--description', + help='description of the vip') + parser.add_argument( + '--name', + required=True, + help='name of the vip') + parser.add_argument( + '--protocol-port', + required=True, + help='TCP port on which to listen for client traffic that is ' + 'associated with the vip address') + parser.add_argument( + '--protocol', + required=True, + help='protocol for balancing') + parser.add_argument( + '--subnet-id', + required=True, + help='the subnet on which to allocate the vip address') + + def args2body(self, parsed_args): + _pool_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'pool', parsed_args.pool_id) + _subnet_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'subnet', parsed_args.subnet_id) + body = { + self.resource: { + 'pool_id': _pool_id, + 'admin_state_up': parsed_args.admin_state, + 'subnet_id': _subnet_id, + }, + } + neutronV20.update_dict(parsed_args, body[self.resource], + ['address', 'connection_limit', 'description', + 'name', 'protocol_port', 'protocol', + 'tenant_id']) + return body + + +class UpdateVip(neutronV20.UpdateCommand): + """Update a given vip.""" + + resource = 'vip' + log = logging.getLogger(__name__ + '.UpdateVip') + + +class DeleteVip(neutronV20.DeleteCommand): + """Delete a given vip.""" + + resource = 'vip' + log = logging.getLogger(__name__ + '.DeleteVip') diff --git a/neutronclient/neutron/v2_0/network.py b/neutronclient/neutron/v2_0/network.py new file mode 100644 index 000000000..0f384ab95 --- /dev/null +++ b/neutronclient/neutron/v2_0/network.py @@ -0,0 +1,152 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import argparse +import logging + +from neutronclient.common import exceptions +from neutronclient.neutron import v2_0 as neutronV20 + + +def _format_subnets(network): + try: + return '\n'.join([' '.join([s['id'], s.get('cidr', '')]) + for s in network['subnets']]) + except Exception: + return '' + + +class ListNetwork(neutronV20.ListCommand): + """List networks that belong to a given tenant.""" + + # Length of a query filter on subnet id + # id=& (with len(uuid)=36) + subnet_id_filter_len = 40 + resource = 'network' + log = logging.getLogger(__name__ + '.ListNetwork') + _formatters = {'subnets': _format_subnets, } + list_columns = ['id', 'name', 'subnets'] + pagination_support = True + sorting_support = True + + def extend_list(self, data, parsed_args): + """Add subnet information to a network list.""" + neutron_client = self.get_client() + search_opts = {'fields': ['id', 'cidr']} + if self.pagination_support: + page_size = parsed_args.page_size + if page_size: + search_opts.update({'limit': page_size}) + subnet_ids = [] + for n in data: + if 'subnets' in n: + subnet_ids.extend(n['subnets']) + + def _get_subnet_list(sub_ids): + search_opts['id'] = sub_ids + return neutron_client.list_subnets( + **search_opts).get('subnets', []) + + try: + subnets = _get_subnet_list(subnet_ids) + except exceptions.RequestURITooLong as uri_len_exc: + # The URI is too long because of too many subnet_id filters + # Use the excess attribute of the exception to know how many + # subnet_id filters can be inserted into a single request + subnet_count = len(subnet_ids) + max_size = ((self.subnet_id_filter_len * subnet_count) - + uri_len_exc.excess) + chunk_size = max_size / self.subnet_id_filter_len + subnets = [] + for i in xrange(0, subnet_count, chunk_size): + subnets.extend( + _get_subnet_list(subnet_ids[i: i + chunk_size])) + + subnet_dict = dict([(s['id'], s) for s in subnets]) + for n in data: + if 'subnets' in n: + n['subnets'] = [(subnet_dict.get(s) or {"id": s}) + for s in n['subnets']] + + +class ListExternalNetwork(ListNetwork): + """List external networks that belong to a given tenant.""" + + log = logging.getLogger(__name__ + '.ListExternalNetwork') + pagination_support = True + sorting_support = True + + def retrieve_list(self, parsed_args): + external = '--router:external=True' + if external not in self.values_specs: + self.values_specs.append('--router:external=True') + return super(ListExternalNetwork, self).retrieve_list(parsed_args) + + +class ShowNetwork(neutronV20.ShowCommand): + """Show information of a given network.""" + + resource = 'network' + log = logging.getLogger(__name__ + '.ShowNetwork') + + +class CreateNetwork(neutronV20.CreateCommand): + """Create a network for a given tenant.""" + + resource = 'network' + log = logging.getLogger(__name__ + '.CreateNetwork') + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help='Set Admin State Up to false') + parser.add_argument( + '--admin_state_down', + dest='admin_state', action='store_false', + help=argparse.SUPPRESS) + parser.add_argument( + '--shared', + action='store_true', + help='Set the network as shared') + parser.add_argument( + 'name', metavar='NAME', + help='Name of network to create') + + def args2body(self, parsed_args): + body = {'network': { + 'name': parsed_args.name, + 'admin_state_up': parsed_args.admin_state}, } + if parsed_args.tenant_id: + body['network'].update({'tenant_id': parsed_args.tenant_id}) + if parsed_args.shared: + body['network'].update({'shared': parsed_args.shared}) + return body + + +class DeleteNetwork(neutronV20.DeleteCommand): + """Delete a given network.""" + + log = logging.getLogger(__name__ + '.DeleteNetwork') + resource = 'network' + + +class UpdateNetwork(neutronV20.UpdateCommand): + """Update network's information.""" + + log = logging.getLogger(__name__ + '.UpdateNetwork') + resource = 'network' diff --git a/neutronclient/neutron/v2_0/nvp_qos_queue.py b/neutronclient/neutron/v2_0/nvp_qos_queue.py new file mode 100644 index 000000000..79a1801dd --- /dev/null +++ b/neutronclient/neutron/v2_0/nvp_qos_queue.py @@ -0,0 +1,89 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Nicira 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. + +import logging + +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListQoSQueue(neutronV20.ListCommand): + """List queues that belong to a given tenant.""" + + resource = 'qos_queue' + log = logging.getLogger(__name__ + '.ListQoSQueue') + list_columns = ['id', 'name', 'min', 'max', + 'qos_marking', 'dscp', 'default'] + + +class ShowQoSQueue(neutronV20.ShowCommand): + """Show information of a given queue.""" + + resource = 'qos_queue' + log = logging.getLogger(__name__ + '.ShowQoSQueue') + allow_names = True + + +class CreateQoSQueue(neutronV20.CreateCommand): + """Create a queue.""" + + resource = 'qos_queue' + log = logging.getLogger(__name__ + '.CreateQoSQueue') + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', metavar='NAME', + help='Name of queue') + parser.add_argument( + '--min', + help='min-rate'), + parser.add_argument( + '--max', + help='max-rate'), + parser.add_argument( + '--qos-marking', + help='qos marking untrusted/trusted'), + parser.add_argument( + '--default', + default=False, + help=('If true all ports created with be the size of this queue' + ' if queue is not specified')), + parser.add_argument( + '--dscp', + help='Differentiated Services Code Point'), + + def args2body(self, parsed_args): + params = {'name': parsed_args.name, + 'default': parsed_args.default} + if parsed_args.min: + params['min'] = parsed_args.min + if parsed_args.max: + params['max'] = parsed_args.max + if parsed_args.qos_marking: + params['qos_marking'] = parsed_args.qos_marking + if parsed_args.dscp: + params['dscp'] = parsed_args.dscp + if parsed_args.tenant_id: + params['tenant_id'] = parsed_args.tenant_id + return {'qos_queue': params} + + +class DeleteQoSQueue(neutronV20.DeleteCommand): + """Delete a given queue.""" + + log = logging.getLogger(__name__ + '.DeleteQoSQueue') + resource = 'qos_queue' + allow_names = True diff --git a/neutronclient/neutron/v2_0/nvpnetworkgateway.py b/neutronclient/neutron/v2_0/nvpnetworkgateway.py new file mode 100644 index 000000000..1853514ef --- /dev/null +++ b/neutronclient/neutron/v2_0/nvpnetworkgateway.py @@ -0,0 +1,159 @@ +# Copyright 2013 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import logging + +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.openstack.common.gettextutils import _ + +RESOURCE = 'network_gateway' + + +class ListNetworkGateway(neutronV20.ListCommand): + """List network gateways for a given tenant.""" + + resource = RESOURCE + log = logging.getLogger(__name__ + '.ListNetworkGateway') + list_columns = ['id', 'name'] + + +class ShowNetworkGateway(neutronV20.ShowCommand): + """Show information of a given network gateway.""" + + resource = RESOURCE + log = logging.getLogger(__name__ + '.ShowNetworkGateway') + + +class CreateNetworkGateway(neutronV20.CreateCommand): + """Create a network gateway.""" + + resource = RESOURCE + log = logging.getLogger(__name__ + '.CreateNetworkGateway') + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', metavar='NAME', + help='Name of network gateway to create') + parser.add_argument( + '--device', + action='append', + help='device info for this gateway ' + 'device_id=,' + 'interface_name= ' + 'It can be repeated for multiple devices for HA gateways') + + def args2body(self, parsed_args): + body = {self.resource: { + 'name': parsed_args.name}} + devices = [] + if parsed_args.device: + for device in parsed_args.device: + devices.append(utils.str2dict(device)) + if devices: + body[self.resource].update({'devices': devices}) + if parsed_args.tenant_id: + body[self.resource].update({'tenant_id': parsed_args.tenant_id}) + return body + + +class DeleteNetworkGateway(neutronV20.DeleteCommand): + """Delete a given network gateway.""" + + resource = RESOURCE + log = logging.getLogger(__name__ + '.DeleteNetworkGateway') + + +class UpdateNetworkGateway(neutronV20.UpdateCommand): + """Update the name for a network gateway.""" + + resource = RESOURCE + log = logging.getLogger(__name__ + '.UpdateNetworkGateway') + + +class NetworkGatewayInterfaceCommand(neutronV20.NeutronCommand): + """Base class for connecting/disconnecting networks to/from a gateway.""" + + resource = RESOURCE + + def get_parser(self, prog_name): + parser = super(NetworkGatewayInterfaceCommand, + self).get_parser(prog_name) + parser.add_argument( + 'net_gateway_id', metavar='NET-GATEWAY-ID', + help='ID of the network gateway') + parser.add_argument( + 'network_id', metavar='NETWORK-ID', + help='ID of the internal network to connect on the gateway') + parser.add_argument( + '--segmentation-type', + help=('L2 segmentation strategy on the external side of ' + 'the gateway (e.g.: VLAN, FLAT)')) + parser.add_argument( + '--segmentation-id', + help=('Identifier for the L2 segment on the external side ' + 'of the gateway')) + return parser + + def retrieve_ids(self, client, args): + gateway_id = neutronV20.find_resourceid_by_name_or_id( + client, self.resource, args.net_gateway_id) + network_id = neutronV20.find_resourceid_by_name_or_id( + client, 'network', args.network_id) + return (gateway_id, network_id) + + +class ConnectNetworkGateway(NetworkGatewayInterfaceCommand): + """Add an internal network interface to a router.""" + + log = logging.getLogger(__name__ + '.ConnectNetworkGateway') + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + (gateway_id, network_id) = self.retrieve_ids(neutron_client, + parsed_args) + neutron_client.connect_network_gateway( + gateway_id, {'network_id': network_id, + 'segmentation_type': parsed_args.segmentation_type, + 'segmentation_id': parsed_args.segmentation_id}) + # TODO(Salvatore-Orlando): Do output formatting as + # any other command + print >>self.app.stdout, ( + _('Connected network to gateway %s') % gateway_id) + + +class DisconnectNetworkGateway(NetworkGatewayInterfaceCommand): + """Remove a network from a network gateway.""" + + log = logging.getLogger(__name__ + '.DisconnectNetworkGateway') + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + (gateway_id, network_id) = self.retrieve_ids(neutron_client, + parsed_args) + neutron_client.disconnect_network_gateway( + gateway_id, {'network_id': network_id, + 'segmentation_type': parsed_args.segmentation_type, + 'segmentation_id': parsed_args.segmentation_id}) + # TODO(Salvatore-Orlando): Do output formatting as + # any other command + print >>self.app.stdout, ( + _('Disconnected network from gateway %s') % gateway_id) diff --git a/neutronclient/neutron/v2_0/port.py b/neutronclient/neutron/v2_0/port.py new file mode 100644 index 000000000..f8ac9a9f4 --- /dev/null +++ b/neutronclient/neutron/v2_0/port.py @@ -0,0 +1,184 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import argparse +import logging + +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _format_fixed_ips(port): + try: + return '\n'.join([utils.dumps(ip) for ip in port['fixed_ips']]) + except Exception: + return '' + + +class ListPort(neutronV20.ListCommand): + """List ports that belong to a given tenant.""" + + resource = 'port' + log = logging.getLogger(__name__ + '.ListPort') + _formatters = {'fixed_ips': _format_fixed_ips, } + list_columns = ['id', 'name', 'mac_address', 'fixed_ips'] + pagination_support = True + sorting_support = True + + +class ListRouterPort(neutronV20.ListCommand): + """List ports that belong to a given tenant, with specified router.""" + + resource = 'port' + log = logging.getLogger(__name__ + '.ListRouterPort') + _formatters = {'fixed_ips': _format_fixed_ips, } + list_columns = ['id', 'name', 'mac_address', 'fixed_ips'] + pagination_support = True + sorting_support = True + + def get_parser(self, prog_name): + parser = super(ListRouterPort, self).get_parser(prog_name) + parser.add_argument( + 'id', metavar='router', + help='ID or name of router to look up') + return parser + + def get_data(self, parsed_args): + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'router', parsed_args.id) + self.values_specs.append('--device_id=%s' % _id) + return super(ListRouterPort, self).get_data(parsed_args) + + +class ShowPort(neutronV20.ShowCommand): + """Show information of a given port.""" + + resource = 'port' + log = logging.getLogger(__name__ + '.ShowPort') + + +class CreatePort(neutronV20.CreateCommand): + """Create a port for a given tenant.""" + + resource = 'port' + log = logging.getLogger(__name__ + '.CreatePort') + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help='name of this port') + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help='set admin state up to false') + parser.add_argument( + '--admin_state_down', + dest='admin_state', action='store_false', + help=argparse.SUPPRESS) + parser.add_argument( + '--mac-address', + help='mac address of this port') + parser.add_argument( + '--mac_address', + help=argparse.SUPPRESS) + parser.add_argument( + '--device-id', + help='device id of this port') + parser.add_argument( + '--device_id', + help=argparse.SUPPRESS) + parser.add_argument( + '--fixed-ip', metavar='ip_address=IP_ADDR', + action='append', + help='desired IP for this port: ' + 'subnet_id=,ip_address=, ' + '(This option can be repeated.)') + parser.add_argument( + '--fixed_ip', + action='append', + help=argparse.SUPPRESS) + parser.add_argument( + '--security-group', metavar='SECURITY_GROUP', + default=[], action='append', dest='security_groups', + help='security group associated with the port ' + '(This option can be repeated)') + parser.add_argument( + 'network_id', metavar='NETWORK', + help='Network id or name this port belongs to') + + def args2body(self, parsed_args): + _network_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'network', parsed_args.network_id) + body = {'port': {'admin_state_up': parsed_args.admin_state, + 'network_id': _network_id, }, } + if parsed_args.mac_address: + body['port'].update({'mac_address': parsed_args.mac_address}) + if parsed_args.device_id: + body['port'].update({'device_id': parsed_args.device_id}) + if parsed_args.tenant_id: + body['port'].update({'tenant_id': parsed_args.tenant_id}) + if parsed_args.name: + body['port'].update({'name': parsed_args.name}) + ips = [] + if parsed_args.fixed_ip: + for ip_spec in parsed_args.fixed_ip: + ip_dict = utils.str2dict(ip_spec) + if 'subnet_id' in ip_dict: + subnet_name_id = ip_dict['subnet_id'] + _subnet_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'subnet', subnet_name_id) + ip_dict['subnet_id'] = _subnet_id + ips.append(ip_dict) + if ips: + body['port'].update({'fixed_ips': ips}) + + _sgids = [] + for sg in parsed_args.security_groups: + _sgids.append(neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'security_group', sg)) + if _sgids: + body['port']['security_groups'] = _sgids + + return body + + +class DeletePort(neutronV20.DeleteCommand): + """Delete a given port.""" + + resource = 'port' + log = logging.getLogger(__name__ + '.DeletePort') + + +class UpdatePort(neutronV20.UpdateCommand): + """Update port's information.""" + + resource = 'port' + log = logging.getLogger(__name__ + '.UpdatePort') + + def add_known_arguments(self, parser): + parser.add_argument( + '--no-security-groups', + action='store_true', + help='remove security groups from port') + + def args2body(self, parsed_args): + body = {'port': {}} + if parsed_args.no_security_groups: + body['port'].update({'security_groups': None}) + return body diff --git a/neutronclient/neutron/v2_0/quota.py b/neutronclient/neutron/v2_0/quota.py new file mode 100644 index 000000000..bc568efd4 --- /dev/null +++ b/neutronclient/neutron/v2_0/quota.py @@ -0,0 +1,232 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import argparse +import logging + +from cliff import lister +from cliff import show + +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.openstack.common.gettextutils import _ + + +def get_tenant_id(tenant_id, client): + return (tenant_id if tenant_id else + client.get_quotas_tenant()['tenant']['tenant_id']) + + +class DeleteQuota(neutronV20.NeutronCommand): + """Delete defined quotas of a given tenant.""" + + api = 'network' + resource = 'quota' + log = logging.getLogger(__name__ + '.DeleteQuota') + + def get_parser(self, prog_name): + parser = super(DeleteQuota, self).get_parser(prog_name) + parser.add_argument( + '--tenant-id', metavar='tenant-id', + help='the owner tenant ID') + parser.add_argument( + '--tenant_id', + help=argparse.SUPPRESS) + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + tenant_id = get_tenant_id(parsed_args.tenant_id, + neutron_client) + obj_deleter = getattr(neutron_client, + "delete_%s" % self.resource) + obj_deleter(tenant_id) + print >>self.app.stdout, (_('Deleted %(resource)s: %(tenant_id)s') + % {'tenant_id': tenant_id, + 'resource': self.resource}) + return + + +class ListQuota(neutronV20.NeutronCommand, lister.Lister): + """List defined quotas of all tenants.""" + + api = 'network' + resource = 'quota' + log = logging.getLogger(__name__ + '.ListQuota') + + def get_parser(self, prog_name): + parser = super(ListQuota, self).get_parser(prog_name) + return parser + + def get_data(self, parsed_args): + self.log.debug('get_data(%s)' % parsed_args) + neutron_client = self.get_client() + search_opts = {} + self.log.debug('search options: %s', search_opts) + neutron_client.format = parsed_args.request_format + obj_lister = getattr(neutron_client, + "list_%ss" % self.resource) + data = obj_lister(**search_opts) + info = [] + collection = self.resource + "s" + if collection in data: + info = data[collection] + _columns = len(info) > 0 and sorted(info[0].keys()) or [] + return (_columns, (utils.get_item_properties(s, _columns) + for s in info)) + + +class ShowQuota(neutronV20.NeutronCommand, show.ShowOne): + """Show quotas of a given tenant + + """ + api = 'network' + resource = "quota" + log = logging.getLogger(__name__ + '.ShowQuota') + + def get_parser(self, prog_name): + parser = super(ShowQuota, self).get_parser(prog_name) + parser.add_argument( + '--tenant-id', metavar='tenant-id', + help='the owner tenant ID') + parser.add_argument( + '--tenant_id', + help=argparse.SUPPRESS) + return parser + + def get_data(self, parsed_args): + self.log.debug('get_data(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + tenant_id = get_tenant_id(parsed_args.tenant_id, + neutron_client) + params = {} + obj_shower = getattr(neutron_client, + "show_%s" % self.resource) + data = obj_shower(tenant_id, **params) + if self.resource in data: + for k, v in data[self.resource].iteritems(): + if isinstance(v, list): + value = "" + for _item in v: + if value: + value += "\n" + if isinstance(_item, dict): + value += utils.dumps(_item) + else: + value += str(_item) + data[self.resource][k] = value + elif v is None: + data[self.resource][k] = '' + return zip(*sorted(data[self.resource].iteritems())) + else: + return None + + +class UpdateQuota(neutronV20.NeutronCommand, show.ShowOne): + """Define tenant's quotas not to use defaults.""" + + resource = 'quota' + log = logging.getLogger(__name__ + '.UpdateQuota') + + def get_parser(self, prog_name): + parser = super(UpdateQuota, self).get_parser(prog_name) + parser.add_argument( + '--tenant-id', metavar='tenant-id', + help='the owner tenant ID') + parser.add_argument( + '--tenant_id', + help=argparse.SUPPRESS) + parser.add_argument( + '--network', metavar='networks', + help='the limit of networks') + parser.add_argument( + '--subnet', metavar='subnets', + help='the limit of subnets') + parser.add_argument( + '--port', metavar='ports', + help='the limit of ports') + parser.add_argument( + '--router', metavar='routers', + help='the limit of routers') + parser.add_argument( + '--floatingip', metavar='floatingips', + help='the limit of floating IPs') + parser.add_argument( + '--security-group', metavar='security_groups', + help='the limit of security groups') + parser.add_argument( + '--security-group-rule', metavar='security_group_rules', + help='the limit of security groups rules') + return parser + + def _validate_int(self, name, value): + try: + return_value = int(value) + except Exception: + message = (_('quota limit for %(name)s must be an integer') % + {'name': name}) + raise exceptions.NeutronClientException(message=message) + return return_value + + def args2body(self, parsed_args): + quota = {} + for resource in ('network', 'subnet', 'port', 'router', 'floatingip', + 'security_group', 'security_group_rule'): + if getattr(parsed_args, resource): + quota[resource] = self._validate_int( + resource, + getattr(parsed_args, resource)) + return {self.resource: quota} + + def get_data(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _extra_values = neutronV20.parse_args_to_dict(self.values_specs) + neutronV20._merge_args(self, parsed_args, _extra_values, + self.values_specs) + body = self.args2body(parsed_args) + if self.resource in body: + body[self.resource].update(_extra_values) + else: + body[self.resource] = _extra_values + obj_updator = getattr(neutron_client, + "update_%s" % self.resource) + tenant_id = get_tenant_id(parsed_args.tenant_id, + neutron_client) + data = obj_updator(tenant_id, body) + if self.resource in data: + for k, v in data[self.resource].iteritems(): + if isinstance(v, list): + value = "" + for _item in v: + if value: + value += "\n" + if isinstance(_item, dict): + value += utils.dumps(_item) + else: + value += str(_item) + data[self.resource][k] = value + elif v is None: + data[self.resource][k] = '' + return zip(*sorted(data[self.resource].iteritems())) + else: + return None diff --git a/neutronclient/neutron/v2_0/router.py b/neutronclient/neutron/v2_0/router.py new file mode 100644 index 000000000..54b811738 --- /dev/null +++ b/neutronclient/neutron/v2_0/router.py @@ -0,0 +1,230 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import argparse +import logging + +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.openstack.common.gettextutils import _ + + +def _format_external_gateway_info(router): + try: + return utils.dumps(router['external_gateway_info']) + except Exception: + return '' + + +class ListRouter(neutronV20.ListCommand): + """List routers that belong to a given tenant.""" + + resource = 'router' + log = logging.getLogger(__name__ + '.ListRouter') + _formatters = {'external_gateway_info': _format_external_gateway_info, } + list_columns = ['id', 'name', 'external_gateway_info'] + pagination_support = True + sorting_support = True + + +class ShowRouter(neutronV20.ShowCommand): + """Show information of a given router.""" + + resource = 'router' + log = logging.getLogger(__name__ + '.ShowRouter') + + +class CreateRouter(neutronV20.CreateCommand): + """Create a router for a given tenant.""" + + resource = 'router' + log = logging.getLogger(__name__ + '.CreateRouter') + _formatters = {'external_gateway_info': _format_external_gateway_info, } + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help='Set Admin State Up to false') + parser.add_argument( + '--admin_state_down', + dest='admin_state', action='store_false', + help=argparse.SUPPRESS) + parser.add_argument( + 'name', metavar='NAME', + help='Name of router to create') + + def args2body(self, parsed_args): + body = {'router': { + 'name': parsed_args.name, + 'admin_state_up': parsed_args.admin_state, }, } + if parsed_args.tenant_id: + body['router'].update({'tenant_id': parsed_args.tenant_id}) + return body + + +class DeleteRouter(neutronV20.DeleteCommand): + """Delete a given router.""" + + log = logging.getLogger(__name__ + '.DeleteRouter') + resource = 'router' + + +class UpdateRouter(neutronV20.UpdateCommand): + """Update router's information.""" + + log = logging.getLogger(__name__ + '.UpdateRouter') + resource = 'router' + + +class RouterInterfaceCommand(neutronV20.NeutronCommand): + """Based class to Add/Remove router interface.""" + + api = 'network' + resource = 'router' + + def call_api(self, neutron_client, router_id, body): + raise NotImplementedError() + + def success_message(self, router_id, portinfo): + raise NotImplementedError() + + def get_parser(self, prog_name): + parser = super(RouterInterfaceCommand, self).get_parser(prog_name) + parser.add_argument( + 'router_id', metavar='router-id', + help='ID of the router') + parser.add_argument( + 'interface', metavar='INTERFACE', + help='The format is "SUBNET|subnet=SUBNET|port=PORT". ' + 'Either a subnet or port must be specified. ' + 'Both ID and name are accepted as SUBNET or PORT. ' + 'Note that "subnet=" can be omitted when specifying subnet.') + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + + if '=' in parsed_args.interface: + resource, value = parsed_args.interface.split('=', 1) + if resource not in ['subnet', 'port']: + exceptions.CommandError('You must specify either subnet or ' + 'port for INTERFACE parameter.') + else: + resource = 'subnet' + value = parsed_args.interface + + _router_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, self.resource, parsed_args.router_id) + + _interface_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, resource, value) + body = {'%s_id' % resource: _interface_id} + + portinfo = self.call_api(neutron_client, _router_id, body) + print >>self.app.stdout, self.success_message(parsed_args.router_id, + portinfo) + + +class AddInterfaceRouter(RouterInterfaceCommand): + """Add an internal network interface to a router.""" + + log = logging.getLogger(__name__ + '.AddInterfaceRouter') + + def call_api(self, neutron_client, router_id, body): + return neutron_client.add_interface_router(router_id, body) + + def success_message(self, router_id, portinfo): + return (_('Added interface %(port)s to router %(router)s.') % + {'router': router_id, 'port': portinfo['port_id']}) + + +class RemoveInterfaceRouter(RouterInterfaceCommand): + """Remove an internal network interface from a router.""" + + log = logging.getLogger(__name__ + '.RemoveInterfaceRouter') + + def call_api(self, neutron_client, router_id, body): + return neutron_client.remove_interface_router(router_id, body) + + def success_message(self, router_id, portinfo): + # portinfo is not used since it is None for router-interface-delete. + return _('Removed interface from router %s.') % router_id + + +class SetGatewayRouter(neutronV20.NeutronCommand): + """Set the external network gateway for a router.""" + + log = logging.getLogger(__name__ + '.SetGatewayRouter') + api = 'network' + resource = 'router' + + def get_parser(self, prog_name): + parser = super(SetGatewayRouter, self).get_parser(prog_name) + parser.add_argument( + 'router_id', metavar='router-id', + help='ID of the router') + parser.add_argument( + 'external_network_id', metavar='external-network-id', + help='ID of the external network for the gateway') + parser.add_argument( + '--disable-snat', action='store_false', dest='enable_snat', + help='Disable Source NAT on the router gateway') + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _router_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, self.resource, parsed_args.router_id) + _ext_net_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'network', parsed_args.external_network_id) + neutron_client.add_gateway_router( + _router_id, + {'network_id': _ext_net_id, + 'enable_snat': parsed_args.enable_snat}) + print >>self.app.stdout, ( + _('Set gateway for router %s') % parsed_args.router_id) + + +class RemoveGatewayRouter(neutronV20.NeutronCommand): + """Remove an external network gateway from a router.""" + + log = logging.getLogger(__name__ + '.RemoveGatewayRouter') + api = 'network' + resource = 'router' + + def get_parser(self, prog_name): + parser = super(RemoveGatewayRouter, self).get_parser(prog_name) + parser.add_argument( + 'router_id', metavar='router-id', + help='ID of the router') + return parser + + def run(self, parsed_args): + self.log.debug('run(%s)' % parsed_args) + neutron_client = self.get_client() + neutron_client.format = parsed_args.request_format + _router_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, self.resource, parsed_args.router_id) + neutron_client.remove_gateway_router(_router_id) + print >>self.app.stdout, ( + _('Removed gateway from router %s') % parsed_args.router_id) diff --git a/neutronclient/neutron/v2_0/securitygroup.py b/neutronclient/neutron/v2_0/securitygroup.py new file mode 100644 index 000000000..92570fa1b --- /dev/null +++ b/neutronclient/neutron/v2_0/securitygroup.py @@ -0,0 +1,259 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import argparse +import logging + +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListSecurityGroup(neutronV20.ListCommand): + """List security groups that belong to a given tenant.""" + + resource = 'security_group' + log = logging.getLogger(__name__ + '.ListSecurityGroup') + list_columns = ['id', 'name', 'description'] + pagination_support = True + sorting_support = True + + +class ShowSecurityGroup(neutronV20.ShowCommand): + """Show information of a given security group.""" + + resource = 'security_group' + log = logging.getLogger(__name__ + '.ShowSecurityGroup') + allow_names = True + + +class CreateSecurityGroup(neutronV20.CreateCommand): + """Create a security group.""" + + resource = 'security_group' + log = logging.getLogger(__name__ + '.CreateSecurityGroup') + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', metavar='NAME', + help='Name of security group') + parser.add_argument( + '--description', + help='description of security group') + + def args2body(self, parsed_args): + body = {'security_group': { + 'name': parsed_args.name}} + if parsed_args.description: + body['security_group'].update( + {'description': parsed_args.description}) + if parsed_args.tenant_id: + body['security_group'].update({'tenant_id': parsed_args.tenant_id}) + return body + + +class DeleteSecurityGroup(neutronV20.DeleteCommand): + """Delete a given security group.""" + + log = logging.getLogger(__name__ + '.DeleteSecurityGroup') + resource = 'security_group' + allow_names = True + + +class UpdateSecurityGroup(neutronV20.UpdateCommand): + """Update a given security group.""" + + log = logging.getLogger(__name__ + '.UpdateSecurityGroup') + resource = 'security_group' + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help='Name of security group') + parser.add_argument( + '--description', + help='description of security group') + + def args2body(self, parsed_args): + body = {'security_group': {}} + if parsed_args.name: + body['security_group'].update( + {'name': parsed_args.name}) + if parsed_args.description: + body['security_group'].update( + {'description': parsed_args.description}) + return body + + +class ListSecurityGroupRule(neutronV20.ListCommand): + """List security group rules that belong to a given tenant.""" + + resource = 'security_group_rule' + log = logging.getLogger(__name__ + '.ListSecurityGroupRule') + list_columns = ['id', 'security_group_id', 'direction', 'protocol', + 'remote_ip_prefix', 'remote_group_id'] + replace_rules = {'security_group_id': 'security_group', + 'remote_group_id': 'remote_group'} + pagination_support = True + sorting_support = True + + def get_parser(self, prog_name): + parser = super(ListSecurityGroupRule, self).get_parser(prog_name) + parser.add_argument( + '--no-nameconv', action='store_true', + help='Do not convert security group ID to its name') + return parser + + @staticmethod + def replace_columns(cols, rules, reverse=False): + if reverse: + rules = dict((rules[k], k) for k in rules.keys()) + return [rules.get(col, col) for col in cols] + + def retrieve_list(self, parsed_args): + parsed_args.fields = self.replace_columns(parsed_args.fields, + self.replace_rules, + reverse=True) + return super(ListSecurityGroupRule, self).retrieve_list(parsed_args) + + def extend_list(self, data, parsed_args): + if parsed_args.no_nameconv: + return + neutron_client = self.get_client() + search_opts = {'fields': ['id', 'name']} + if self.pagination_support: + page_size = parsed_args.page_size + if page_size: + search_opts.update({'limit': page_size}) + sec_group_ids = set() + for rule in data: + for key in self.replace_rules: + sec_group_ids.add(rule[key]) + search_opts.update({"id": sec_group_ids}) + secgroups = neutron_client.list_security_groups(**search_opts) + secgroups = secgroups.get('security_groups', []) + sg_dict = dict([(sg['id'], sg['name']) + for sg in secgroups if sg['name']]) + for rule in data: + for key in self.replace_rules: + rule[key] = sg_dict.get(rule[key], rule[key]) + + def setup_columns(self, info, parsed_args): + parsed_args.columns = self.replace_columns(parsed_args.columns, + self.replace_rules, + reverse=True) + # NOTE(amotoki): 2nd element of the tuple returned by setup_columns() + # is a generator, so if you need to create a look using the generator + # object, you need to recreate a generator to show a list expectedly. + info = super(ListSecurityGroupRule, self).setup_columns(info, + parsed_args) + cols = info[0] + if not parsed_args.no_nameconv: + cols = self.replace_columns(info[0], self.replace_rules) + parsed_args.columns = cols + return (cols, info[1]) + + +class ShowSecurityGroupRule(neutronV20.ShowCommand): + """Show information of a given security group rule.""" + + resource = 'security_group_rule' + log = logging.getLogger(__name__ + '.ShowSecurityGroupRule') + allow_names = False + + +class CreateSecurityGroupRule(neutronV20.CreateCommand): + """Create a security group rule.""" + + resource = 'security_group_rule' + log = logging.getLogger(__name__ + '.CreateSecurityGroupRule') + + def add_known_arguments(self, parser): + parser.add_argument( + 'security_group_id', metavar='SECURITY_GROUP', + help='Security group name or id to add rule.') + parser.add_argument( + '--direction', + default='ingress', choices=['ingress', 'egress'], + help='direction of traffic: ingress/egress') + parser.add_argument( + '--ethertype', + default='IPv4', + help='IPv4/IPv6') + parser.add_argument( + '--protocol', + help='protocol of packet') + parser.add_argument( + '--port-range-min', + help='starting port range') + parser.add_argument( + '--port_range_min', + help=argparse.SUPPRESS) + parser.add_argument( + '--port-range-max', + help='ending port range') + parser.add_argument( + '--port_range_max', + help=argparse.SUPPRESS) + parser.add_argument( + '--remote-ip-prefix', + help='cidr to match on') + parser.add_argument( + '--remote_ip_prefix', + help=argparse.SUPPRESS) + parser.add_argument( + '--remote-group-id', metavar='REMOTE_GROUP', + help='remote security group name or id to apply rule') + parser.add_argument( + '--remote_group_id', + help=argparse.SUPPRESS) + + def args2body(self, parsed_args): + _security_group_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'security_group', parsed_args.security_group_id) + body = {'security_group_rule': { + 'security_group_id': _security_group_id, + 'direction': parsed_args.direction, + 'ethertype': parsed_args.ethertype}} + if parsed_args.protocol: + body['security_group_rule'].update( + {'protocol': parsed_args.protocol}) + if parsed_args.port_range_min: + body['security_group_rule'].update( + {'port_range_min': parsed_args.port_range_min}) + if parsed_args.port_range_max: + body['security_group_rule'].update( + {'port_range_max': parsed_args.port_range_max}) + if parsed_args.remote_ip_prefix: + body['security_group_rule'].update( + {'remote_ip_prefix': parsed_args.remote_ip_prefix}) + if parsed_args.remote_group_id: + _remote_group_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'security_group', + parsed_args.remote_group_id) + body['security_group_rule'].update( + {'remote_group_id': _remote_group_id}) + if parsed_args.tenant_id: + body['security_group_rule'].update( + {'tenant_id': parsed_args.tenant_id}) + return body + + +class DeleteSecurityGroupRule(neutronV20.DeleteCommand): + """Delete a given security group rule.""" + + log = logging.getLogger(__name__ + '.DeleteSecurityGroupRule') + resource = 'security_group_rule' + allow_names = False diff --git a/neutronclient/neutron/v2_0/subnet.py b/neutronclient/neutron/v2_0/subnet.py new file mode 100644 index 000000000..3674db89d --- /dev/null +++ b/neutronclient/neutron/v2_0/subnet.py @@ -0,0 +1,168 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import argparse +import logging + +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _format_allocation_pools(subnet): + try: + return '\n'.join([utils.dumps(pool) for pool in + subnet['allocation_pools']]) + except Exception: + return '' + + +def _format_dns_nameservers(subnet): + try: + return '\n'.join([utils.dumps(server) for server in + subnet['dns_nameservers']]) + except Exception: + return '' + + +def _format_host_routes(subnet): + try: + return '\n'.join([utils.dumps(route) for route in + subnet['host_routes']]) + except Exception: + return '' + + +class ListSubnet(neutronV20.ListCommand): + """List networks that belong to a given tenant.""" + + resource = 'subnet' + log = logging.getLogger(__name__ + '.ListSubnet') + _formatters = {'allocation_pools': _format_allocation_pools, + 'dns_nameservers': _format_dns_nameservers, + 'host_routes': _format_host_routes, } + list_columns = ['id', 'name', 'cidr', 'allocation_pools'] + pagination_support = True + sorting_support = True + + +class ShowSubnet(neutronV20.ShowCommand): + """Show information of a given subnet.""" + + resource = 'subnet' + log = logging.getLogger(__name__ + '.ShowSubnet') + + +class CreateSubnet(neutronV20.CreateCommand): + """Create a subnet for a given tenant.""" + + resource = 'subnet' + log = logging.getLogger(__name__ + '.CreateSubnet') + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help='name of this subnet') + parser.add_argument( + '--ip-version', + type=int, + default=4, choices=[4, 6], + help='IP version with default 4') + parser.add_argument( + '--ip_version', + type=int, + choices=[4, 6], + help=argparse.SUPPRESS) + parser.add_argument( + '--gateway', metavar='GATEWAY_IP', + help='gateway ip of this subnet') + parser.add_argument( + '--no-gateway', + action='store_true', + help='No distribution of gateway') + parser.add_argument( + '--allocation-pool', metavar='start=IP_ADDR,end=IP_ADDR', + action='append', dest='allocation_pools', type=utils.str2dict, + help='Allocation pool IP addresses for this subnet ' + '(This option can be repeated)') + parser.add_argument( + '--allocation_pool', + action='append', dest='allocation_pools', type=utils.str2dict, + help=argparse.SUPPRESS) + parser.add_argument( + '--host-route', metavar='destination=CIDR,nexthop=IP_ADDR', + action='append', dest='host_routes', type=utils.str2dict, + help='Additional route (This option can be repeated)') + parser.add_argument( + '--dns-nameserver', metavar='DNS_NAMESERVER', + action='append', dest='dns_nameservers', + help='DNS name server for this subnet ' + '(This option can be repeated)') + parser.add_argument( + '--disable-dhcp', + action='store_true', + help='Disable DHCP for this subnet') + parser.add_argument( + 'network_id', metavar='NETWORK', + help='network id or name this subnet belongs to') + parser.add_argument( + 'cidr', metavar='CIDR', + help='cidr of subnet to create') + + def args2body(self, parsed_args): + _network_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'network', parsed_args.network_id) + body = {'subnet': {'cidr': parsed_args.cidr, + 'network_id': _network_id, + 'ip_version': parsed_args.ip_version, }, } + + if parsed_args.gateway and parsed_args.no_gateway: + raise exceptions.CommandError("--gateway option and " + "--no-gateway option can " + "not be used same time") + if parsed_args.no_gateway: + body['subnet'].update({'gateway_ip': None}) + if parsed_args.gateway: + body['subnet'].update({'gateway_ip': parsed_args.gateway}) + if parsed_args.tenant_id: + body['subnet'].update({'tenant_id': parsed_args.tenant_id}) + if parsed_args.name: + body['subnet'].update({'name': parsed_args.name}) + if parsed_args.disable_dhcp: + body['subnet'].update({'enable_dhcp': False}) + if parsed_args.allocation_pools: + body['subnet']['allocation_pools'] = parsed_args.allocation_pools + if parsed_args.host_routes: + body['subnet']['host_routes'] = parsed_args.host_routes + if parsed_args.dns_nameservers: + body['subnet']['dns_nameservers'] = parsed_args.dns_nameservers + + return body + + +class DeleteSubnet(neutronV20.DeleteCommand): + """Delete a given subnet.""" + + resource = 'subnet' + log = logging.getLogger(__name__ + '.DeleteSubnet') + + +class UpdateSubnet(neutronV20.UpdateCommand): + """Update subnet's information.""" + + resource = 'subnet' + log = logging.getLogger(__name__ + '.UpdateSubnet') diff --git a/quantum/client/tests/__init__.py b/neutronclient/openstack/__init__.py similarity index 100% rename from quantum/client/tests/__init__.py rename to neutronclient/openstack/__init__.py diff --git a/quantum/client/tests/unit/__init__.py b/neutronclient/openstack/common/__init__.py similarity index 100% rename from quantum/client/tests/unit/__init__.py rename to neutronclient/openstack/common/__init__.py diff --git a/neutronclient/openstack/common/exception.py b/neutronclient/openstack/common/exception.py new file mode 100644 index 000000000..bb6e6dc6a --- /dev/null +++ b/neutronclient/openstack/common/exception.py @@ -0,0 +1,142 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack Foundation. +# 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. + +""" +Exceptions common to OpenStack projects +""" + +import logging + +from neutronclient.openstack.common.gettextutils import _ + +_FATAL_EXCEPTION_FORMAT_ERRORS = False + + +class Error(Exception): + def __init__(self, message=None): + super(Error, self).__init__(message) + + +class ApiError(Error): + def __init__(self, message='Unknown', code='Unknown'): + self.message = message + self.code = code + super(ApiError, self).__init__('%s: %s' % (code, message)) + + +class NotFound(Error): + pass + + +class UnknownScheme(Error): + + msg = "Unknown scheme '%s' found in URI" + + def __init__(self, scheme): + msg = self.__class__.msg % scheme + super(UnknownScheme, self).__init__(msg) + + +class BadStoreUri(Error): + + msg = "The Store URI %s was malformed. Reason: %s" + + def __init__(self, uri, reason): + msg = self.__class__.msg % (uri, reason) + super(BadStoreUri, self).__init__(msg) + + +class Duplicate(Error): + pass + + +class NotAuthorized(Error): + pass + + +class NotEmpty(Error): + pass + + +class Invalid(Error): + pass + + +class BadInputError(Exception): + """Error resulting from a client sending bad input to a server""" + pass + + +class MissingArgumentError(Error): + pass + + +class DatabaseMigrationError(Error): + pass + + +class ClientConnectionError(Exception): + """Error resulting from a client connecting to a server""" + pass + + +def wrap_exception(f): + def _wrap(*args, **kw): + try: + return f(*args, **kw) + except Exception, e: + if not isinstance(e, Error): + #exc_type, exc_value, exc_traceback = sys.exc_info() + logging.exception(_('Uncaught exception')) + #logging.error(traceback.extract_stack(exc_traceback)) + raise Error(str(e)) + raise + _wrap.func_name = f.func_name + return _wrap + + +class OpenstackException(Exception): + """ + Base Exception + + To correctly use this class, inherit from it and define + a 'message' property. That message will get printf'd + with the keyword arguments provided to the constructor. + """ + message = "An unknown exception occurred" + + def __init__(self, **kwargs): + try: + self._error_string = self.message % kwargs + + except Exception as e: + if _FATAL_EXCEPTION_FORMAT_ERRORS: + raise e + else: + # at least get the core message out if something happened + self._error_string = self.message + + def __str__(self): + return self._error_string + + +class MalformedRequestBody(OpenstackException): + message = "Malformed message body: %(reason)s" + + +class InvalidContentType(OpenstackException): + message = "Invalid content type %(content_type)s" diff --git a/neutronclient/openstack/common/gettextutils.py b/neutronclient/openstack/common/gettextutils.py new file mode 100644 index 000000000..5c597c7e2 --- /dev/null +++ b/neutronclient/openstack/common/gettextutils.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Red Hat, 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. + +""" +gettext for openstack-common modules. + +Usual usage in an openstack.common module: + + from neutronclient.openstack.common.gettextutils import _ +""" + +import gettext + + +t = gettext.translation('openstack-common', 'locale', fallback=True) + + +def _(msg): + return t.ugettext(msg) diff --git a/neutronclient/openstack/common/jsonutils.py b/neutronclient/openstack/common/jsonutils.py new file mode 100644 index 000000000..9ff741597 --- /dev/null +++ b/neutronclient/openstack/common/jsonutils.py @@ -0,0 +1,148 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Justin Santa Barbara +# 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. + +''' +JSON related utilities. + +This module provides a few things: + + 1) A handy function for getting an object down to something that can be + JSON serialized. See to_primitive(). + + 2) Wrappers around loads() and dumps(). The dumps() wrapper will + automatically use to_primitive() for you if needed. + + 3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson + is available. +''' + + +import datetime +import inspect +import itertools +import json +import xmlrpclib + +from neutronclient.openstack.common import timeutils + + +def to_primitive(value, convert_instances=False, level=0): + """Convert a complex object into primitives. + + Handy for JSON serialization. We can optionally handle instances, + but since this is a recursive function, we could have cyclical + data structures. + + To handle cyclical data structures we could track the actual objects + visited in a set, but not all objects are hashable. Instead we just + track the depth of the object inspections and don't go too deep. + + Therefore, convert_instances=True is lossy ... be aware. + + """ + nasty = [inspect.ismodule, inspect.isclass, inspect.ismethod, + inspect.isfunction, inspect.isgeneratorfunction, + inspect.isgenerator, inspect.istraceback, inspect.isframe, + inspect.iscode, inspect.isbuiltin, inspect.isroutine, + inspect.isabstract] + for test in nasty: + if test(value): + return unicode(value) + + # value of itertools.count doesn't get caught by inspects + # above and results in infinite loop when list(value) is called. + if type(value) == itertools.count: + return unicode(value) + + # FIXME(vish): Workaround for LP bug 852095. Without this workaround, + # tests that raise an exception in a mocked method that + # has a @wrap_exception with a notifier will fail. If + # we up the dependency to 0.5.4 (when it is released) we + # can remove this workaround. + if getattr(value, '__module__', None) == 'mox': + return 'mock' + + if level > 3: + return '?' + + # The try block may not be necessary after the class check above, + # but just in case ... + try: + # It's not clear why xmlrpclib created their own DateTime type, but + # for our purposes, make it a datetime type which is explicitly + # handled + if isinstance(value, xmlrpclib.DateTime): + value = datetime.datetime(*tuple(value.timetuple())[:6]) + + if isinstance(value, (list, tuple)): + o = [] + for v in value: + o.append(to_primitive(v, convert_instances=convert_instances, + level=level)) + return o + elif isinstance(value, dict): + o = {} + for k, v in value.iteritems(): + o[k] = to_primitive(v, convert_instances=convert_instances, + level=level) + return o + elif isinstance(value, datetime.datetime): + return timeutils.strtime(value) + elif hasattr(value, 'iteritems'): + return to_primitive(dict(value.iteritems()), + convert_instances=convert_instances, + level=level + 1) + elif hasattr(value, '__iter__'): + return to_primitive(list(value), + convert_instances=convert_instances, + level=level) + elif convert_instances and hasattr(value, '__dict__'): + # Likely an instance of something. Watch for cycles. + # Ignore class member vars. + return to_primitive(value.__dict__, + convert_instances=convert_instances, + level=level + 1) + else: + return value + except TypeError: + # Class objects are tricky since they may define something like + # __iter__ defined but it isn't callable as list(). + return unicode(value) + + +def dumps(value, default=to_primitive, **kwargs): + return json.dumps(value, default=default, **kwargs) + + +def loads(s): + return json.loads(s) + + +def load(s): + return json.load(s) + + +try: + import anyjson +except ImportError: + pass +else: + anyjson._modules.append((__name__, 'dumps', TypeError, + 'loads', ValueError, 'load')) + anyjson.force_implementation(__name__) diff --git a/neutronclient/openstack/common/strutils.py b/neutronclient/openstack/common/strutils.py new file mode 100644 index 000000000..ecf3cfdc4 --- /dev/null +++ b/neutronclient/openstack/common/strutils.py @@ -0,0 +1,133 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack Foundation. +# 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. + +""" +System-level utilities and helper functions. +""" + +import logging +import sys + +LOG = logging.getLogger(__name__) + + +def int_from_bool_as_string(subject): + """ + Interpret a string as a boolean and return either 1 or 0. + + Any string value in: + + ('True', 'true', 'On', 'on', '1') + + is interpreted as a boolean True. + + Useful for JSON-decoded stuff and config file parsing + """ + return bool_from_string(subject) and 1 or 0 + + +def bool_from_string(subject): + """ + Interpret a string as a boolean. + + Any string value in: + + ('True', 'true', 'On', 'on', 'Yes', 'yes', '1') + + is interpreted as a boolean True. + + Useful for JSON-decoded stuff and config file parsing + """ + if isinstance(subject, bool): + return subject + if isinstance(subject, basestring): + if subject.strip().lower() in ('true', 'on', 'yes', '1'): + return True + return False + + +def safe_decode(text, incoming=None, errors='strict'): + """ + Decodes incoming str using `incoming` if they're + not already unicode. + + :param incoming: Text's current encoding + :param errors: Errors handling policy. See here for valid + values http://docs.python.org/2/library/codecs.html + :returns: text or a unicode `incoming` encoded + representation of it. + :raises TypeError: If text is not an isntance of basestring + """ + if not isinstance(text, basestring): + raise TypeError("%s can't be decoded" % type(text)) + + if isinstance(text, unicode): + return text + + if not incoming: + incoming = (sys.stdin.encoding or + sys.getdefaultencoding()) + + try: + return text.decode(incoming, errors) + except UnicodeDecodeError: + # Note(flaper87) If we get here, it means that + # sys.stdin.encoding / sys.getdefaultencoding + # didn't return a suitable encoding to decode + # text. This happens mostly when global LANG + # var is not set correctly and there's no + # default encoding. In this case, most likely + # python will use ASCII or ANSI encoders as + # default encodings but they won't be capable + # of decoding non-ASCII characters. + # + # Also, UTF-8 is being used since it's an ASCII + # extension. + return text.decode('utf-8', errors) + + +def safe_encode(text, incoming=None, + encoding='utf-8', errors='strict'): + """ + Encodes incoming str/unicode using `encoding`. If + incoming is not specified, text is expected to + be encoded with current python's default encoding. + (`sys.getdefaultencoding`) + + :param incoming: Text's current encoding + :param encoding: Expected encoding for text (Default UTF-8) + :param errors: Errors handling policy. See here for valid + values http://docs.python.org/2/library/codecs.html + :returns: text or a bytestring `encoding` encoded + representation of it. + :raises TypeError: If text is not an isntance of basestring + """ + if not isinstance(text, basestring): + raise TypeError("%s can't be encoded" % type(text)) + + if not incoming: + incoming = (sys.stdin.encoding or + sys.getdefaultencoding()) + + if isinstance(text, unicode): + return text.encode(encoding, errors) + elif text and encoding != incoming: + # Decode text before encoding it with `encoding` + text = safe_decode(text, incoming, errors) + return text.encode(encoding, errors) + + return text diff --git a/neutronclient/openstack/common/timeutils.py b/neutronclient/openstack/common/timeutils.py new file mode 100644 index 000000000..0f346087f --- /dev/null +++ b/neutronclient/openstack/common/timeutils.py @@ -0,0 +1,164 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +""" +Time related utilities and helper functions. +""" + +import calendar +import datetime + +import iso8601 + + +TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" +PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" + + +def isotime(at=None): + """Stringify time in ISO 8601 format""" + if not at: + at = utcnow() + str = at.strftime(TIME_FORMAT) + tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' + str += ('Z' if tz == 'UTC' else tz) + return str + + +def parse_isotime(timestr): + """Parse time from ISO 8601 format""" + try: + return iso8601.parse_date(timestr) + except iso8601.ParseError as e: + raise ValueError(e.message) + except TypeError as e: + raise ValueError(e.message) + + +def strtime(at=None, fmt=PERFECT_TIME_FORMAT): + """Returns formatted utcnow.""" + if not at: + at = utcnow() + return at.strftime(fmt) + + +def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): + """Turn a formatted time back into a datetime.""" + return datetime.datetime.strptime(timestr, fmt) + + +def normalize_time(timestamp): + """Normalize time in arbitrary timezone to UTC naive object""" + offset = timestamp.utcoffset() + if offset is None: + return timestamp + return timestamp.replace(tzinfo=None) - offset + + +def is_older_than(before, seconds): + """Return True if before is older than seconds.""" + if isinstance(before, basestring): + before = parse_strtime(before).replace(tzinfo=None) + return utcnow() - before > datetime.timedelta(seconds=seconds) + + +def is_newer_than(after, seconds): + """Return True if after is newer than seconds.""" + if isinstance(after, basestring): + after = parse_strtime(after).replace(tzinfo=None) + return after - utcnow() > datetime.timedelta(seconds=seconds) + + +def utcnow_ts(): + """Timestamp version of our utcnow function.""" + return calendar.timegm(utcnow().timetuple()) + + +def utcnow(): + """Overridable version of utils.utcnow.""" + if utcnow.override_time: + try: + return utcnow.override_time.pop(0) + except AttributeError: + return utcnow.override_time + return datetime.datetime.utcnow() + + +utcnow.override_time = None + + +def set_time_override(override_time=datetime.datetime.utcnow()): + """ + Override utils.utcnow to return a constant time or a list thereof, + one at a time. + """ + utcnow.override_time = override_time + + +def advance_time_delta(timedelta): + """Advance overridden time using a datetime.timedelta.""" + assert(not utcnow.override_time is None) + try: + for dt in utcnow.override_time: + dt += timedelta + except TypeError: + utcnow.override_time += timedelta + + +def advance_time_seconds(seconds): + """Advance overridden time by seconds.""" + advance_time_delta(datetime.timedelta(0, seconds)) + + +def clear_time_override(): + """Remove the overridden time.""" + utcnow.override_time = None + + +def marshall_now(now=None): + """Make an rpc-safe datetime with microseconds. + + Note: tzinfo is stripped, but not required for relative times.""" + if not now: + now = utcnow() + return dict(day=now.day, month=now.month, year=now.year, hour=now.hour, + minute=now.minute, second=now.second, + microsecond=now.microsecond) + + +def unmarshall_time(tyme): + """Unmarshall a datetime dict.""" + return datetime.datetime(day=tyme['day'], + month=tyme['month'], + year=tyme['year'], + hour=tyme['hour'], + minute=tyme['minute'], + second=tyme['second'], + microsecond=tyme['microsecond']) + + +def delta_seconds(before, after): + """ + Compute the difference in seconds between two date, time, or + datetime objects (as a float, to microsecond resolution). + """ + delta = after - before + try: + return delta.total_seconds() + except AttributeError: + return ((delta.days * 24 * 3600) + delta.seconds + + float(delta.microseconds) / (10 ** 6)) diff --git a/neutronclient/shell.py b/neutronclient/shell.py new file mode 100644 index 000000000..89c7adf0c --- /dev/null +++ b/neutronclient/shell.py @@ -0,0 +1,583 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +""" +Command-line interface to the Neutron APIs +""" + +import argparse +import logging +import os +import sys + +from cliff import app +from cliff import commandmanager + +from neutronclient.common import clientmanager +from neutronclient.common import exceptions as exc +from neutronclient.common import utils +from neutronclient.neutron.v2_0 import agent +from neutronclient.neutron.v2_0 import agentscheduler +from neutronclient.neutron.v2_0 import extension +from neutronclient.neutron.v2_0 import floatingip +from neutronclient.neutron.v2_0.lb import healthmonitor as lb_healthmonitor +from neutronclient.neutron.v2_0.lb import member as lb_member +from neutronclient.neutron.v2_0.lb import pool as lb_pool +from neutronclient.neutron.v2_0.lb import vip as lb_vip +from neutronclient.neutron.v2_0 import network +from neutronclient.neutron.v2_0 import nvp_qos_queue +from neutronclient.neutron.v2_0 import nvpnetworkgateway +from neutronclient.neutron.v2_0 import port +from neutronclient.neutron.v2_0 import quota +from neutronclient.neutron.v2_0 import router +from neutronclient.neutron.v2_0 import securitygroup +from neutronclient.neutron.v2_0 import subnet +from neutronclient.openstack.common import strutils +from neutronclient.version import __version__ + + +VERSION = '2.0' +NEUTRON_API_VERSION = '2.0' + + +def run_command(cmd, cmd_parser, sub_argv): + _argv = sub_argv + index = -1 + values_specs = [] + if '--' in sub_argv: + index = sub_argv.index('--') + _argv = sub_argv[:index] + values_specs = sub_argv[index:] + known_args, _values_specs = cmd_parser.parse_known_args(_argv) + cmd.values_specs = (index == -1 and _values_specs or values_specs) + return cmd.run(known_args) + + +def env(*_vars, **kwargs): + """Search for the first defined of possibly many env vars + + Returns the first environment variable defined in vars, or + returns the default defined in kwargs. + + """ + for v in _vars: + value = os.environ.get(v, None) + if value: + return value + return kwargs.get('default', '') + + +COMMAND_V2 = { + 'net-list': network.ListNetwork, + 'net-external-list': network.ListExternalNetwork, + 'net-show': network.ShowNetwork, + 'net-create': network.CreateNetwork, + 'net-delete': network.DeleteNetwork, + 'net-update': network.UpdateNetwork, + 'subnet-list': subnet.ListSubnet, + 'subnet-show': subnet.ShowSubnet, + 'subnet-create': subnet.CreateSubnet, + 'subnet-delete': subnet.DeleteSubnet, + 'subnet-update': subnet.UpdateSubnet, + 'port-list': port.ListPort, + 'port-show': port.ShowPort, + 'port-create': port.CreatePort, + 'port-delete': port.DeletePort, + 'port-update': port.UpdatePort, + 'quota-list': quota.ListQuota, + 'quota-show': quota.ShowQuota, + 'quota-delete': quota.DeleteQuota, + 'quota-update': quota.UpdateQuota, + 'ext-list': extension.ListExt, + 'ext-show': extension.ShowExt, + 'router-list': router.ListRouter, + 'router-port-list': port.ListRouterPort, + 'router-show': router.ShowRouter, + 'router-create': router.CreateRouter, + 'router-delete': router.DeleteRouter, + 'router-update': router.UpdateRouter, + 'router-interface-add': router.AddInterfaceRouter, + 'router-interface-delete': router.RemoveInterfaceRouter, + 'router-gateway-set': router.SetGatewayRouter, + 'router-gateway-clear': router.RemoveGatewayRouter, + 'floatingip-list': floatingip.ListFloatingIP, + 'floatingip-show': floatingip.ShowFloatingIP, + 'floatingip-create': floatingip.CreateFloatingIP, + 'floatingip-delete': floatingip.DeleteFloatingIP, + 'floatingip-associate': floatingip.AssociateFloatingIP, + 'floatingip-disassociate': floatingip.DisassociateFloatingIP, + 'security-group-list': securitygroup.ListSecurityGroup, + 'security-group-show': securitygroup.ShowSecurityGroup, + 'security-group-create': securitygroup.CreateSecurityGroup, + 'security-group-delete': securitygroup.DeleteSecurityGroup, + 'security-group-update': securitygroup.UpdateSecurityGroup, + 'security-group-rule-list': securitygroup.ListSecurityGroupRule, + 'security-group-rule-show': securitygroup.ShowSecurityGroupRule, + 'security-group-rule-create': securitygroup.CreateSecurityGroupRule, + 'security-group-rule-delete': securitygroup.DeleteSecurityGroupRule, + 'lb-vip-list': lb_vip.ListVip, + 'lb-vip-show': lb_vip.ShowVip, + 'lb-vip-create': lb_vip.CreateVip, + 'lb-vip-update': lb_vip.UpdateVip, + 'lb-vip-delete': lb_vip.DeleteVip, + 'lb-pool-list': lb_pool.ListPool, + 'lb-pool-show': lb_pool.ShowPool, + 'lb-pool-create': lb_pool.CreatePool, + 'lb-pool-update': lb_pool.UpdatePool, + 'lb-pool-delete': lb_pool.DeletePool, + 'lb-pool-stats': lb_pool.RetrievePoolStats, + 'lb-member-list': lb_member.ListMember, + 'lb-member-show': lb_member.ShowMember, + 'lb-member-create': lb_member.CreateMember, + 'lb-member-update': lb_member.UpdateMember, + 'lb-member-delete': lb_member.DeleteMember, + 'lb-healthmonitor-list': lb_healthmonitor.ListHealthMonitor, + 'lb-healthmonitor-show': lb_healthmonitor.ShowHealthMonitor, + 'lb-healthmonitor-create': lb_healthmonitor.CreateHealthMonitor, + 'lb-healthmonitor-update': lb_healthmonitor.UpdateHealthMonitor, + 'lb-healthmonitor-delete': lb_healthmonitor.DeleteHealthMonitor, + 'lb-healthmonitor-associate': lb_healthmonitor.AssociateHealthMonitor, + 'lb-healthmonitor-disassociate': ( + lb_healthmonitor.DisassociateHealthMonitor + ), + 'queue-create': nvp_qos_queue.CreateQoSQueue, + 'queue-delete': nvp_qos_queue.DeleteQoSQueue, + 'queue-show': nvp_qos_queue.ShowQoSQueue, + 'queue-list': nvp_qos_queue.ListQoSQueue, + 'agent-list': agent.ListAgent, + 'agent-show': agent.ShowAgent, + 'agent-delete': agent.DeleteAgent, + 'agent-update': agent.UpdateAgent, + 'net-gateway-create': nvpnetworkgateway.CreateNetworkGateway, + 'net-gateway-update': nvpnetworkgateway.UpdateNetworkGateway, + 'net-gateway-delete': nvpnetworkgateway.DeleteNetworkGateway, + 'net-gateway-show': nvpnetworkgateway.ShowNetworkGateway, + 'net-gateway-list': nvpnetworkgateway.ListNetworkGateway, + 'net-gateway-connect': nvpnetworkgateway.ConnectNetworkGateway, + 'net-gateway-disconnect': nvpnetworkgateway.DisconnectNetworkGateway, + 'dhcp-agent-network-add': agentscheduler.AddNetworkToDhcpAgent, + 'dhcp-agent-network-remove': agentscheduler.RemoveNetworkFromDhcpAgent, + 'net-list-on-dhcp-agent': agentscheduler.ListNetworksOnDhcpAgent, + 'dhcp-agent-list-hosting-net': agentscheduler.ListDhcpAgentsHostingNetwork, + 'l3-agent-router-add': agentscheduler.AddRouterToL3Agent, + 'l3-agent-router-remove': agentscheduler.RemoveRouterFromL3Agent, + 'router-list-on-l3-agent': agentscheduler.ListRoutersOnL3Agent, + 'l3-agent-list-hosting-router': agentscheduler.ListL3AgentsHostingRouter, +} + +COMMANDS = {'2.0': COMMAND_V2} + + +class HelpAction(argparse.Action): + """Provide a custom action so the -h and --help options + to the main app will print a list of the commands. + + The commands are determined by checking the CommandManager + instance, passed in as the "default" value for the action. + """ + def __call__(self, parser, namespace, values, option_string=None): + outputs = [] + max_len = 0 + app = self.default + parser.print_help(app.stdout) + app.stdout.write('\nCommands for API v%s:\n' % app.api_version) + command_manager = app.command_manager + for name, ep in sorted(command_manager): + factory = ep.load() + cmd = factory(self, None) + one_liner = cmd.get_description().split('\n')[0] + outputs.append((name, one_liner)) + max_len = max(len(name), max_len) + for (name, one_liner) in outputs: + app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner)) + sys.exit(0) + + +class NeutronShell(app.App): + + CONSOLE_MESSAGE_FORMAT = '%(message)s' + DEBUG_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' + log = logging.getLogger(__name__) + + def __init__(self, apiversion): + super(NeutronShell, self).__init__( + description=__doc__.strip(), + version=VERSION, + command_manager=commandmanager.CommandManager('neutron.cli'), ) + self.commands = COMMANDS + for k, v in self.commands[apiversion].items(): + self.command_manager.add_command(k, v) + + # This is instantiated in initialize_app() only when using + # password flow auth + self.auth_client = None + self.api_version = apiversion + + def build_option_parser(self, description, version): + """Return an argparse option parser for this application. + + Subclasses may override this method to extend + the parser with more global options. + + :param description: full description of the application + :paramtype description: str + :param version: version number for the application + :paramtype version: str + """ + parser = argparse.ArgumentParser( + description=description, + add_help=False, ) + parser.add_argument( + '--version', + action='version', + version=__version__, ) + parser.add_argument( + '-v', '--verbose', + action='count', + dest='verbose_level', + default=self.DEFAULT_VERBOSE_LEVEL, + help='Increase verbosity of output. Can be repeated.', ) + parser.add_argument( + '-q', '--quiet', + action='store_const', + dest='verbose_level', + const=0, + help='suppress output except warnings and errors', ) + parser.add_argument( + '-h', '--help', + action=HelpAction, + nargs=0, + default=self, # tricky + help="show this help message and exit", ) + parser.add_argument( + '--debug', + default=False, + action='store_true', + help='show tracebacks on errors', ) + # Global arguments + parser.add_argument( + '--os-auth-strategy', metavar='', + default=env('OS_AUTH_STRATEGY', default='keystone'), + help='Authentication strategy (Env: OS_AUTH_STRATEGY' + ', default keystone). For now, any other value will' + ' disable the authentication') + parser.add_argument( + '--os_auth_strategy', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-auth-url', metavar='', + default=env('OS_AUTH_URL'), + help='Authentication URL (Env: OS_AUTH_URL)') + parser.add_argument( + '--os_auth_url', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-tenant-name', metavar='', + default=env('OS_TENANT_NAME'), + help='Authentication tenant name (Env: OS_TENANT_NAME)') + parser.add_argument( + '--os_tenant_name', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-tenant-id', metavar='', + default=env('OS_TENANT_ID'), + help='Authentication tenant name (Env: OS_TENANT_ID)') + + parser.add_argument( + '--os-username', metavar='', + default=utils.env('OS_USERNAME'), + help='Authentication username (Env: OS_USERNAME)') + parser.add_argument( + '--os_username', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-password', metavar='', + default=utils.env('OS_PASSWORD'), + help='Authentication password (Env: OS_PASSWORD)') + parser.add_argument( + '--os_password', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-region-name', metavar='', + default=env('OS_REGION_NAME'), + help='Authentication region name (Env: OS_REGION_NAME)') + parser.add_argument( + '--os_region_name', + help=argparse.SUPPRESS) + + parser.add_argument( + '--os-token', metavar='', + default=env('OS_TOKEN'), + help='Defaults to env[OS_TOKEN]') + parser.add_argument( + '--os_token', + help=argparse.SUPPRESS) + + parser.add_argument( + '--endpoint-type', metavar='', + default=env('OS_ENDPOINT_TYPE', default='publicURL'), + help='Defaults to env[OS_ENDPOINT_TYPE] or publicURL.') + + parser.add_argument( + '--os-url', metavar='', + default=env('OS_URL'), + help='Defaults to env[OS_URL]') + parser.add_argument( + '--os_url', + help=argparse.SUPPRESS) + + parser.add_argument( + '--insecure', + action='store_true', + default=env('NEUTRONCLIENT_INSECURE', default=False), + help="Explicitly allow neutronclient to perform \"insecure\" " + "SSL (https) requests. The server's certificate will " + "not be verified against any certificate authorities. " + "This option should be used with caution.") + + return parser + + def _bash_completion(self): + """Prints all of the commands and options for bash-completion.""" + commands = set() + options = set() + for option, _action in self.parser._option_string_actions.items(): + options.add(option) + for command_name, command in self.command_manager: + commands.add(command_name) + cmd_factory = command.load() + cmd = cmd_factory(self, None) + cmd_parser = cmd.get_parser('') + for option, _action in cmd_parser._option_string_actions.items(): + options.add(option) + print ' '.join(commands | options) + + def run(self, argv): + """Equivalent to the main program for the application. + + :param argv: input arguments and options + :paramtype argv: list of str + """ + try: + index = 0 + command_pos = -1 + help_pos = -1 + help_command_pos = -1 + for arg in argv: + if arg == 'bash-completion': + self._bash_completion() + return 0 + if arg in self.commands[self.api_version]: + if command_pos == -1: + command_pos = index + elif arg in ('-h', '--help'): + if help_pos == -1: + help_pos = index + elif arg == 'help': + if help_command_pos == -1: + help_command_pos = index + index = index + 1 + if command_pos > -1 and help_pos > command_pos: + argv = ['help', argv[command_pos]] + if help_command_pos > -1 and command_pos == -1: + argv[help_command_pos] = '--help' + self.options, remainder = self.parser.parse_known_args(argv) + self.configure_logging() + self.interactive_mode = not remainder + self.initialize_app(remainder) + except Exception as err: + if self.options.debug: + self.log.exception(unicode(err)) + raise + else: + self.log.error(unicode(err)) + return 1 + result = 1 + if self.interactive_mode: + _argv = [sys.argv[0]] + sys.argv = _argv + result = self.interact() + else: + result = self.run_subcommand(remainder) + return result + + def run_subcommand(self, argv): + subcommand = self.command_manager.find_command(argv) + cmd_factory, cmd_name, sub_argv = subcommand + cmd = cmd_factory(self, self.options) + err = None + result = 1 + try: + self.prepare_to_run_command(cmd) + full_name = (cmd_name + if self.interactive_mode + else ' '.join([self.NAME, cmd_name]) + ) + cmd_parser = cmd.get_parser(full_name) + return run_command(cmd, cmd_parser, sub_argv) + except Exception as err: + if self.options.debug: + self.log.exception(unicode(err)) + else: + self.log.error(unicode(err)) + try: + self.clean_up(cmd, result, err) + except Exception as err2: + if self.options.debug: + self.log.exception(unicode(err2)) + else: + self.log.error('Could not clean up: %s', unicode(err2)) + if self.options.debug: + raise + else: + try: + self.clean_up(cmd, result, None) + except Exception as err3: + if self.options.debug: + self.log.exception(unicode(err3)) + else: + self.log.error('Could not clean up: %s', unicode(err3)) + return result + + def authenticate_user(self): + """Make sure the user has provided all of the authentication + info we need. + """ + if self.options.os_auth_strategy == 'keystone': + if self.options.os_token or self.options.os_url: + # Token flow auth takes priority + if not self.options.os_token: + raise exc.CommandError( + "You must provide a token via" + " either --os-token or env[OS_TOKEN]") + + if not self.options.os_url: + raise exc.CommandError( + "You must provide a service URL via" + " either --os-url or env[OS_URL]") + + else: + # Validate password flow auth + if not self.options.os_username: + raise exc.CommandError( + "You must provide a username via" + " either --os-username or env[OS_USERNAME]") + + if not self.options.os_password: + raise exc.CommandError( + "You must provide a password via" + " either --os-password or env[OS_PASSWORD]") + + if (not self.options.os_tenant_name + and not self.options.os_tenant_id): + raise exc.CommandError( + "You must provide a tenant_name or tenant_id via" + " --os-tenant-name, env[OS_TENANT_NAME]" + " --os-tenant-id, or via env[OS_TENANT_ID]") + + if not self.options.os_auth_url: + raise exc.CommandError( + "You must provide an auth url via" + " either --os-auth-url or via env[OS_AUTH_URL]") + else: # not keystone + if not self.options.os_url: + raise exc.CommandError( + "You must provide a service URL via" + " either --os-url or env[OS_URL]") + + self.client_manager = clientmanager.ClientManager( + token=self.options.os_token, + url=self.options.os_url, + auth_url=self.options.os_auth_url, + tenant_name=self.options.os_tenant_name, + tenant_id=self.options.os_tenant_id, + username=self.options.os_username, + password=self.options.os_password, + region_name=self.options.os_region_name, + api_version=self.api_version, + auth_strategy=self.options.os_auth_strategy, + endpoint_type=self.options.endpoint_type, + insecure=self.options.insecure, ) + return + + def initialize_app(self, argv): + """Global app init bits: + + * set up API versions + * validate authentication info + """ + + super(NeutronShell, self).initialize_app(argv) + + self.api_version = {'network': self.api_version} + + # If the user is not asking for help, make sure they + # have given us auth. + cmd_name = None + if argv: + cmd_info = self.command_manager.find_command(argv) + cmd_factory, cmd_name, sub_argv = cmd_info + if self.interactive_mode or cmd_name != 'help': + self.authenticate_user() + + def clean_up(self, cmd, result, err): + self.log.debug('clean_up %s', cmd.__class__.__name__) + if err: + self.log.debug('got an error: %s', unicode(err)) + + def configure_logging(self): + """Create logging handlers for any log output. + """ + root_logger = logging.getLogger('') + + # Set up logging to a file + root_logger.setLevel(logging.DEBUG) + + # Send higher-level messages to the console via stderr + console = logging.StreamHandler(self.stderr) + console_level = {0: logging.WARNING, + 1: logging.INFO, + 2: logging.DEBUG, + }.get(self.options.verbose_level, logging.DEBUG) + console.setLevel(console_level) + if logging.DEBUG == console_level: + formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT) + else: + formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT) + console.setFormatter(formatter) + root_logger.addHandler(console) + return + + +def main(argv=sys.argv[1:]): + try: + return NeutronShell(NEUTRON_API_VERSION).run(map(strutils.safe_decode, + argv)) + except exc.NeutronClientException: + return 1 + except Exception as e: + print unicode(e) + return 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/neutronclient/tests/unit/test_utils.py b/neutronclient/tests/unit/test_utils.py new file mode 100644 index 000000000..22c4ba45d --- /dev/null +++ b/neutronclient/tests/unit/test_utils.py @@ -0,0 +1,45 @@ +# Copyright 2013 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import testtools + +from neutronclient.common import utils + + +class UtilsTest(testtools.TestCase): + def test_safe_encode_list(self): + o = object() + unicode_text = u'\u7f51\u7edc' + l = ['abc', unicode_text, unicode_text.encode('utf-8'), o] + expected = ['abc', unicode_text.encode('utf-8'), + unicode_text.encode('utf-8'), o] + self.assertEqual(utils.safe_encode_list(l), expected) + + def test_safe_encode_dict(self): + o = object() + unicode_text = u'\u7f51\u7edc' + d = {'test1': unicode_text, + 'test2': [unicode_text, o], + 'test3': o, + 'test4': {'test5': unicode_text}, + 'test6': unicode_text.encode('utf-8')} + expected = {'test1': unicode_text.encode('utf-8'), + 'test2': [unicode_text.encode('utf-8'), o], + 'test3': o, + 'test4': {'test5': unicode_text.encode('utf-8')}, + 'test6': unicode_text.encode('utf-8')} + self.assertEqual(utils.safe_encode_dict(d), expected) diff --git a/neutronclient/v2_0/__init__.py b/neutronclient/v2_0/__init__.py new file mode 100644 index 000000000..63c3905e2 --- /dev/null +++ b/neutronclient/v2_0/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2012 OpenStack LLC. +# 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. diff --git a/neutronclient/v2_0/client.py b/neutronclient/v2_0/client.py new file mode 100644 index 000000000..e2e1c792c --- /dev/null +++ b/neutronclient/v2_0/client.py @@ -0,0 +1,881 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import httplib +import logging +import time +import urllib +import urlparse + +from neutronclient import client +from neutronclient.common import _ +from neutronclient.common import constants +from neutronclient.common import exceptions +from neutronclient.common import serializer +from neutronclient.common import utils + + +_logger = logging.getLogger(__name__) + + +def exception_handler_v20(status_code, error_content): + """Exception handler for API v2.0 client + + This routine generates the appropriate + Neutron exception according to the contents of the + response body + + :param status_code: HTTP error status code + :param error_content: deserialized body of error response + """ + + neutron_errors = { + 'NetworkNotFound': exceptions.NetworkNotFoundClient, + 'NetworkInUse': exceptions.NetworkInUseClient, + 'PortNotFound': exceptions.PortNotFoundClient, + 'RequestedStateInvalid': exceptions.StateInvalidClient, + 'PortInUse': exceptions.PortInUseClient, + 'AlreadyAttached': exceptions.AlreadyAttachedClient, } + + error_dict = None + if isinstance(error_content, dict): + error_dict = error_content.get('NeutronError') + # Find real error type + bad_neutron_error_flag = False + if error_dict: + # If Neutron key is found, it will definitely contain + # a 'message' and 'type' keys? + try: + error_type = error_dict['type'] + error_message = (error_dict['message'] + "\n" + + error_dict['detail']) + except Exception: + bad_neutron_error_flag = True + if not bad_neutron_error_flag: + ex = None + try: + # raise the appropriate error! + ex = neutron_errors[error_type](message=error_message) + ex.args = ([dict(status_code=status_code, + message=error_message)], ) + except Exception: + pass + if ex: + raise ex + else: + raise exceptions.NeutronClientException(status_code=status_code, + message=error_dict) + else: + message = None + if isinstance(error_content, dict): + message = error_content.get('message', None) + if message: + raise exceptions.NeutronClientException(status_code=status_code, + message=message) + + # If we end up here the exception was not a neutron error + msg = "%s-%s" % (status_code, error_content) + raise exceptions.NeutronClientException(status_code=status_code, + message=msg) + + +class APIParamsCall(object): + """A Decorator to add support for format and tenant overriding + and filters + """ + def __init__(self, function): + self.function = function + + def __get__(self, instance, owner): + def with_params(*args, **kwargs): + _format = instance.format + if 'format' in kwargs: + instance.format = kwargs['format'] + ret = self.function(instance, *args, **kwargs) + instance.format = _format + return ret + return with_params + + +class Client(object): + """Client for the OpenStack Neutron v2.0 API. + + :param string username: Username for authentication. (optional) + :param string password: Password for authentication. (optional) + :param string token: Token for authentication. (optional) + :param string tenant_name: Tenant name. (optional) + :param string tenant_id: Tenant id. (optional) + :param string auth_url: Keystone service endpoint for authorization. + :param string endpoint_type: Network service endpoint type to pull from the + keystone catalog (e.g. 'publicURL', + 'internalURL', or 'adminURL') (optional) + :param string region_name: Name of a region to select when choosing an + endpoint from the service catalog. + :param string endpoint_url: A user-supplied endpoint URL for the neutron + service. Lazy-authentication is possible for API + service calls if endpoint is set at + instantiation.(optional) + :param integer timeout: Allows customization of the timeout for client + http requests. (optional) + :param insecure: ssl certificate validation. (optional) + + Example:: + + from neutronclient.v2_0 import client + neutron = client.Client(username=USER, + password=PASS, + tenant_name=TENANT_NAME, + auth_url=KEYSTONE_URL) + + nets = neutron.list_networks() + ... + + """ + + networks_path = "/networks" + network_path = "/networks/%s" + ports_path = "/ports" + port_path = "/ports/%s" + subnets_path = "/subnets" + subnet_path = "/subnets/%s" + quotas_path = "/quotas" + quota_path = "/quotas/%s" + extensions_path = "/extensions" + extension_path = "/extensions/%s" + routers_path = "/routers" + router_path = "/routers/%s" + floatingips_path = "/floatingips" + floatingip_path = "/floatingips/%s" + security_groups_path = "/security-groups" + security_group_path = "/security-groups/%s" + security_group_rules_path = "/security-group-rules" + security_group_rule_path = "/security-group-rules/%s" + vips_path = "/lb/vips" + vip_path = "/lb/vips/%s" + pools_path = "/lb/pools" + pool_path = "/lb/pools/%s" + pool_path_stats = "/lb/pools/%s/stats" + members_path = "/lb/members" + member_path = "/lb/members/%s" + health_monitors_path = "/lb/health_monitors" + health_monitor_path = "/lb/health_monitors/%s" + associate_pool_health_monitors_path = "/lb/pools/%s/health_monitors" + disassociate_pool_health_monitors_path = ( + "/lb/pools/%(pool)s/health_monitors/%(health_monitor)s") + qos_queues_path = "/qos-queues" + qos_queue_path = "/qos-queues/%s" + agents_path = "/agents" + agent_path = "/agents/%s" + network_gateways_path = "/network-gateways" + network_gateway_path = "/network-gateways/%s" + + DHCP_NETS = '/dhcp-networks' + DHCP_AGENTS = '/dhcp-agents' + L3_ROUTERS = '/l3-routers' + L3_AGENTS = '/l3-agents' + # API has no way to report plurals, so we have to hard code them + EXTED_PLURALS = {'routers': 'router', + 'floatingips': 'floatingip', + 'service_types': 'service_type', + 'service_definitions': 'service_definition', + 'security_groups': 'security_group', + 'security_group_rules': 'security_group_rule', + 'vips': 'vip', + 'pools': 'pool', + 'members': 'member', + 'health_monitors': 'health_monitor', + 'quotas': 'quota', + } + # 8192 Is the default max URI len for eventlet.wsgi.server + MAX_URI_LEN = 8192 + + def get_attr_metadata(self): + if self.format == 'json': + return {} + old_request_format = self.format + self.format = 'json' + exts = self.list_extensions()['extensions'] + self.format = old_request_format + ns = dict([(ext['alias'], ext['namespace']) for ext in exts]) + self.EXTED_PLURALS.update(constants.PLURALS) + return {'plurals': self.EXTED_PLURALS, + 'xmlns': constants.XML_NS_V20, + constants.EXT_NS: ns} + + @APIParamsCall + def get_quotas_tenant(self, **_params): + """Fetch tenant info in server's context for + following quota operation. + """ + return self.get(self.quota_path % 'tenant', params=_params) + + @APIParamsCall + def list_quotas(self, **_params): + """Fetch all tenants' quotas.""" + return self.get(self.quotas_path, params=_params) + + @APIParamsCall + def show_quota(self, tenant_id, **_params): + """Fetch information of a certain tenant's quotas.""" + return self.get(self.quota_path % (tenant_id), params=_params) + + @APIParamsCall + def update_quota(self, tenant_id, body=None): + """Update a tenant's quotas.""" + return self.put(self.quota_path % (tenant_id), body=body) + + @APIParamsCall + def delete_quota(self, tenant_id): + """Delete the specified tenant's quota values.""" + return self.delete(self.quota_path % (tenant_id)) + + @APIParamsCall + def list_extensions(self, **_params): + """Fetch a list of all exts on server side.""" + return self.get(self.extensions_path, params=_params) + + @APIParamsCall + def show_extension(self, ext_alias, **_params): + """Fetch a list of all exts on server side.""" + return self.get(self.extension_path % ext_alias, params=_params) + + @APIParamsCall + def list_ports(self, retrieve_all=True, **_params): + """Fetches a list of all networks for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('ports', self.ports_path, retrieve_all, + **_params) + + @APIParamsCall + def show_port(self, port, **_params): + """Fetches information of a certain network.""" + return self.get(self.port_path % (port), params=_params) + + @APIParamsCall + def create_port(self, body=None): + """Creates a new port.""" + return self.post(self.ports_path, body=body) + + @APIParamsCall + def update_port(self, port, body=None): + """Updates a port.""" + return self.put(self.port_path % (port), body=body) + + @APIParamsCall + def delete_port(self, port): + """Deletes the specified port.""" + return self.delete(self.port_path % (port)) + + @APIParamsCall + def list_networks(self, retrieve_all=True, **_params): + """Fetches a list of all networks for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('networks', self.networks_path, retrieve_all, + **_params) + + @APIParamsCall + def show_network(self, network, **_params): + """Fetches information of a certain network.""" + return self.get(self.network_path % (network), params=_params) + + @APIParamsCall + def create_network(self, body=None): + """Creates a new network.""" + return self.post(self.networks_path, body=body) + + @APIParamsCall + def update_network(self, network, body=None): + """Updates a network.""" + return self.put(self.network_path % (network), body=body) + + @APIParamsCall + def delete_network(self, network): + """Deletes the specified network.""" + return self.delete(self.network_path % (network)) + + @APIParamsCall + def list_subnets(self, retrieve_all=True, **_params): + """Fetches a list of all networks for a tenant.""" + return self.list('subnets', self.subnets_path, retrieve_all, + **_params) + + @APIParamsCall + def show_subnet(self, subnet, **_params): + """Fetches information of a certain subnet.""" + return self.get(self.subnet_path % (subnet), params=_params) + + @APIParamsCall + def create_subnet(self, body=None): + """Creates a new subnet.""" + return self.post(self.subnets_path, body=body) + + @APIParamsCall + def update_subnet(self, subnet, body=None): + """Updates a subnet.""" + return self.put(self.subnet_path % (subnet), body=body) + + @APIParamsCall + def delete_subnet(self, subnet): + """Deletes the specified subnet.""" + return self.delete(self.subnet_path % (subnet)) + + @APIParamsCall + def list_routers(self, retrieve_all=True, **_params): + """Fetches a list of all routers for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('routers', self.routers_path, retrieve_all, + **_params) + + @APIParamsCall + def show_router(self, router, **_params): + """Fetches information of a certain router.""" + return self.get(self.router_path % (router), params=_params) + + @APIParamsCall + def create_router(self, body=None): + """Creates a new router.""" + return self.post(self.routers_path, body=body) + + @APIParamsCall + def update_router(self, router, body=None): + """Updates a router.""" + return self.put(self.router_path % (router), body=body) + + @APIParamsCall + def delete_router(self, router): + """Deletes the specified router.""" + return self.delete(self.router_path % (router)) + + @APIParamsCall + def add_interface_router(self, router, body=None): + """Adds an internal network interface to the specified router.""" + return self.put((self.router_path % router) + "/add_router_interface", + body=body) + + @APIParamsCall + def remove_interface_router(self, router, body=None): + """Removes an internal network interface from the specified router.""" + return self.put((self.router_path % router) + + "/remove_router_interface", body=body) + + @APIParamsCall + def add_gateway_router(self, router, body=None): + """Adds an external network gateway to the specified router.""" + return self.put((self.router_path % router), + body={'router': {'external_gateway_info': body}}) + + @APIParamsCall + def remove_gateway_router(self, router): + """Removes an external network gateway from the specified router.""" + return self.put((self.router_path % router), + body={'router': {'external_gateway_info': {}}}) + + @APIParamsCall + def list_floatingips(self, retrieve_all=True, **_params): + """Fetches a list of all floatingips for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('floatingips', self.floatingips_path, retrieve_all, + **_params) + + @APIParamsCall + def show_floatingip(self, floatingip, **_params): + """Fetches information of a certain floatingip.""" + return self.get(self.floatingip_path % (floatingip), params=_params) + + @APIParamsCall + def create_floatingip(self, body=None): + """Creates a new floatingip.""" + return self.post(self.floatingips_path, body=body) + + @APIParamsCall + def update_floatingip(self, floatingip, body=None): + """Updates a floatingip.""" + return self.put(self.floatingip_path % (floatingip), body=body) + + @APIParamsCall + def delete_floatingip(self, floatingip): + """Deletes the specified floatingip.""" + return self.delete(self.floatingip_path % (floatingip)) + + @APIParamsCall + def create_security_group(self, body=None): + """Creates a new security group.""" + return self.post(self.security_groups_path, body=body) + + @APIParamsCall + def update_security_group(self, security_group, body=None): + """Updates a security group.""" + return self.put(self.security_group_path % + security_group, body=body) + + @APIParamsCall + def list_security_groups(self, retrieve_all=True, **_params): + """Fetches a list of all security groups for a tenant.""" + return self.list('security_groups', self.security_groups_path, + retrieve_all, **_params) + + @APIParamsCall + def show_security_group(self, security_group, **_params): + """Fetches information of a certain security group.""" + return self.get(self.security_group_path % (security_group), + params=_params) + + @APIParamsCall + def delete_security_group(self, security_group): + """Deletes the specified security group.""" + return self.delete(self.security_group_path % (security_group)) + + @APIParamsCall + def create_security_group_rule(self, body=None): + """Creates a new security group rule.""" + return self.post(self.security_group_rules_path, body=body) + + @APIParamsCall + def delete_security_group_rule(self, security_group_rule): + """Deletes the specified security group rule.""" + return self.delete(self.security_group_rule_path % + (security_group_rule)) + + @APIParamsCall + def list_security_group_rules(self, retrieve_all=True, **_params): + """Fetches a list of all security group rules for a tenant.""" + return self.list('security_group_rules', + self.security_group_rules_path, + retrieve_all, **_params) + + @APIParamsCall + def show_security_group_rule(self, security_group_rule, **_params): + """Fetches information of a certain security group rule.""" + return self.get(self.security_group_rule_path % (security_group_rule), + params=_params) + + @APIParamsCall + def list_vips(self, retrieve_all=True, **_params): + """Fetches a list of all load balancer vips for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('vips', self.vips_path, retrieve_all, + **_params) + + @APIParamsCall + def show_vip(self, vip, **_params): + """Fetches information of a certain load balancer vip.""" + return self.get(self.vip_path % (vip), params=_params) + + @APIParamsCall + def create_vip(self, body=None): + """Creates a new load balancer vip.""" + return self.post(self.vips_path, body=body) + + @APIParamsCall + def update_vip(self, vip, body=None): + """Updates a load balancer vip.""" + return self.put(self.vip_path % (vip), body=body) + + @APIParamsCall + def delete_vip(self, vip): + """Deletes the specified load balancer vip.""" + return self.delete(self.vip_path % (vip)) + + @APIParamsCall + def list_pools(self, retrieve_all=True, **_params): + """Fetches a list of all load balancer pools for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('pools', self.pools_path, retrieve_all, + **_params) + + @APIParamsCall + def show_pool(self, pool, **_params): + """Fetches information of a certain load balancer pool.""" + return self.get(self.pool_path % (pool), params=_params) + + @APIParamsCall + def create_pool(self, body=None): + """Creates a new load balancer pool.""" + return self.post(self.pools_path, body=body) + + @APIParamsCall + def update_pool(self, pool, body=None): + """Updates a load balancer pool.""" + return self.put(self.pool_path % (pool), body=body) + + @APIParamsCall + def delete_pool(self, pool): + """Deletes the specified load balancer pool.""" + return self.delete(self.pool_path % (pool)) + + @APIParamsCall + def retrieve_pool_stats(self, pool, **_params): + """Retrieves stats for a certain load balancer pool.""" + return self.get(self.pool_path_stats % (pool), params=_params) + + @APIParamsCall + def list_members(self, retrieve_all=True, **_params): + """Fetches a list of all load balancer members for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('members', self.members_path, retrieve_all, + **_params) + + @APIParamsCall + def show_member(self, member, **_params): + """Fetches information of a certain load balancer member.""" + return self.get(self.member_path % (member), params=_params) + + @APIParamsCall + def create_member(self, body=None): + """Creates a new load balancer member.""" + return self.post(self.members_path, body=body) + + @APIParamsCall + def update_member(self, member, body=None): + """Updates a load balancer member.""" + return self.put(self.member_path % (member), body=body) + + @APIParamsCall + def delete_member(self, member): + """Deletes the specified load balancer member.""" + return self.delete(self.member_path % (member)) + + @APIParamsCall + def list_health_monitors(self, retrieve_all=True, **_params): + """Fetches a list of all load balancer health monitors for a tenant.""" + # Pass filters in "params" argument to do_request + return self.list('health_monitors', self.health_monitors_path, + retrieve_all, **_params) + + @APIParamsCall + def show_health_monitor(self, health_monitor, **_params): + """Fetches information of a certain load balancer health monitor.""" + return self.get(self.health_monitor_path % (health_monitor), + params=_params) + + @APIParamsCall + def create_health_monitor(self, body=None): + """Creates a new load balancer health monitor.""" + return self.post(self.health_monitors_path, body=body) + + @APIParamsCall + def update_health_monitor(self, health_monitor, body=None): + """Updates a load balancer health monitor.""" + return self.put(self.health_monitor_path % (health_monitor), body=body) + + @APIParamsCall + def delete_health_monitor(self, health_monitor): + """Deletes the specified load balancer health monitor.""" + return self.delete(self.health_monitor_path % (health_monitor)) + + @APIParamsCall + def associate_health_monitor(self, pool, body): + """Associate specified load balancer health monitor and pool.""" + return self.post(self.associate_pool_health_monitors_path % (pool), + body=body) + + @APIParamsCall + def disassociate_health_monitor(self, pool, health_monitor): + """Disassociate specified load balancer health monitor and pool.""" + path = (self.disassociate_pool_health_monitors_path % + {'pool': pool, 'health_monitor': health_monitor}) + return self.delete(path) + + @APIParamsCall + def create_qos_queue(self, body=None): + """Creates a new queue.""" + return self.post(self.qos_queues_path, body=body) + + @APIParamsCall + def list_qos_queues(self, **_params): + """Fetches a list of all queues for a tenant.""" + return self.get(self.qos_queues_path, params=_params) + + @APIParamsCall + def show_qos_queue(self, queue, **_params): + """Fetches information of a certain queue.""" + return self.get(self.qos_queue_path % (queue), + params=_params) + + @APIParamsCall + def delete_qos_queue(self, queue): + """Deletes the specified queue.""" + return self.delete(self.qos_queue_path % (queue)) + + @APIParamsCall + def list_agents(self, **_params): + """Fetches agents.""" + # Pass filters in "params" argument to do_request + return self.get(self.agents_path, params=_params) + + @APIParamsCall + def show_agent(self, agent, **_params): + """Fetches information of a certain agent.""" + return self.get(self.agent_path % (agent), params=_params) + + @APIParamsCall + def update_agent(self, agent, body=None): + """Updates an agent.""" + return self.put(self.agent_path % (agent), body=body) + + @APIParamsCall + def delete_agent(self, agent): + """Deletes the specified agent.""" + return self.delete(self.agent_path % (agent)) + + @APIParamsCall + def list_network_gateways(self, **_params): + """Retrieve network gateways.""" + return self.get(self.network_gateways_path, params=_params) + + @APIParamsCall + def show_network_gateway(self, gateway_id, **_params): + """Fetch a network gateway.""" + return self.get(self.network_gateway_path % gateway_id, params=_params) + + @APIParamsCall + def create_network_gateway(self, body=None): + """Create a new network gateway.""" + return self.post(self.network_gateways_path, body=body) + + @APIParamsCall + def update_network_gateway(self, gateway_id, body=None): + """Update a network gateway.""" + return self.put(self.network_gateway_path % gateway_id, body=body) + + @APIParamsCall + def delete_network_gateway(self, gateway_id): + """Delete the specified network gateway.""" + return self.delete(self.network_gateway_path % gateway_id) + + @APIParamsCall + def connect_network_gateway(self, gateway_id, body=None): + """Connect a network gateway to the specified network.""" + base_uri = self.network_gateway_path % gateway_id + return self.put("%s/connect_network" % base_uri, body=body) + + @APIParamsCall + def disconnect_network_gateway(self, gateway_id, body=None): + """Disconnect a network from the specified gateway.""" + base_uri = self.network_gateway_path % gateway_id + return self.put("%s/disconnect_network" % base_uri, body=body) + + @APIParamsCall + def list_dhcp_agent_hosting_networks(self, network, **_params): + """Fetches a list of dhcp agents hosting a network.""" + return self.get((self.network_path + self.DHCP_AGENTS) % network, + params=_params) + + @APIParamsCall + def list_networks_on_dhcp_agent(self, dhcp_agent, **_params): + """Fetches a list of dhcp agents hosting a network.""" + return self.get((self.agent_path + self.DHCP_NETS) % dhcp_agent, + params=_params) + + @APIParamsCall + def add_network_to_dhcp_agent(self, dhcp_agent, body=None): + """Adds a network to dhcp agent.""" + return self.post((self.agent_path + self.DHCP_NETS) % dhcp_agent, + body=body) + + @APIParamsCall + def remove_network_from_dhcp_agent(self, dhcp_agent, network_id): + """Remove a network from dhcp agent.""" + return self.delete((self.agent_path + self.DHCP_NETS + "/%s") % ( + dhcp_agent, network_id)) + + @APIParamsCall + def list_l3_agent_hosting_routers(self, router, **_params): + """Fetches a list of L3 agents hosting a router.""" + return self.get((self.router_path + self.L3_AGENTS) % router, + params=_params) + + @APIParamsCall + def list_routers_on_l3_agent(self, l3_agent, **_params): + """Fetches a list of L3 agents hosting a router.""" + return self.get((self.agent_path + self.L3_ROUTERS) % l3_agent, + params=_params) + + @APIParamsCall + def add_router_to_l3_agent(self, l3_agent, body): + """Adds a router to L3 agent.""" + return self.post((self.agent_path + self.L3_ROUTERS) % l3_agent, + body=body) + + @APIParamsCall + def remove_router_from_l3_agent(self, l3_agent, router_id): + """Remove a router from l3 agent.""" + return self.delete((self.agent_path + self.L3_ROUTERS + "/%s") % ( + l3_agent, router_id)) + + def __init__(self, **kwargs): + """Initialize a new client for the Neutron v2.0 API.""" + super(Client, self).__init__() + self.httpclient = client.HTTPClient(**kwargs) + self.version = '2.0' + self.format = 'json' + self.action_prefix = "/v%s" % (self.version) + self.retries = 0 + self.retry_interval = 1 + + def _handle_fault_response(self, status_code, response_body): + # Create exception with HTTP status code and message + _logger.debug("Error message: %s", response_body) + # Add deserialized error message to exception arguments + try: + des_error_body = self.deserialize(response_body, status_code) + except Exception: + # If unable to deserialized body it is probably not a + # Neutron error + des_error_body = {'message': response_body} + # Raise the appropriate exception + exception_handler_v20(status_code, des_error_body) + + def _check_uri_length(self, action): + uri_len = len(self.httpclient.endpoint_url) + len(action) + if uri_len > self.MAX_URI_LEN: + raise exceptions.RequestURITooLong( + excess=uri_len - self.MAX_URI_LEN) + + def do_request(self, method, action, body=None, headers=None, params=None): + # Add format and tenant_id + action += ".%s" % self.format + action = self.action_prefix + action + if type(params) is dict and params: + params = utils.safe_encode_dict(params) + action += '?' + urllib.urlencode(params, doseq=1) + # Ensure client always has correct uri - do not guesstimate anything + self.httpclient.authenticate_and_fetch_endpoint_url() + self._check_uri_length(action) + + if body: + body = self.serialize(body) + self.httpclient.content_type = self.content_type() + resp, replybody = self.httpclient.do_request(action, method, body=body) + status_code = self.get_status_code(resp) + if status_code in (httplib.OK, + httplib.CREATED, + httplib.ACCEPTED, + httplib.NO_CONTENT): + return self.deserialize(replybody, status_code) + else: + self._handle_fault_response(status_code, replybody) + + def get_auth_info(self): + return self.httpclient.get_auth_info() + + def get_status_code(self, response): + """Returns the integer status code from the response. + + Either a Webob.Response (used in testing) or httplib.Response + is returned. + """ + if hasattr(response, 'status_int'): + return response.status_int + else: + return response.status + + def serialize(self, data): + """Serializes a dictionary into either xml or json. + + A dictionary with a single key can be passed and + it can contain any structure. + """ + if data is None: + return None + elif type(data) is dict: + return serializer.Serializer( + self.get_attr_metadata()).serialize(data, self.content_type()) + else: + raise Exception("unable to serialize object of type = '%s'" % + type(data)) + + def deserialize(self, data, status_code): + """Deserializes an xml or json string into a dictionary.""" + if status_code == 204: + return data + return serializer.Serializer(self.get_attr_metadata()).deserialize( + data, self.content_type())['body'] + + def content_type(self, _format=None): + """Returns the mime-type for either 'xml' or 'json'. + + Defaults to the currently set format. + """ + _format = _format or self.format + return "application/%s" % (_format) + + def retry_request(self, method, action, body=None, + headers=None, params=None): + """Call do_request with the default retry configuration. + + Only idempotent requests should retry failed connection attempts. + :raises: ConnectionFailed if the maximum # of retries is exceeded + """ + max_attempts = self.retries + 1 + for i in xrange(max_attempts): + try: + return self.do_request(method, action, body=body, + headers=headers, params=params) + except exceptions.ConnectionFailed: + # Exception has already been logged by do_request() + if i < self.retries: + _logger.debug(_('Retrying connection to Neutron service')) + time.sleep(self.retry_interval) + + raise exceptions.ConnectionFailed(reason=_("Maximum attempts reached")) + + def delete(self, action, body=None, headers=None, params=None): + return self.retry_request("DELETE", action, body=body, + headers=headers, params=params) + + def get(self, action, body=None, headers=None, params=None): + return self.retry_request("GET", action, body=body, + headers=headers, params=params) + + def post(self, action, body=None, headers=None, params=None): + # Do not retry POST requests to avoid the orphan objects problem. + return self.do_request("POST", action, body=body, + headers=headers, params=params) + + def put(self, action, body=None, headers=None, params=None): + return self.retry_request("PUT", action, body=body, + headers=headers, params=params) + + def list(self, collection, path, retrieve_all=True, **params): + if retrieve_all: + res = [] + for r in self._pagination(collection, path, **params): + res.extend(r[collection]) + return {collection: res} + else: + return self._pagination(collection, path, **params) + + def _pagination(self, collection, path, **params): + if params.get('page_reverse', False): + linkrel = 'previous' + else: + linkrel = 'next' + next = True + while next: + res = self.get(path, params=params) + yield res + next = False + try: + for link in res['%s_links' % collection]: + if link['rel'] == linkrel: + query_str = urlparse.urlparse(link['href']).query + params = urlparse.parse_qs(query_str) + next = True + break + except KeyError: + break diff --git a/neutronclient/version.py b/neutronclient/version.py new file mode 100644 index 000000000..9315671c3 --- /dev/null +++ b/neutronclient/version.py @@ -0,0 +1,22 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# @author: Carl Baldwin, Hewlett-Packard + +import pbr.version + + +__version__ = pbr.version.VersionInfo('python-neutronclient').version_string() diff --git a/openstack-common.conf b/openstack-common.conf new file mode 100644 index 000000000..ecd7b3ef7 --- /dev/null +++ b/openstack-common.conf @@ -0,0 +1,7 @@ +[DEFAULT] + +# The list of modules to copy from openstack-common +modules=exception,gettextutils,jsonutils,strutils,timeutils + +# The base module to hold the copy of openstack.common +base=neutronclient diff --git a/quantum/client/__init__.py b/quantum/client/__init__.py deleted file mode 100644 index d14eac66b..000000000 --- a/quantum/client/__init__.py +++ /dev/null @@ -1,545 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Citrix Systems -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# @author: Tyler Smith, Cisco Systems - -import logging -import httplib -import socket -import time -import urllib - -from quantum.common import exceptions -from quantum.common.serializer import Serializer - -LOG = logging.getLogger('quantum.client') -AUTH_TOKEN_HEADER = "X-Auth-Token" - - -def exception_handler_v10(status_code, error_content): - """ Exception handler for API v1.0 client - - This routine generates the appropriate - Quantum exception according to the contents of the - response body - - :param status_code: HTTP error status code - :param error_content: deserialized body of error response - """ - - quantum_error_types = { - 420: 'networkNotFound', - 421: 'networkInUse', - 430: 'portNotFound', - 431: 'requestedStateInvalid', - 432: 'portInUse', - 440: 'alreadyAttached' - } - - quantum_errors = { - 400: exceptions.BadInputError, - 401: exceptions.NotAuthorized, - 404: exceptions.NotFound, - 420: exceptions.NetworkNotFoundClient, - 421: exceptions.NetworkInUseClient, - 430: exceptions.PortNotFoundClient, - 431: exceptions.StateInvalidClient, - 432: exceptions.PortInUseClient, - 440: exceptions.AlreadyAttachedClient, - 501: NotImplementedError - } - - # Find real error type - error_type = None - if isinstance(error_content, dict): - error_type = quantum_error_types.get(status_code) - if error_type: - error_dict = error_content[error_type] - error_message = error_dict['message'] + "\n" +\ - error_dict['detail'] - else: - error_message = error_content - # raise the appropriate error! - ex = quantum_errors[status_code](message=error_message) - ex.args = ([dict(status_code=status_code, - message=error_message)],) - raise ex - - -def exception_handler_v11(status_code, error_content): - """ Exception handler for API v1.1 client - - This routine generates the appropriate - Quantum exception according to the contents of the - response body - - :param status_code: HTTP error status code - :param error_content: deserialized body of error response - """ - - quantum_errors = { - 'NetworkNotFound': exceptions.NetworkNotFoundClient, - 'NetworkInUse': exceptions.NetworkInUseClient, - 'PortNotFound': exceptions.PortNotFoundClient, - 'RequestedStateInvalid': exceptions.StateInvalidClient, - 'PortInUse': exceptions.PortInUseClient, - 'AlreadyAttached': exceptions.AlreadyAttachedClient, - } - - error_dict = None - if isinstance(error_content, dict): - error_dict = error_content.get('QuantumError') - # Find real error type - if error_dict: - # If QuantumError key is found, it will definitely contain - # a 'message' and 'type' keys - error_type = error_dict['type'] - error_message = (error_dict['message'] + "\n" + - error_dict['detail']) - # raise the appropriate error! - ex = quantum_errors[error_type](message=error_message) - ex.args = ([dict(status_code=status_code, - message=error_message)],) - raise ex - # If we end up here the exception was not a quantum error - msg = "%s-%s" % (status_code, error_content) - raise exceptions.QuantumClientException(message=msg) - - -EXCEPTION_HANDLERS = { - '1.0': exception_handler_v10, - '1.1': exception_handler_v11 -} - - -class ApiCall(object): - """A Decorator to add support for format and tenant overriding""" - def __init__(self, function): - self.function = function - - def __get__(self, instance, owner): - def with_params(*args, **kwargs): - """ - Temporarily sets the format and tenant for this request - """ - (format, tenant) = (instance.format, instance.tenant) - - if 'format' in kwargs: - instance.format = kwargs['format'] - if 'tenant' in kwargs: - instance.tenant = kwargs['tenant'] - - ret = self.function(instance, *args) - (instance.format, instance.tenant) = (format, tenant) - return ret - return with_params - - -class Client(object): - - """A base client class - derived from Glance.BaseClient""" - - #Metadata for deserializing xml - _serialization_metadata = { - "application/xml": { - "attributes": { - "network": ["id", "name"], - "port": ["id", "state"], - "attachment": ["id"]}, - "plurals": {"networks": "network", - "ports": "port"}}, - } - - # Action query strings - networks_path = "/networks" - network_path = "/networks/%s" - ports_path = "/networks/%s/ports" - port_path = "/networks/%s/ports/%s" - attachment_path = "/networks/%s/ports/%s/attachment" - detail_path = "/detail" - - 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, - auth_token=None, logger=None, - version="1.1", - uri_prefix="/tenants/{tenant_id}", - retries=0, - retry_interval=1): - """ - Creates a new client to some service. - - :param host: The host where service resides - :param port: The port where service resides - :param use_ssl: True to use SSL, False to use HTTP - :param tenant: The tenant ID to make requests with - :param format: The format to query the server with - :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 - :param retries: How many times to retry a failed connection attempt - :param retry_interval: The # of seconds between connection attempts - """ - self.host = host - self.port = port - self.use_ssl = use_ssl - self.tenant = tenant - self.format = format - self.connection = None - self.testingStub = testingStub - self.key_file = key_file - self.cert_file = cert_file - self.logger = logger - self.auth_token = auth_token - self.version = version - self.action_prefix = "/v%s%s" % (version, uri_prefix) - self.retries = retries - self.retry_interval = retry_interval - - def _handle_fault_response(self, status_code, response_body): - # Create exception with HTTP status code and message - error_message = response_body - LOG.debug("Server returned error: %s", status_code) - LOG.debug("Error message: %s", error_message) - # Add deserialized error message to exception arguments - try: - des_error_body = Serializer().deserialize(error_message, - self.content_type()) - except: - # If unable to deserialized body it is probably not a - # Quantum error - des_error_body = {'message': error_message} - # Raise the appropriate exception - EXCEPTION_HANDLERS[self.version](status_code, des_error_body) - - def get_connection_type(self): - """ - Returns the proper connection type - """ - if self.testingStub: - return self.testingStub - if self.use_ssl: - return httplib.HTTPSConnection - else: - return httplib.HTTPConnection - - def _send_request(self, conn, method, action, body, headers): - # Salvatore: Isolating this piece of code in its own method to - # facilitate stubout for testing - if self.logger: - self.logger.debug("Quantum Client Request:\n" \ - + method + " " + action + "\n") - if body: - self.logger.debug(body) - conn.request(method, action, body, headers) - return conn.getresponse() - - def do_request(self, method, action, body=None, - headers=None, params=None): - """ - Connects to the server and issues a request. - Returns the result data, or raises an appropriate exception if - HTTP status code is not 2xx - - :param method: HTTP method ("GET", "POST", "PUT", etc...) - :param body: string of data to send, or None (default) - :param headers: mapping of key/value pairs to add as headers - :param params: dictionary of key/value pairs to add to append - to action - :raises: ConnectionFailed - """ - LOG.debug("Client issuing request: %s", action) - # Ensure we have a tenant id - if not self.tenant: - raise Exception("Tenant ID not set") - # Add format and tenant_id - action += ".%s" % self.format - action = self.action_prefix + action - action = action.replace('{tenant_id}', self.tenant) - - if type(params) is dict: - action += '?' + urllib.urlencode(params) - if body: - body = self.serialize(body) - try: - 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) - if self.use_ssl and len(certs): - conn = connection_type(self.host, self.port, **certs) - else: - conn = connection_type(self.host, self.port) - res = self._send_request(conn, method, action, body, headers) - status_code = self.get_status_code(res) - data = res.read() - 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, - httplib.NO_CONTENT): - return self.deserialize(data, status_code) - else: - self._handle_fault_response(status_code, data) - except (socket.error, IOError), e: - exc = exceptions.ConnectionFailed(reason=str(e)) - LOG.exception(exc.message) - raise exc - - def get_status_code(self, response): - """ - Returns the integer status code from the response, which - can be either a Webob.Response (used in testing) or httplib.Response - """ - if hasattr(response, 'status_int'): - return response.status_int - else: - return response.status - - def serialize(self, data): - """ - Serializes a dictionary with a single key (which can contain any - structure) into either xml or json - """ - if data is None: - return None - elif type(data) is dict: - return Serializer().serialize(data, self.content_type()) - else: - raise Exception("unable to serialize object of type = '%s'" \ - % type(data)) - - def deserialize(self, data, status_code): - """ - Deserializes a an xml or json string into a dictionary - """ - if status_code == 204: - return data - return Serializer(self._serialization_metadata).\ - deserialize(data, self.content_type()) - - def content_type(self, format=None): - """ - Returns the mime-type for either 'xml' or 'json'. Defaults to the - currently set format - """ - if not format: - format = self.format - return "application/%s" % (format) - - def retry_request(self, method, action, body=None, - headers=None, params=None): - """ - Call do_request with the default retry configuration. Only - idempotent requests should retry failed connection attempts. - - :raises: ConnectionFailed if the maximum # of retries is exceeded - """ - max_attempts = self.retries + 1 - for i in xrange(max_attempts): - try: - return self.do_request(method, action, body=body, - headers=headers, params=params) - except exceptions.ConnectionFailed: - # Exception has already been logged by do_request() - if i < self.retries: - LOG.debug(_('Retrying connection to quantum service')) - time.sleep(self.retry_interval) - - raise exceptions.ConnectionFailed(reason=_("Maximum attempts reached")) - - def delete(self, action, body=None, headers=None, params=None): - return self.retry_request("DELETE", action, body=body, - headers=headers, params=params) - - def get(self, action, body=None, headers=None, params=None): - return self.retry_request("GET", action, body=body, - headers=headers, params=params) - - def post(self, action, body=None, headers=None, params=None): - # Do not retry POST requests to avoid the orphan objects problem. - return self.do_request("POST", action, body=body, - headers=headers, params=params) - - def put(self, action, body=None, headers=None, params=None): - return self.retry_request("PUT", action, body=body, - headers=headers, params=params) - - @ApiCall - def list_networks(self): - """ - Fetches a list of all networks for a tenant - """ - return self.get(self.networks_path) - - @ApiCall - def list_networks_details(self): - """ - Fetches a detailed list of all networks for a tenant - """ - return self.get(self.networks_path + self.detail_path) - - @ApiCall - def show_network(self, network): - """ - Fetches information of a certain network - """ - return self.get(self.network_path % (network)) - - @ApiCall - def show_network_details(self, network): - """ - Fetches the details of a certain network - """ - return self.get((self.network_path + self.detail_path) % (network)) - - @ApiCall - def create_network(self, body=None): - """ - Creates a new network - """ - return self.post(self.networks_path, body=body) - - @ApiCall - def update_network(self, network, body=None): - """ - Updates a network - """ - return self.put(self.network_path % (network), body=body) - - @ApiCall - def delete_network(self, network): - """ - Deletes the specified network - """ - return self.delete(self.network_path % (network)) - - @ApiCall - def list_ports(self, network): - """ - Fetches a list of ports on a given network - """ - return self.get(self.ports_path % (network)) - - @ApiCall - def list_ports_details(self, network): - """ - Fetches a detailed list of ports on a given network - """ - return self.get((self.ports_path + self.detail_path) % (network)) - - @ApiCall - def show_port(self, network, port): - """ - Fetches the information of a certain port - """ - return self.get(self.port_path % (network, port)) - - @ApiCall - def show_port_details(self, network, port): - """ - Fetches the details of a certain port - """ - return self.get((self.port_path + self.detail_path) % (network, port)) - - @ApiCall - def create_port(self, network, body=None): - """ - Creates a new port on a given network - """ - return self.post(self.ports_path % (network), body=body) - - @ApiCall - def delete_port(self, network, port): - """ - Deletes the specified port from a network - """ - return self.delete(self.port_path % (network, port)) - - @ApiCall - def update_port(self, network, port, body=None): - """ - Sets the attributes of the specified port - """ - return self.put(self.port_path % (network, port), body=body) - - @ApiCall - def show_port_attachment(self, network, port): - """ - Fetches the attachment-id associated with the specified port - """ - return self.get(self.attachment_path % (network, port)) - - @ApiCall - def attach_resource(self, network, port, body=None): - """ - Sets the attachment-id of the specified port - """ - return self.put(self.attachment_path % (network, port), body=body) - - @ApiCall - def detach_resource(self, network, port): - """ - Removes the attachment-id of the specified port - """ - return self.delete(self.attachment_path % (network, port)) - - -class ClientV11(Client): - """ - This class overiddes some methods of the Client class in order to deal with - features specific to API v1.1 such as filters - """ - - @ApiCall - def list_networks(self, **filters): - """ - Fetches a list of all networks for a tenant - """ - # Pass filters in "params" argument to do_request - return self.get(self.networks_path, params=filters) - - @ApiCall - def list_networks_details(self, **filters): - """ - Fetches a detailed list of all networks for a tenant - """ - return self.get(self.networks_path + self.detail_path, params=filters) - - @ApiCall - def list_ports(self, network, **filters): - """ - Fetches a list of ports on a given network - """ - # Pass filters in "params" argument to do_request - return self.get(self.ports_path % (network), params=filters) - - @ApiCall - def list_ports_details(self, network, **filters): - """ - Fetches a detailed list of ports on a given network - """ - return self.get((self.ports_path + self.detail_path) % (network), - params=filters) diff --git a/quantum/client/batch_config.py b/quantum/client/batch_config.py deleted file mode 100755 index edcb75825..000000000 --- a/quantum/client/batch_config.py +++ /dev/null @@ -1,121 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Nicira Networks, 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: Dan Wendlandt, Nicira Networks, Inc. - -import logging as LOG -from optparse import OptionParser -import os -import sys - -possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), - os.pardir, - os.pardir, - os.pardir)) - -if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')): - sys.path.insert(0, possible_topdir) - -from quantum.client import Client - -FORMAT = "json" -CONTENT_TYPE = "application/" + FORMAT - - -def delete_all_nets(client): - res = client.list_networks() - for n in res["networks"]: - nid = n["id"] - pres = client.list_ports(nid) - for port in pres["ports"]: - pid = port['id'] - client.detach_resource(nid, pid) - client.delete_port(nid, pid) - print "Deleted Virtual Port:%s " \ - "on Virtual Network:%s" % (pid, nid) - client.delete_network(nid) - print "Deleted Virtual Network with ID:%s" % nid - - -def create_net_with_attachments(client, net_name, iface_ids): - data = {'network': {'name': '%s' % net_name}} - res = client.create_network(data) - nid = res["network"]["id"] - print "Created a new Virtual Network %s with ID:%s" % (net_name, nid) - - for iface_id in iface_ids: - res = client.create_port(nid, {'port': {'state': 'ACTIVE'}}) - new_port_id = res["port"]["id"] - print "Created Virtual Port:%s " \ - "on Virtual Network:%s" % (new_port_id, nid) - data = {'attachment': {'id': iface_id}} - client.attach_resource(nid, new_port_id, data) - print "Plugged interface \"%s\" to port:%s on network:%s" % \ - (iface_id, new_port_id, nid) - -if __name__ == "__main__": - usagestr = "Usage: %prog [OPTIONS] [args]\n" \ - "Example config-string: net1=instance-1,instance-2"\ - ":net2=instance-3,instance-4\n" \ - "This string would create two networks: \n" \ - "'net1' would have two ports, with iface-ids "\ - "instance-1 and instance-2 attached\n" \ - "'net2' would have two ports, with iface-ids"\ - " instance-3 and instance-4 attached\n" - 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("-d", "--delete", dest="delete", - action="store_true", default=False, \ - help="delete existing tenants networks") - - options, args = parser.parse_args() - - if options.verbose: - LOG.basicConfig(level=LOG.DEBUG) - else: - LOG.basicConfig(level=LOG.WARN) - - if len(args) < 1: - parser.print_help() - sys.exit(1) - - nets = {} - tenant_id = args[0] - if len(args) > 1: - config_str = args[1] - for net_str in config_str.split(":"): - arr = net_str.split("=") - net_name = arr[0] - nets[net_name] = arr[1].split(",") - - print "nets: %s" % str(nets) - - client = Client(options.host, options.port, options.ssl, - format='json', tenant=tenant_id) - - if options.delete: - delete_all_nets(client) - - for net_name, iface_ids in nets.items(): - create_net_with_attachments(client, net_name, iface_ids) - - sys.exit(0) diff --git a/quantum/client/cli.py b/quantum/client/cli.py deleted file mode 100755 index 18e0a6a2e..000000000 --- a/quantum/client/cli.py +++ /dev/null @@ -1,304 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Nicira Networks, Inc. -# Copyright 2012 Citrix Systems -# -# 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: Somik Behera, Nicira Networks, Inc. -# @author: Brad Hall, Nicira Networks, Inc. -# @author: Salvatore Orlando, Citrix - -import gettext -import logging -import logging.handlers -import os -import sys - -from optparse import OptionParser - -from quantum.common import exceptions - -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 cli_lib -from quantum.client import Client -from quantum.client import ClientV11 -from quantum.common import utils - -#Configure logger for client - cli logger is a child of it -#NOTE(salvatore-orlando): logger name does not map to package -#this is deliberate. Simplifies logger configuration -LOG = logging.getLogger('quantum') -DEFAULT_QUANTUM_VERSION = '1.1' -FORMAT = 'json' -commands_v10 = { - "list_nets": { - "func": cli_lib.list_nets, - "args": ["tenant-id"]}, - "list_nets_detail": { - "func": cli_lib.list_nets_detail, - "args": ["tenant-id"]}, - "create_net": { - "func": cli_lib.create_net, - "args": ["tenant-id", "net-name"]}, - "delete_net": { - "func": cli_lib.delete_net, - "args": ["tenant-id", "net-id"]}, - "show_net": { - "func": cli_lib.show_net, - "args": ["tenant-id", "net-id"]}, - "show_net_detail": { - "func": cli_lib.show_net_detail, - "args": ["tenant-id", "net-id"]}, - "update_net": { - "func": cli_lib.update_net, - "args": ["tenant-id", "net-id", "new-name"]}, - "list_ports": { - "func": cli_lib.list_ports, - "args": ["tenant-id", "net-id"]}, - "list_ports_detail": { - "func": cli_lib.list_ports_detail, - "args": ["tenant-id", "net-id"]}, - "create_port": { - "func": cli_lib.create_port, - "args": ["tenant-id", "net-id"]}, - "delete_port": { - "func": cli_lib.delete_port, - "args": ["tenant-id", "net-id", "port-id"]}, - "update_port": { - "func": cli_lib.update_port, - "args": ["tenant-id", "net-id", "port-id", "params"]}, - "show_port": { - "func": cli_lib.show_port, - "args": ["tenant-id", "net-id", "port-id"]}, - "show_port_detail": { - "func": cli_lib.show_port_detail, - "args": ["tenant-id", "net-id", "port-id"]}, - "plug_iface": { - "func": cli_lib.plug_iface, - "args": ["tenant-id", "net-id", "port-id", "iface-id"]}, - "unplug_iface": { - "func": cli_lib.unplug_iface, - "args": ["tenant-id", "net-id", "port-id"]}, - "show_iface": { - "func": cli_lib.show_iface, - "args": ["tenant-id", "net-id", "port-id"]}, } - -commands_v11 = commands_v10.copy() -commands_v11.update({ - "list_nets": { - "func": cli_lib.list_nets_v11, - "args": ["tenant-id"], - "filters": ["name", "op-status", "port-op-status", "port-state", - "has-attachment", "attachment", "port"]}, - "list_nets_detail": { - "func": cli_lib.list_nets_detail_v11, - "args": ["tenant-id"], - "filters": ["name", "op-status", "port-op-status", "port-state", - "has-attachment", "attachment", "port"]}, - "list_ports": { - "func": cli_lib.list_ports_v11, - "args": ["tenant-id", "net-id"], - "filters": ["name", "op-status", "has-attachment", "attachment"]}, - "list_ports_detail": { - "func": cli_lib.list_ports_detail_v11, - "args": ["tenant-id", "net-id"], - "filters": ["name", "op-status", "has-attachment", "attachment"]}, - }) -commands = { - '1.0': commands_v10, - '1.1': commands_v11 - } -clients = { - '1.0': Client, - '1.1': ClientV11 - } - - -def help(version): - print "\nCommands:" - cmds = commands[version] - for k in cmds.keys(): - print " %s %s %s" % (k, - " ".join(["<%s>" % y for y in cmds[k]["args"]]), - 'filters' in cmds[k] and "[filterspec ...]" or "") - - -def print_usage(cmd, version): - cmds = commands[version] - print "Usage:\n %s %s" % (cmd, - " ".join(["<%s>" % y for y in cmds[cmd]["args"]])) - - -def build_args(cmd, cmdargs, arglist): - arglist_len = len(arglist) - cmdargs_len = len(cmdargs) - if arglist_len < cmdargs_len: - message = "Not enough arguments for \"%s\" (expected: %d, got: %d)"\ - % (cmd, len(cmdargs), arglist_len) - raise exceptions.QuantumCLIError(message=message) - args = arglist[:cmdargs_len] - return args - - -def build_filters(cmd, cmd_filters, filter_list, version): - filters = {} - # Each filter is expected to be in the = format - for flt in filter_list: - split_filter = flt.split("=") - if len(split_filter) != 2: - message = "Invalid filter argument detected (%s)" % flt - raise exceptions.QuantumCLIError(message=message) - filter_key, filter_value = split_filter - # Ensure the filter is allowed - if not filter_key in cmd_filters: - message = "Invalid filter key (%s)" % filter_key - raise exceptions.QuantumCLIError(message=message) - filters[filter_key] = filter_value - return filters - - -def build_cmd(cmd, cmd_args, cmd_filters, arglist, version): - """ - Builds arguments and filters to be passed to the cli library routines - - :param cmd: Command to be executed - :param cmd_args: List of arguments required by the command - :param cmd_filters: List of filters allowed by the command - :param arglist: Command line arguments (includes both arguments and - filter specifications) - :param version: API version - """ - arglist_len = len(arglist) - try: - # Parse arguments - args = build_args(cmd, cmd_args, arglist) - # Parse filters - filters = None - if cmd_filters: - # Pop consumed arguments - arglist = arglist[len(args):] - filters = build_filters(cmd, cmd_filters, arglist, version) - except exceptions.QuantumCLIError as cli_ex: - LOG.error(cli_ex.message) - print " Error in command line:%s" % cli_ex.message - print_usage(cmd, version) - return None, None - filter_len = (filters is not None) and len(filters) or 0 - if len(arglist) - len(args) - filter_len > 0: - message = "Too many arguments for \"%s\" (expected: %d, got: %d)"\ - % (cmd, len(cmd_args), arglist_len) - LOG.error(message) - print "Error in command line: %s " % message - print "Usage:\n %s %s" % (cmd, - " ".join(["<%s>" % y for y in commands[version][cmd]["args"]])) - return None, None - # Append version to arguments for cli functions - args.append(version) - return args, filters - - -def instantiate_client(host, port, ssl, tenant, token, version): - client = clients[version](host, - port, - ssl, - tenant, - FORMAT, - auth_token=token, - version=version) - return client - - -def main(): - usagestr = "Usage: %prog [OPTIONS] [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") - parser.add_option("-t", "--token", dest="token", - type="string", default=None, help="authentication token") - parser.add_option('--version', - default=utils.env('QUANTUM_VERSION', default=DEFAULT_QUANTUM_VERSION), - help='Accepts 1.1 and 1.0, defaults to env[QUANTUM_VERSION].') - 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)) - # Set permissions on log file - os.chmod(options.logfile, 0644) - - version = options.version - if not version in commands: - LOG.error("Unknown API version specified:%s", version) - print "Unknown API version: %s" % version - - if len(args) < 1: - parser.print_help() - help(version) - sys.exit(1) - - cmd = args[0] - if cmd not in commands[version].keys(): - LOG.error("Unknown command: %s" % cmd) - help(version) - sys.exit(1) - - # Build argument list for CLI command - # The argument list will include the version number as well - args, filters = build_cmd(cmd, - commands[version][cmd]["args"], - commands[version][cmd].get("filters", None), - args[1:], - options.version) - if not args: - sys.exit(1) - LOG.info("Executing command \"%s\" with args: %s" % (cmd, args)) - - client = instantiate_client(options.host, - options.port, - options.ssl, - args[0], - options.token, - options.version) - # append filters to arguments - # this will allow for using the same prototype for v10 and v11 - # TODO: Use **kwargs instead of *args (keyword is better than positional) - if filters: - args.append(filters) - commands[version][cmd]["func"](client, *args) - - LOG.info("Command execution completed") - sys.exit(0) - -if __name__ == '__main__': - main() diff --git a/quantum/client/cli_lib.py b/quantum/client/cli_lib.py deleted file mode 100755 index 449e0add3..000000000 --- a/quantum/client/cli_lib.py +++ /dev/null @@ -1,537 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Nicira Networks, Inc. -# Copyright 2011 Citrix Systems -# -# 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: Somik Behera, Nicira Networks, Inc. -# @author: Brad Hall, Nicira Networks, Inc. -# @author: Salvatore Orlando, Citrix - -""" Functions providing implementation for CLI commands. """ - -import logging -import os -import sys -import traceback - -FORMAT = "json" -LOG = logging.getLogger('quantum.client.cli_lib') - - -class OutputTemplate(object): - """ A class for generating simple templated output. - Based on Python templating mechanism. - Templates can also express attributes on objects, such as network.id; - templates can also be nested, thus allowing for iteration on inner - templates. - - Examples: - 1) template with class attributes - Name: %(person.name)s \n - Surname: %(person.surname)s \n - 2) template with iteration - Telephone numbers: \n - %(phone_numbers|Telephone number:%(number)s) - 3) template with iteration and class attributes - Addresses: \n - %(Addresses|Street:%(address.street)s\nNumber%(address.number)) - - Instances of this class are initialized with a template string and - the dictionary for performing substitution. The class implements the - __str__ method, so it can be directly printed. - """ - - def __init__(self, template, data): - self._template = template - self.data = data - - def __str__(self): - return self._template % self - - def __getitem__(self, key): - items = key.split("|") - # Note(madhav-puri): for now the logic supports only 0 or 1 list - # indicators (|) in a template. - if len(items) > 2: - raise Exception("Found more than one list indicator (|).") - if len(items) == 1: - return self._make_attribute(key) - else: - # Note(salvatore-orlando): items[0] must be subscriptable - return self._make_list(self._make_attribute(items[0]), items[1]) - - def _make_attribute(self, item): - """ Renders an entity attribute key in the template. - e.g.: entity.attribute - """ - items = item.split('.') - attr = self.data[items[0]] - for _item in items[1:]: - attr = attr[_item] - return attr - - def _make_list(self, items, inner_template): - """ Renders a list key in the template. - e.g.: %(list|item data:%(item)) - """ - #make sure list is subscriptable - if not hasattr(items, '__getitem__'): - raise Exception("Element is not iterable") - return "\n".join([str(OutputTemplate(inner_template, item)) - for item in items]) - - -class CmdOutputTemplate(OutputTemplate): - """ This class provides templated output for CLI commands. - Extends OutputTemplate loading a different template for each command. - """ - - _templates_v10 = { - "list_nets": "Virtual Networks for Tenant %(tenant_id)s\n" + - "%(networks|\tNetwork ID: %(id)s)s", - "list_nets_detail": "Virtual Networks for Tenant %(tenant_id)s\n" + - "%(networks|\tNetwork %(name)s with ID: %(id)s)s", - "show_net": "Network ID: %(network.id)s\n" + - "Network Name: %(network.name)s", - "show_net_detail": "Network ID: %(network.id)s\n" + - "Network Name: %(network.name)s\n" + - "Ports: %(network.ports|\tID: %(id)s\n" + - "\t\tadministrative state: %(state)s\n" + - "\t\tinterface: %(attachment.id)s)s", - "create_net": "Created a new Virtual Network with ID: " + - "%(network_id)s\n" + - "for Tenant: %(tenant_id)s", - "update_net": "Updated Virtual Network with ID: " + - "%(network.id)s\n" + - "for Tenant: %(tenant_id)s\n", - "delete_net": "Deleted Virtual Network with ID: " + - "%(network_id)s\n" + - "for Tenant %(tenant_id)s", - "list_ports": "Ports on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s\n" + - "%(ports|\tLogical Port: %(id)s)s", - "list_ports_detail": "Ports on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s\n" + - "%(ports|\tLogical Port: %(id)s\n" + - "\t\tadministrative State: %(state)s)s", - "create_port": "Created new Logical Port with ID: " + - "%(port_id)s\n" + - "on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s", - "show_port": "Logical Port ID: %(port.id)s\n" + - "administrative State: %(port.state)s\n" + - "on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s", - "show_port_detail": "Logical Port ID: %(port.id)s\n" + - "administrative State: %(port.state)s\n" + - "interface: %(port.attachment.id)s\n" + - "on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s", - "update_port": "Updated Logical Port " + - "with ID: %(port.id)s\n" + - "on Virtual Network: %(network_id)s\n" + - "for tenant: %(tenant_id)s", - "delete_port": "Deleted Logical Port with ID: %(port_id)s\n" + - "on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s", - "plug_iface": "Plugged interface %(attachment)s\n" + - "into Logical Port: %(port_id)s\n" + - "on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s", - "unplug_iface": "Unplugged interface from Logical Port:" + - "%(port_id)s\n" + - "on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s", - "show_iface": "interface: %(iface.id)s\n" + - "plugged in Logical Port ID: %(port_id)s\n" + - "on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s"} - - _templates_v11 = _templates_v10.copy() - _templates_v11.update({ - "show_net": "Network ID: %(network.id)s\n" + - "network Name: %(network.name)s\n" + - "operational Status: %(network.op-status)s", - "show_net_detail": "Network ID: %(network.id)s\n" + - "Network Name: %(network.name)s\n" + - "operational Status: %(network.op-status)s\n" + - "Ports: %(network.ports|\tID: %(id)s\n" + - "\t\tadministrative state: %(state)s\n" + - "\t\tinterface: %(attachment.id)s)s", - "show_port": "Logical Port ID: %(port.id)s\n" + - "administrative state: %(port.state)s\n" + - "operational status: %(port.op-status)s\n" + - "on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s", - "show_port_detail": "Logical Port ID: %(port.id)s\n" + - "administrative State: %(port.state)s\n" + - "operational status: %(port.op-status)s\n" + - "interface: %(port.attachment.id)s\n" + - "on Virtual Network: %(network_id)s\n" + - "for Tenant: %(tenant_id)s", - }) - - _templates = { - '1.0': _templates_v10, - '1.1': _templates_v11 - } - - def __init__(self, cmd, data, version): - super(CmdOutputTemplate, self).__init__( - self._templates[version][cmd], data) - - -def _handle_exception(ex): - status_code = None - message = None - # Retrieve dict at 1st element of tuple at last argument - if ex.args and isinstance(ex.args[-1][0], dict): - status_code = ex.args[-1][0].get('status_code', None) - message = ex.args[-1][0].get('message', None) - msg_1 = "Command failed with error code: %s" \ - % (status_code or '') - msg_2 = "Error message:%s" % (message or '') - print msg_1 - print msg_2 - else: - traceback.print_exc() - - -def prepare_output(cmd, tenant_id, response, version): - LOG.debug("Preparing output for response:%s, version:%s" - % (response, version)) - response['tenant_id'] = tenant_id - output = str(CmdOutputTemplate(cmd, response, version)) - LOG.debug("Finished preparing output for command:%s", cmd) - return output - - -def list_nets(client, *args): - tenant_id, version = args - try: - res = client.list_networks() - LOG.debug("Operation 'list_networks' executed.") - output = prepare_output("list_nets", tenant_id, res, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_nets_v11(client, *args): - filters = {} - tenant_id, version = args[:2] - if len(args) > 2: - filters = args[2] - try: - res = client.list_networks(**filters) - LOG.debug("Operation 'list_networks' executed.") - output = prepare_output("list_nets", tenant_id, res, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_nets_detail(client, *args): - tenant_id, version = args - try: - res = client.list_networks_details() - LOG.debug("Operation 'list_networks_details' executed.") - output = prepare_output("list_nets_detail", tenant_id, res, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_nets_detail_v11(client, *args): - filters = {} - tenant_id, version = args[:2] - if len(args) > 2: - filters = args[2] - try: - res = client.list_networks_details(**filters) - LOG.debug("Operation 'list_networks_details' executed.") - output = prepare_output("list_nets_detail", tenant_id, res, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def create_net(client, *args): - tenant_id, name, version = args - data = {'network': {'name': name}} - new_net_id = None - try: - res = client.create_network(data) - new_net_id = res["network"]["id"] - LOG.debug("Operation 'create_network' executed.") - output = prepare_output("create_net", - tenant_id, - dict(network_id=new_net_id), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def delete_net(client, *args): - tenant_id, network_id, version = args - try: - client.delete_network(network_id) - LOG.debug("Operation 'delete_network' executed.") - output = prepare_output("delete_net", - tenant_id, - dict(network_id=network_id), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def show_net(client, *args): - tenant_id, network_id, version = args - try: - res = client.show_network(network_id)["network"] - LOG.debug("Operation 'show_network' executed.") - output = prepare_output("show_net", tenant_id, - dict(network=res), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def show_net_detail(client, *args): - tenant_id, network_id, version = args - try: - res = client.show_network_details(network_id)["network"] - LOG.debug("Operation 'show_network_details' executed.") - if not len(res["ports"]): - res["ports"] = [{"id": "", - "state": "", - "attachment": {"id": ""}}] - else: - for port in res["ports"]: - if "attachment" not in port: - port["attachment"] = {"id": ""} - - output = prepare_output("show_net_detail", - tenant_id, - dict(network=res), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def update_net(client, *args): - tenant_id, network_id, param_data, version = args - data = {'network': {}} - for kv in param_data.split(","): - k, v = kv.split("=") - data['network'][k] = v - data['network']['id'] = network_id - try: - client.update_network(network_id, data) - LOG.debug("Operation 'update_network' executed.") - # Response has no body. Use data for populating output - output = prepare_output("update_net", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_ports(client, *args): - tenant_id, network_id, version = args - try: - ports = client.list_ports(network_id) - LOG.debug("Operation 'list_ports' executed.") - data = ports - data['network_id'] = network_id - output = prepare_output("list_ports", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_ports_v11(client, *args): - filters = {} - tenant_id, network_id, version = args[:3] - if len(args) > 3: - filters = args[3] - try: - ports = client.list_ports(network_id, **filters) - LOG.debug("Operation 'list_ports' executed.") - data = ports - data['network_id'] = network_id - output = prepare_output("list_ports", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_ports_detail(client, *args): - tenant_id, network_id, version = args - try: - ports = client.list_ports_details(network_id) - LOG.debug("Operation 'list_ports_details' executed.") - data = ports - data['network_id'] = network_id - output = prepare_output("list_ports_detail", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_ports_detail_v11(client, *args): - filters = {} - tenant_id, network_id, version = args[:3] - if len(args) > 3: - filters = args[3] - try: - ports = client.list_ports_details(network_id, **filters) - LOG.debug("Operation 'list_ports' executed.") - data = ports - data['network_id'] = network_id - output = prepare_output("list_ports_detail", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def create_port(client, *args): - tenant_id, network_id, version = args - try: - res = client.create_port(network_id) - LOG.debug("Operation 'create_port' executed.") - new_port_id = res["port"]["id"] - output = prepare_output("create_port", - tenant_id, - dict(network_id=network_id, - port_id=new_port_id), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def delete_port(client, *args): - tenant_id, network_id, port_id, version = args - try: - client.delete_port(network_id, port_id) - LOG.debug("Operation 'delete_port' executed.") - output = prepare_output("delete_port", - tenant_id, - dict(network_id=network_id, - port_id=port_id), - version) - print output - except Exception as ex: - _handle_exception(ex) - return - - -def show_port(client, *args): - tenant_id, network_id, port_id, version = args - try: - port = client.show_port(network_id, port_id)["port"] - LOG.debug("Operation 'show_port' executed.") - output = prepare_output("show_port", tenant_id, - dict(network_id=network_id, - port=port), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def show_port_detail(client, *args): - tenant_id, network_id, port_id, version = args - try: - port = client.show_port_details(network_id, port_id)["port"] - LOG.debug("Operation 'show_port_details' executed.") - if 'attachment' not in port: - port['attachment'] = {'id': ''} - output = prepare_output("show_port_detail", tenant_id, - dict(network_id=network_id, - port=port), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def update_port(client, *args): - tenant_id, network_id, port_id, param_data, version = args - data = {'port': {}} - for kv in param_data.split(","): - k, v = kv.split("=") - data['port'][k] = v - data['network_id'] = network_id - data['port']['id'] = port_id - try: - client.update_port(network_id, port_id, data) - LOG.debug("Operation 'udpate_port' executed.") - # Response has no body. Use data for populating output - output = prepare_output("update_port", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def plug_iface(client, *args): - tenant_id, network_id, port_id, attachment, version = args - try: - data = {'attachment': {'id': '%s' % attachment}} - client.attach_resource(network_id, port_id, data) - LOG.debug("Operation 'attach_resource' executed.") - output = prepare_output("plug_iface", tenant_id, - dict(network_id=network_id, - port_id=port_id, - attachment=attachment), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def unplug_iface(client, *args): - tenant_id, network_id, port_id, version = args - try: - client.detach_resource(network_id, port_id) - LOG.debug("Operation 'detach_resource' executed.") - output = prepare_output("unplug_iface", - tenant_id, - dict(network_id=network_id, - port_id=port_id), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def show_iface(client, *args): - tenant_id, network_id, port_id, version = args - try: - iface = client.show_port_attachment(network_id, port_id)['attachment'] - if 'id' not in iface: - iface['id'] = '' - LOG.debug("Operation 'show_port_attachment' executed.") - output = prepare_output("show_iface", tenant_id, - dict(network_id=network_id, - port_id=port_id, - iface=iface), - version) - print output - except Exception as ex: - _handle_exception(ex) diff --git a/quantum/client/tests/unit/test_cli.py b/quantum/client/tests/unit/test_cli.py deleted file mode 100644 index 571012a28..000000000 --- a/quantum/client/tests/unit/test_cli.py +++ /dev/null @@ -1,817 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 ???? -# 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: Salvatore Orlando, Citrix Systems - -""" Module containing unit tests for Quantum - command line interface - -""" - - -import logging -import sys -import unittest - -from quantum import api as server -from quantum.client import cli_lib as cli -from quantum.client import Client -from quantum.db import api as db -from quantum.tests.unit.client_tools import stubs as client_stubs - -LOG = logging.getLogger('quantum.tests.test_cli') -API_VERSION = "1.1" -FORMAT = 'json' - - -class CLITest(unittest.TestCase): - - def setUp(self): - """Prepare the test environment""" - options = {} - options['plugin_provider'] = \ - 'quantum.plugins.sample.SamplePlugin.FakePlugin' - #TODO: make the version of the API router configurable - self.api = server.APIRouterV11(options) - - self.tenant_id = "test_tenant" - self.network_name_1 = "test_network_1" - self.network_name_2 = "test_network_2" - self.version = API_VERSION - # Prepare client and plugin manager - self.client = Client(tenant=self.tenant_id, - format=FORMAT, - testingStub=client_stubs.FakeHTTPConnection, - version=self.version) - # Redirect stdout - self.fake_stdout = client_stubs.FakeStdout() - sys.stdout = self.fake_stdout - - def tearDown(self): - """Clear the test environment""" - db.clear_db() - sys.stdout = sys.__stdout__ - - def _verify_list_networks(self): - # Verification - get raw result from db - nw_list = db.network_list(self.tenant_id) - networks = [{'id': nw.uuid, 'name': nw.name} - for nw in nw_list] - # Fill CLI template - output = cli.prepare_output('list_nets', - self.tenant_id, - dict(networks=networks), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_list_networks_details(self): - # Verification - get raw result from db - nw_list = db.network_list(self.tenant_id) - networks = [dict(id=nw.uuid, name=nw.name) for nw in nw_list] - # Fill CLI template - output = cli.prepare_output('list_nets_detail', - self.tenant_id, - dict(networks=networks), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_create_network(self): - # Verification - get raw result from db - nw_list = db.network_list(self.tenant_id) - if len(nw_list) != 1: - self.fail("No network created") - network_id = nw_list[0].uuid - # Fill CLI template - output = cli.prepare_output('create_net', - self.tenant_id, - dict(network_id=network_id), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_delete_network(self, network_id): - # Verification - get raw result from db - nw_list = db.network_list(self.tenant_id) - if len(nw_list) != 0: - self.fail("DB should not contain any network") - # Fill CLI template - output = cli.prepare_output('delete_net', - self.tenant_id, - dict(network_id=network_id), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_update_network(self): - # Verification - get raw result from db - nw_list = db.network_list(self.tenant_id) - network_data = {'id': nw_list[0].uuid, - 'name': nw_list[0].name, - 'op-status': nw_list[0].op_status} - # Fill CLI template - output = cli.prepare_output('update_net', - self.tenant_id, - dict(network=network_data), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_show_network(self): - # Verification - get raw result from db - nw = db.network_list(self.tenant_id)[0] - network = {'id': nw.uuid, - 'name': nw.name, - 'op-status': nw.op_status} - # Fill CLI template - output = cli.prepare_output('show_net', - self.tenant_id, - dict(network=network), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_show_network_details(self): - # Verification - get raw result from db - nw = db.network_list(self.tenant_id)[0] - network = {'id': nw.uuid, - 'name': nw.name, - 'op-status': nw.op_status} - port_list = db.port_list(nw.uuid) - if not port_list: - network['ports'] = [{'id': '', - 'state': '', - 'attachment': {'id': ''}}] - else: - network['ports'] = [] - for port in port_list: - network['ports'].append({'id': port.uuid, - 'state': port.state, - 'attachment': {'id': - port.interface_id - or ''}}) - - # Fill CLI template - output = cli.prepare_output('show_net_detail', - self.tenant_id, - dict(network=network), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_list_ports(self, network_id): - # Verification - get raw result from db - port_list = db.port_list(network_id) - ports = [dict(id=port.uuid, state=port.state) - for port in port_list] - # Fill CLI template - output = cli.prepare_output('list_ports', - self.tenant_id, - dict(network_id=network_id, - ports=ports), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_list_ports_details(self, network_id): - # Verification - get raw result from db - port_list = db.port_list(network_id) - ports = [dict(id=port.uuid, state=port.state) - for port in port_list] - # Fill CLI template - output = cli.prepare_output('list_ports_detail', - self.tenant_id, - dict(network_id=network_id, - ports=ports), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_create_port(self, network_id): - # Verification - get raw result from db - port_list = db.port_list(network_id) - if len(port_list) != 1: - self.fail("No port created") - port_id = port_list[0].uuid - # Fill CLI template - output = cli.prepare_output('create_port', - self.tenant_id, - dict(network_id=network_id, - port_id=port_id), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_delete_port(self, network_id, port_id): - # Verification - get raw result from db - port_list = db.port_list(network_id) - if len(port_list) != 0: - self.fail("DB should not contain any port") - # Fill CLI template - output = cli.prepare_output('delete_port', - self.tenant_id, - dict(network_id=network_id, - port_id=port_id), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_update_port(self, network_id, port_id): - # Verification - get raw result from db - port = db.port_get(port_id, network_id) - port_data = {'id': port.uuid, - 'state': port.state, - 'op-status': port.op_status} - # Fill CLI template - output = cli.prepare_output('update_port', - self.tenant_id, - dict(network_id=network_id, - port=port_data), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_show_port(self, network_id, port_id): - # Verification - get raw result from db - port = db.port_get(port_id, network_id) - port_data = {'id': port.uuid, - 'state': port.state, - 'attachment': {'id': port.interface_id or ''}, - 'op-status': port.op_status} - - # Fill CLI template - output = cli.prepare_output('show_port', - self.tenant_id, - dict(network_id=network_id, - port=port_data), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_show_port_details(self, network_id, port_id): - # Verification - get raw result from db - # TODO(salvatore-orlando): Must resolve this issue with - # attachment in separate bug fix. - port = db.port_get(port_id, network_id) - port_data = {'id': port.uuid, - 'state': port.state, - 'attachment': {'id': port.interface_id or ''}, - 'op-status': port.op_status} - - # Fill CLI template - output = cli.prepare_output('show_port_detail', - self.tenant_id, - dict(network_id=network_id, - port=port_data), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_plug_iface(self, network_id, port_id): - # Verification - get raw result from db - port = db.port_get(port_id, network_id) - # Fill CLI template - output = cli.prepare_output("plug_iface", - self.tenant_id, - dict(network_id=network_id, - port_id=port['uuid'], - attachment=port['interface_id']), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_unplug_iface(self, network_id, port_id): - # Verification - get raw result from db - port = db.port_get(port_id, network_id) - # Fill CLI template - output = cli.prepare_output("unplug_iface", - self.tenant_id, - dict(network_id=network_id, - port_id=port['uuid']), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_show_iface(self, network_id, port_id): - # Verification - get raw result from db - port = db.port_get(port_id, network_id) - iface = {'id': port.interface_id or ''} - - # Fill CLI template - output = cli.prepare_output('show_iface', - self.tenant_id, - dict(network_id=network_id, - port_id=port.uuid, - iface=iface), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def test_list_networks_v10(self): - try: - # Pre-populate data for testing using db api - db.network_create(self.tenant_id, self.network_name_1) - db.network_create(self.tenant_id, self.network_name_2) - - cli.list_nets(self.client, - self.tenant_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_networks_v10 failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_networks() - - def test_list_networks_details_v10(self): - try: - # Pre-populate data for testing using db api - db.network_create(self.tenant_id, self.network_name_1) - db.network_create(self.tenant_id, self.network_name_2) - - cli.list_nets_detail(self.client, - self.tenant_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_networks_details_v10 failed due to" + - " an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_networks_details() - - def test_list_networks_v11(self): - try: - # Pre-populate data for testing using db api - db.network_create(self.tenant_id, self.network_name_1) - db.network_create(self.tenant_id, self.network_name_2) - #TODO: test filters - cli.list_nets_v11(self.client, - self.tenant_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_networks_v11 failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_networks() - - def test_list_networks_details_v11(self): - try: - # Pre-populate data for testing using db api - db.network_create(self.tenant_id, self.network_name_1) - db.network_create(self.tenant_id, self.network_name_2) - #TODO: test filters - cli.list_nets_detail_v11(self.client, - self.tenant_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_networks_details_v11 failed due to " + - "an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_networks_details() - - def test_create_network(self): - try: - cli.create_net(self.client, - self.tenant_id, - "test", - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_create_network failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_create_network() - - def test_delete_network(self): - try: - db.network_create(self.tenant_id, self.network_name_1) - network_id = db.network_list(self.tenant_id)[0]['uuid'] - cli.delete_net(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_delete_network failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_delete_network(network_id) - - def test_show_network(self): - try: - # Load some data into the datbase - net = db.network_create(self.tenant_id, self.network_name_1) - cli.show_net(self.client, - self.tenant_id, - net['uuid'], - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_detail_network failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_network() - - def test_show_network_details_no_ports(self): - try: - # Load some data into the datbase - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - cli.show_net_detail(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_network_details_no_ports failed due to" + - " an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_network_details() - - def test_show_network_details(self): - iface_id = "flavor crystals" - try: - # Load some data into the datbase - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - db.port_set_attachment(port_id, network_id, iface_id) - port = db.port_create(network_id) - cli.show_net_detail(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_network_details failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_network_details() - - def test_update_network(self): - try: - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - cli.update_net(self.client, - self.tenant_id, - network_id, - 'name=%s' % self.network_name_2, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_update_network failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_update_network() - - def test_list_ports_v10(self): - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - db.port_create(network_id) - db.port_create(network_id) - cli.list_ports(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_ports failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_ports(network_id) - - def test_list_ports_details_v10(self): - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - db.port_create(network_id) - db.port_create(network_id) - cli.list_ports_detail(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_ports_details_v10 failed due to" + - " an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_ports_details(network_id) - - def test_list_ports_v11(self): - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - db.port_create(network_id) - db.port_create(network_id) - #TODO: test filters - cli.list_ports_v11(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_ports_v11 failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_ports(network_id) - - def test_list_ports_details_v11(self): - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - db.port_create(network_id) - db.port_create(network_id) - #TODO: test filters - cli.list_ports_detail_v11(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_ports_details_v11 failed due to " + - "an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_ports_details(network_id) - - def test_create_port(self): - network_id = None - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - cli.create_port(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_create_port failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_create_port(network_id) - - def test_delete_port(self): - network_id = None - port_id = None - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - cli.delete_port(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_delete_port failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_delete_port(network_id, port_id) - - def test_update_port(self): - try: - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - # Default state is DOWN - change to ACTIVE. - cli.update_port(self.client, - self.tenant_id, - network_id, - port_id, - 'state=ACTIVE', - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_update_port failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_update_port(network_id, port_id) - - def test_show_port(self): - network_id = None - port_id = None - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - cli.show_port(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_port failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_port(network_id, port_id) - - def test_show_port_details_no_attach(self): - network_id = None - port_id = None - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - cli.show_port_detail(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_port_details_no_attach failed due to" + - " an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_port_details(network_id, port_id) - - def test_show_port_details_with_attach(self): - network_id = None - port_id = None - iface_id = "flavor crystals" - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - db.port_set_attachment(port_id, network_id, iface_id) - cli.show_port_detail(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_port_details_with_attach failed due" + - " to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_port_details(network_id, port_id) - - def test_plug_iface(self): - network_id = None - port_id = None - try: - # Load some data into the datbase - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(net['uuid']) - port_id = port['uuid'] - cli.plug_iface(self.client, - self.tenant_id, - network_id, - port_id, - "test_iface_id", - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_plug_iface failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_plug_iface(network_id, port_id) - - def test_unplug_iface(self): - network_id = None - port_id = None - try: - # Load some data into the datbase - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(net['uuid']) - port_id = port['uuid'] - db.port_set_attachment(port_id, network_id, "test_iface_id") - cli.unplug_iface(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_plug_iface failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_unplug_iface(network_id, port_id) - - def test_show_iface_no_attach(self): - network_id = None - port_id = None - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - cli.show_iface(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_iface_no_attach failed due to" + - " an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_iface(network_id, port_id) - - def test_show_iface_with_attach(self): - network_id = None - port_id = None - iface_id = "flavor crystals" - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - db.port_set_attachment(port_id, network_id, iface_id) - cli.show_iface(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_iface_with_attach failed due" + - " to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_iface(network_id, port_id) diff --git a/quantum/client/tests/unit/test_clientlib.py b/quantum/client/tests/unit/test_clientlib.py deleted file mode 100644 index 33f094526..000000000 --- a/quantum/client/tests/unit/test_clientlib.py +++ /dev/null @@ -1,768 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Cisco Systems -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# @author: Tyler Smith, Cisco Systems - -import logging -import unittest -import re - -from quantum.common import exceptions -from quantum.common.serializer import Serializer -from quantum.client import Client - -LOG = logging.getLogger('quantum.tests.test_api') - -# Set a couple tenants to use for testing -TENANT_1 = 'totore' -TENANT_2 = 'totore2' - - -class ServerStub(): - """This class stubs a basic server for the API client to talk to""" - - class Response(object): - """This class stubs a basic response to send the API client""" - def __init__(self, content=None, status=None): - self.content = content - self.status = status - - def read(self): - return self.content - - def status(self): - return self.status - - # To test error codes, set the host to 10.0.0.1, and the port to the code - def __init__(self, host, port=9696, key_file="", cert_file=""): - self.host = host - self.port = port - self.key_file = key_file - self.cert_file = cert_file - - def request(self, method, action, body, headers): - self.method = method - self.action = action - self.body = body - - def status(self, status=None): - return status or 200 - - def getresponse(self): - res = self.Response(status=self.status()) - - # If the host is 10.0.0.1, return the port as an error code - if self.host == "10.0.0.1": - res.status = self.port - return res - - # Extract important information from the action string to assure sanity - match = re.search('tenants/(.+?)/(.+)\.(json|xml)$', self.action) - - tenant = match.group(1) - path = match.group(2) - format = match.group(3) - - data = {'data': {'method': self.method, 'action': self.action, - 'body': self.body, 'tenant': tenant, 'path': path, - 'format': format, 'key_file': self.key_file, - 'cert_file': self.cert_file}} - - # Serialize it to the proper format so the API client can handle it - if data['data']['format'] == 'json': - res.content = Serializer().serialize(data, "application/json") - else: - res.content = Serializer().serialize(data, "application/xml") - return res - - -class APITest(unittest.TestCase): - - def setUp(self): - """ Setups a test environment for the API client """ - HOST = '127.0.0.1' - PORT = 9696 - USE_SSL = False - - self.client = Client(HOST, PORT, USE_SSL, TENANT_1, 'json', ServerStub) - - def _assert_sanity(self, call, status, method, path, data=[], params={}): - """ Perform common assertions to test the sanity of client requests """ - - # Handle an error case first - if status != 200: - (self.client.host, self.client.port) = ("10.0.0.1", status) - self.assertRaises(Exception, call, *data, **params) - return - - # Make the call, then get the data from the root node and assert it - data = call(*data, **params)['data'] - - self.assertEqual(data['method'], method) - self.assertEqual(data['format'], params['format']) - self.assertEqual(data['tenant'], params['tenant']) - self.assertEqual(data['path'], path) - - return data - - def _test_list_networks(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_list_networks - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.list_networks, - status, - "GET", - "networks", - data=[], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_list_networks - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_list_networks_details(self, - tenant=TENANT_1, format='json', - status=200): - LOG.debug("_test_list_networks_details - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.list_networks_details, - status, - "GET", - "networks/detail", - data=[], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_list_networks_details - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_show_network(self, - tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_show_network - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.show_network, - status, - "GET", - "networks/001", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_show_network - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_show_network_details(self, - tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_show_network_details - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.show_network_details, - status, - "GET", - "networks/001/detail", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_show_network_details - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_create_network(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_create_network - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.create_network, - status, - "POST", - "networks", - data=[{'network': {'net-name': 'testNetwork'}}], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_create_network - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_update_network(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_update_network - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.update_network, - status, - "PUT", - "networks/001", - data=["001", - {'network': {'net-name': 'newName'}}], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_update_network - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_delete_network(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_delete_network - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.delete_network, - status, - "DELETE", - "networks/001", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_delete_network - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_list_ports(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_list_ports - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.list_ports, - status, - "GET", - "networks/001/ports", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_list_ports - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_list_ports_details(self, - tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_list_ports_details - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.list_ports_details, - status, - "GET", - "networks/001/ports/detail", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_list_ports_details - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_show_port(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_show_port - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.show_port, - status, - "GET", - "networks/001/ports/001", - data=["001", "001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_show_port - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_show_port_details(self, - tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_show_port_details - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.show_port_details, - status, - "GET", - "networks/001/ports/001/detail", - data=["001", "001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_show_port_details - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_create_port(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_create_port - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.create_port, - status, - "POST", - "networks/001/ports", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_create_port - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_delete_port(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_delete_port - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.delete_port, - status, - "DELETE", - "networks/001/ports/001", - data=["001", "001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_delete_port - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_update_port(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_update_port - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.update_port, - status, - "PUT", - "networks/001/ports/001", - data=["001", "001", - {'port': {'state': 'ACTIVE'}}], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_update_port - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_show_port_attachment(self, - tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_show_port_attachment - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.show_port_attachment, - status, - "GET", - "networks/001/ports/001/attachment", - data=["001", "001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_show_port_attachment - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_attach_resource(self, tenant=TENANT_1, - format='json', status=200): - LOG.debug("_test_attach_resource - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.attach_resource, - status, - "PUT", - "networks/001/ports/001/attachment", - data=["001", "001", - {'resource': {'id': '1234'}}], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_attach_resource - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_detach_resource(self, tenant=TENANT_1, - format='json', status=200): - LOG.debug("_test_detach_resource - tenant:%s "\ - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.detach_resource, - status, - "DELETE", - "networks/001/ports/001/attachment", - data=["001", "001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_detach_resource - tenant:%s "\ - "- format:%s - END", format, tenant) - - def _test_ssl_certificates(self, tenant=TENANT_1, - format='json', status=200): - LOG.debug("_test_ssl_certificates - tenant:%s "\ - "- format:%s - START", format, tenant) - - # Set SSL, and our cert file - self.client.use_ssl = True - cert_file = "/fake.cert" - self.client.key_file = self.client.cert_file = cert_file - - data = self._assert_sanity(self.client.list_networks, - status, - "GET", - "networks", - data=[], - params={'tenant': tenant, 'format': format}) - - self.assertEquals(data["key_file"], cert_file) - self.assertEquals(data["cert_file"], cert_file) - - LOG.debug("_test_ssl_certificates - tenant:%s "\ - "- format:%s - END", format, tenant) - - def test_list_networks_json(self): - self._test_list_networks(format='json') - - def test_list_networks_xml(self): - self._test_list_networks(format='xml') - - def test_list_networks_alt_tenant(self): - self._test_list_networks(tenant=TENANT_2) - - def test_list_networks_error_470(self): - self._test_list_networks(status=470) - - def test_list_networks_error_401(self): - self._test_list_networks(status=401) - - def test_list_networks_details_json(self): - self._test_list_networks_details(format='json') - - def test_list_networks_details_xml(self): - self._test_list_networks_details(format='xml') - - def test_list_networks_details_alt_tenant(self): - self._test_list_networks_details(tenant=TENANT_2) - - def test_list_networks_details_error_470(self): - self._test_list_networks_details(status=470) - - def test_list_networks_details_error_401(self): - self._test_list_networks_details(status=401) - - def test_show_network_json(self): - self._test_show_network(format='json') - - def test_show_network__xml(self): - self._test_show_network(format='xml') - - def test_show_network_alt_tenant(self): - self._test_show_network(tenant=TENANT_2) - - def test_show_network_error_470(self): - self._test_show_network(status=470) - - def test_show_network_error_401(self): - self._test_show_network(status=401) - - def test_show_network_error_420(self): - self._test_show_network(status=420) - - def test_show_network_details_json(self): - self._test_show_network_details(format='json') - - def test_show_network_details_xml(self): - self._test_show_network_details(format='xml') - - def test_show_network_details_alt_tenant(self): - self._test_show_network_details(tenant=TENANT_2) - - def test_show_network_details_error_470(self): - self._test_show_network_details(status=470) - - def test_show_network_details_error_401(self): - self._test_show_network_details(status=401) - - def test_show_network_details_error_420(self): - self._test_show_network_details(status=420) - - def test_create_network_json(self): - self._test_create_network(format='json') - - def test_create_network_xml(self): - self._test_create_network(format='xml') - - def test_create_network_alt_tenant(self): - self._test_create_network(tenant=TENANT_2) - - def test_create_network_error_470(self): - self._test_create_network(status=470) - - def test_create_network_error_401(self): - self._test_create_network(status=401) - - def test_create_network_error_400(self): - self._test_create_network(status=400) - - def test_create_network_error_422(self): - self._test_create_network(status=422) - - def test_update_network_json(self): - self._test_update_network(format='json') - - def test_update_network_xml(self): - self._test_update_network(format='xml') - - def test_update_network_alt_tenant(self): - self._test_update_network(tenant=TENANT_2) - - def test_update_network_error_470(self): - self._test_update_network(status=470) - - def test_update_network_error_401(self): - self._test_update_network(status=401) - - def test_update_network_error_400(self): - self._test_update_network(status=400) - - def test_update_network_error_420(self): - self._test_update_network(status=420) - - def test_update_network_error_422(self): - self._test_update_network(status=422) - - def test_delete_network_json(self): - self._test_delete_network(format='json') - - def test_delete_network_xml(self): - self._test_delete_network(format='xml') - - def test_delete_network_alt_tenant(self): - self._test_delete_network(tenant=TENANT_2) - - def test_delete_network_error_470(self): - self._test_delete_network(status=470) - - def test_delete_network_error_401(self): - self._test_delete_network(status=401) - - def test_delete_network_error_420(self): - self._test_delete_network(status=420) - - def test_delete_network_error_421(self): - self._test_delete_network(status=421) - - def test_list_ports_json(self): - self._test_list_ports(format='json') - - def test_list_ports_xml(self): - self._test_list_ports(format='xml') - - def test_list_ports_alt_tenant(self): - self._test_list_ports(tenant=TENANT_2) - - def test_list_ports_error_470(self): - self._test_list_ports(status=470) - - def test_list_ports_error_401(self): - self._test_list_ports(status=401) - - def test_list_ports_error_420(self): - self._test_list_ports(status=420) - - def test_list_ports_details_json(self): - self._test_list_ports_details(format='json') - - def test_list_ports_details_xml(self): - self._test_list_ports_details(format='xml') - - def test_list_ports_details_alt_tenant(self): - self._test_list_ports_details(tenant=TENANT_2) - - def test_list_ports_details_error_470(self): - self._test_list_ports_details(status=470) - - def test_list_ports_details_error_401(self): - self._test_list_ports_details(status=401) - - def test_list_ports_details_error_420(self): - self._test_list_ports_details(status=420) - - def test_show_port_json(self): - self._test_show_port(format='json') - - def test_show_port_xml(self): - self._test_show_port(format='xml') - - def test_show_port_alt_tenant(self): - self._test_show_port(tenant=TENANT_2) - - def test_show_port_error_470(self): - self._test_show_port(status=470) - - def test_show_port_error_401(self): - self._test_show_port(status=401) - - def test_show_port_error_420(self): - self._test_show_port(status=420) - - def test_show_port_error_430(self): - self._test_show_port(status=430) - - def test_show_port_details_json(self): - self._test_show_port_details(format='json') - - def test_show_port_details_xml(self): - self._test_show_port_details(format='xml') - - def test_show_port_details_alt_tenant(self): - self._test_show_port_details(tenant=TENANT_2) - - def test_show_port_details_error_470(self): - self._test_show_port_details(status=470) - - def test_show_port_details_error_401(self): - self._test_show_port_details(status=401) - - def test_show_port_details_error_420(self): - self._test_show_port_details(status=420) - - def test_show_port_details_error_430(self): - self._test_show_port_details(status=430) - - def test_create_port_json(self): - self._test_create_port(format='json') - - def test_create_port_xml(self): - self._test_create_port(format='xml') - - def test_create_port_alt_tenant(self): - self._test_create_port(tenant=TENANT_2) - - def test_create_port_error_470(self): - self._test_create_port(status=470) - - def test_create_port_error_401(self): - self._test_create_port(status=401) - - def test_create_port_error_400(self): - self._test_create_port(status=400) - - def test_create_port_error_420(self): - self._test_create_port(status=420) - - def test_create_port_error_430(self): - self._test_create_port(status=430) - - def test_create_port_error_431(self): - self._test_create_port(status=431) - - def test_delete_port_json(self): - self._test_delete_port(format='json') - - def test_delete_port_xml(self): - self._test_delete_port(format='xml') - - def test_delete_port_alt_tenant(self): - self._test_delete_port(tenant=TENANT_2) - - def test_delete_port_error_470(self): - self._test_delete_port(status=470) - - def test_delete_port_error_401(self): - self._test_delete_port(status=401) - - def test_delete_port_error_420(self): - self._test_delete_port(status=420) - - def test_delete_port_error_430(self): - self._test_delete_port(status=430) - - def test_delete_port_error_432(self): - self._test_delete_port(status=432) - - def test_update_port_json(self): - self._test_update_port(format='json') - - def test_update_port_xml(self): - self._test_update_port(format='xml') - - def test_update_port_alt_tenant(self): - self._test_update_port(tenant=TENANT_2) - - def test_update_port_error_470(self): - self._test_update_port(status=470) - - def test_update_port_error_401(self): - self._test_update_port(status=401) - - def test_update_port_error_400(self): - self._test_update_port(status=400) - - def test_update_port_error_420(self): - self._test_update_port(status=420) - - def test_update_port_error_430(self): - self._test_update_port(status=430) - - def test_update_port_error_431(self): - self._test_update_port(status=431) - - def test_show_port_attachment_json(self): - self._test_show_port_attachment(format='json') - - def test_show_port_attachment_xml(self): - self._test_show_port_attachment(format='xml') - - def test_show_port_attachment_alt_tenant(self): - self._test_show_port_attachment(tenant=TENANT_2) - - def test_show_port_attachment_error_470(self): - self._test_show_port_attachment(status=470) - - def test_show_port_attachment_error_401(self): - self._test_show_port_attachment(status=401) - - def test_show_port_attachment_error_400(self): - self._test_show_port_attachment(status=400) - - def test_show_port_attachment_error_420(self): - self._test_show_port_attachment(status=420) - - def test_show_port_attachment_error_430(self): - self._test_show_port_attachment(status=430) - - def test_attach_resource_json(self): - self._test_attach_resource(format='json') - - def test_attach_resource_xml(self): - self._test_attach_resource(format='xml') - - def test_attach_resource_alt_tenant(self): - self._test_attach_resource(tenant=TENANT_2) - - def test_attach_resource_error_470(self): - self._test_attach_resource(status=470) - - def test_attach_resource_error_401(self): - self._test_attach_resource(status=401) - - def test_attach_resource_error_400(self): - self._test_attach_resource(status=400) - - def test_attach_resource_error_420(self): - self._test_attach_resource(status=420) - - def test_attach_resource_error_430(self): - self._test_attach_resource(status=430) - - def test_attach_resource_error_432(self): - self._test_attach_resource(status=432) - - def test_attach_resource_error_440(self): - self._test_attach_resource(status=440) - - def test_detach_resource_json(self): - self._test_detach_resource(format='json') - - def test_detach_resource_xml(self): - self._test_detach_resource(format='xml') - - def test_detach_resource_alt_tenant(self): - self._test_detach_resource(tenant=TENANT_2) - - def test_detach_resource_error_470(self): - self._test_detach_resource(status=470) - - def test_detach_resource_error_401(self): - self._test_detach_resource(status=401) - - def test_detach_resource_error_420(self): - self._test_detach_resource(status=420) - - def test_detach_resource_error_430(self): - self._test_detach_resource(status=430) - - def test_ssl_certificates(self): - self._test_ssl_certificates() - - def test_connection_retry_failure(self): - self.client = Client(port=55555, tenant=TENANT_1, retries=1, - retry_interval=0) - try: - self.client.list_networks() - except exceptions.ConnectionFailed as exc: - self.assertTrue('Maximum attempts reached' in str(exc)) - else: - self.fail('ConnectionFailed not raised') diff --git a/quantum/common/config.py b/quantum/common/config.py deleted file mode 100644 index 67c831d35..000000000 --- a/quantum/common/config.py +++ /dev/null @@ -1,345 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Nicira Networks, 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. - -""" -Routines for configuring Quantum -""" - -import ConfigParser -import logging -import logging.config -import logging.handlers -import optparse -import os -import re -import sys -import socket - -from paste import deploy - -from quantum.common import flags -from quantum.common import exceptions as exception - -DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" -DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" - -FLAGS = flags.FLAGS -LOG = logging.getLogger('quantum.wsgi') - - -def parse_options(parser, cli_args=None): - """ - Returns the parsed CLI options, command to run and its arguments, merged - with any same-named options found in a configuration file. - - The function returns a tuple of (options, args), where options is a - mapping of option key/str(value) pairs, and args is the set of arguments - (not options) supplied on the command-line. - - The reason that the option values are returned as strings only is that - ConfigParser and paste.deploy only accept string values... - - :param parser: The option parser - :param cli_args: (Optional) Set of arguments to process. If not present, - sys.argv[1:] is used. - :retval tuple of (options, args) - """ - - (options, args) = parser.parse_args(cli_args) - - return (vars(options), args) - - -def add_common_options(parser): - """ - Given a supplied optparse.OptionParser, adds an OptionGroup that - represents all common configuration options. - - :param parser: optparse.OptionParser - """ - help_text = "The following configuration options are common to "\ - "all quantum programs." - - group = optparse.OptionGroup(parser, "Common Options", help_text) - group.add_option('-v', '--verbose', default=False, dest="verbose", - action="store_true", - help="Print more verbose output") - group.add_option('-d', '--debug', default=False, dest="debug", - action="store_true", - help="Print debugging output") - group.add_option('--config-file', default=None, metavar="PATH", - help="Path to the config file to use. When not specified " - "(the default), we generally look at the first " - "argument specified to be a config file, and if " - "that is also missing, we search standard " - "directories for a config file.") - parser.add_option_group(group) - - -def add_log_options(parser): - """ - Given a supplied optparse.OptionParser, adds an OptionGroup that - represents all the configuration options around logging. - - :param parser: optparse.OptionParser - """ - help_text = "The following configuration options are specific to logging "\ - "functionality for this program." - - group = optparse.OptionGroup(parser, "Logging Options", help_text) - group.add_option('--log-config', default=None, metavar="PATH", - help="If this option is specified, the logging " - "configuration file specified is used and overrides " - "any other logging options specified. Please see " - "the Python logging module documentation for " - "details on logging configuration files.") - group.add_option('--log-date-format', metavar="FORMAT", - default=DEFAULT_LOG_DATE_FORMAT, - help="Format string for %(asctime)s in log records. " - "Default: %default") - group.add_option('--use-syslog', default=False, - action="store_true", - help="Output logs to syslog.") - group.add_option('--log-file', default=None, metavar="PATH", - help="(Optional) Name of log file to output to. " - "If not set, logging will go to stdout.") - group.add_option("--log-dir", default=None, - help="(Optional) The directory to keep log files in " - "(will be prepended to --logfile)") - parser.add_option_group(group) - - -def setup_logging(options, conf): - """ - Sets up the logging options for a log with supplied name - - :param options: Mapping of typed option key/values - :param conf: Mapping of untyped key/values from config file - """ - - if options.get('log_config', None): - # Use a logging configuration file for all settings... - if os.path.exists(options['log_config']): - logging.config.fileConfig(options['log_config']) - return - else: - raise RuntimeError("Unable to locate specified logging " - "config file: %s" % options['log_config']) - - # If either the CLI option or the conf value - # is True, we set to True - debug = options.get('debug') or \ - get_option(conf, 'debug', type='bool', default=False) - verbose = options.get('verbose') or \ - get_option(conf, 'verbose', type='bool', default=False) - root_logger = logging.root - if debug: - root_logger.setLevel(logging.DEBUG) - elif verbose: - root_logger.setLevel(logging.INFO) - else: - root_logger.setLevel(logging.WARNING) - - # Set log configuration from options... - # Note that we use a hard-coded log format in the options - # because of Paste.Deploy bug #379 - # http://trac.pythonpaste.org/pythonpaste/ticket/379 - log_format = options.get('log_format', DEFAULT_LOG_FORMAT) - log_date_format = options.get('log_date_format', DEFAULT_LOG_DATE_FORMAT) - formatter = logging.Formatter(log_format, log_date_format) - - syslog = options.get('use_syslog') - if not syslog: - syslog = conf.get('use_syslog') - - if syslog: - SysLogHandler = logging.handlers.SysLogHandler - try: - handler = SysLogHandler(address='/dev/log', - facility=SysLogHandler.LOG_SYSLOG) - except socket.error: - handler = SysLogHandler(address='/var/run/syslog', - facility=SysLogHandler.LOG_SYSLOG) - handler.setFormatter(formatter) - root_logger.addHandler(handler) - - logfile = options.get('log_file') - if not logfile: - logfile = conf.get('log_file') - - if logfile: - logdir = options.get('log_dir') - if not logdir: - logdir = conf.get('log_dir') - if logdir: - logfile = os.path.join(logdir, logfile) - logfile = logging.FileHandler(logfile) - logfile.setFormatter(formatter) - logfile.setFormatter(formatter) - root_logger.addHandler(logfile) - else: - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(formatter) - root_logger.addHandler(handler) - - -def find_config_file(options, args, config_file='quantum.conf'): - """ - Return the first config file found. - - We search for the paste config file in the following order: - * If --config-file option is used, use that - * If args[0] is a file, use that - * Search for the configuration file in standard directories: - * . - * ~.quantum/ - * ~ - * $FLAGS.state_path/etc/quantum - * $FLAGS.state_path/etc - - :retval Full path to config file, or None if no config file found - """ - - fix_path = lambda p: os.path.abspath(os.path.expanduser(p)) - if options.get('config_file'): - if os.path.exists(options['config_file']): - return fix_path(options['config_file']) - elif args: - if os.path.exists(args[0]): - return fix_path(args[0]) - - dir_to_common = os.path.dirname(os.path.abspath(__file__)) - root = os.path.join(dir_to_common, '..', '..', '..', '..') - # Handle standard directory search for the config file - config_file_dirs = [fix_path(os.path.join(os.getcwd(), 'etc')), - fix_path(os.path.join('~', '.quantum-venv', 'etc', - 'quantum')), - fix_path('~'), - os.path.join(FLAGS.state_path, 'etc'), - os.path.join(FLAGS.state_path, 'etc', 'quantum'), - fix_path(os.path.join('~', '.local', - 'etc', 'quantum')), - '/usr/etc/quantum', - '/usr/local/etc/quantum', - '/etc/quantum/', - '/etc'] - - if 'plugin' in options: - config_file_dirs = [os.path.join(x, 'quantum', 'plugins', - options['plugin']) - for x in config_file_dirs] - - if os.path.exists(os.path.join(root, 'plugins')): - plugins = [fix_path(os.path.join(root, 'plugins', p, 'etc')) - for p in os.listdir(os.path.join(root, 'plugins'))] - plugins = [p for p in plugins if os.path.isdir(p)] - config_file_dirs.extend(plugins) - - for cfg_dir in config_file_dirs: - cfg_file = os.path.join(cfg_dir, config_file) - if os.path.exists(cfg_file): - return cfg_file - - -def load_paste_config(app_name, options, args): - """ - Looks for a config file to use for an app and returns the - config file path and a configuration mapping from a paste config file. - - We search for the paste config file in the following order: - * If --config-file option is used, use that - * If args[0] is a file, use that - * Search for quantum.conf in standard directories: - * . - * ~.quantum/ - * ~ - * /etc/quantum - * /etc - - :param app_name: Name of the application to load config for, or None. - None signifies to only load the [DEFAULT] section of - the config file. - :param options: Set of typed options returned from parse_options() - :param args: Command line arguments from argv[1:] - :retval Tuple of (conf_file, conf) - - :raises RuntimeError when config file cannot be located or there was a - problem loading the configuration file. - """ - conf_file = find_config_file(options, args) - if not conf_file: - raise RuntimeError("Unable to locate any configuration file. " - "Cannot load application %s" % app_name) - try: - conf = deploy.appconfig("config:%s" % conf_file, name=app_name) - return conf_file, conf - except Exception, e: - raise RuntimeError("Error trying to load config %s: %s" - % (conf_file, e)) - - -def load_paste_app(app_name, options, args): - """ - Builds and returns a WSGI app from a paste config file. - - We search for the paste config file in the following order: - * If --config-file option is used, use that - * If args[0] is a file, use that - * Search for quantum.conf in standard directories: - * . - * ~.quantum/ - * ~ - * /etc/quantum - * /etc - - :param app_name: Name of the application to load - :param options: Set of typed options returned from parse_options() - :param args: Command line arguments from argv[1:] - - :raises RuntimeError when config file cannot be located or application - cannot be loaded from config file - """ - conf_file, conf = load_paste_config(app_name, options, args) - - try: - app = deploy.loadapp("config:%s" % conf_file, name=app_name) - except (LookupError, ImportError), e: - raise RuntimeError("Unable to load %(app_name)s from " - "configuration file %(conf_file)s." - "\nGot: %(e)r" % locals()) - return conf, app - - -def get_option(options, option, **kwargs): - if option in options: - value = options[option] - type_ = kwargs.get('type', 'str') - if type_ == 'bool': - if hasattr(value, 'lower'): - return value.lower() == 'true' - else: - return value - elif type_ == 'int': - return int(value) - elif type_ == 'float': - return float(value) - else: - return value - elif 'default' in kwargs: - return kwargs['default'] - else: - raise KeyError("option '%s' not found" % option) diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py deleted file mode 100644 index 69b7a3f6f..000000000 --- a/quantum/common/exceptions.py +++ /dev/null @@ -1,212 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Nicira Networks, 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. - -""" -Quantum base exception handling, including decorator for re-raising -Quantum-type exceptions. SHOULD include dedicated exception logging. -""" - -import logging -import gettext - -gettext.install('quantum', unicode=1) - - -class QuantumException(Exception): - """Base Quantum Exception - - Taken from nova.exception.NovaException - To correctly use this class, inherit from it and define - a 'message' property. That message will get printf'd - with the keyword arguments provided to the constructor. - - """ - message = _("An unknown exception occurred.") - - def __init__(self, **kwargs): - try: - self._error_string = self.message % kwargs - - except Exception: - # at least get the core message out if something happened - self._error_string = self.message - - def __str__(self): - return self._error_string - - -class NotFound(QuantumException): - pass - - -class ClassNotFound(NotFound): - message = _("Class %(class_name)s could not be found") - - -class NetworkNotFound(NotFound): - message = _("Network %(net_id)s could not be found") - - -class PortNotFound(NotFound): - message = _("Port %(port_id)s could not be found " \ - "on network %(net_id)s") - - -class StateInvalid(QuantumException): - message = _("Unsupported port state: %(port_state)s") - - -class NetworkInUse(QuantumException): - message = _("Unable to complete operation on network %(net_id)s. " \ - "There is one or more attachments plugged into its ports.") - - -class PortInUse(QuantumException): - message = _("Unable to complete operation on port %(port_id)s " \ - "for network %(net_id)s. The attachment '%(att_id)s" \ - "is plugged into the logical port.") - - -class AlreadyAttached(QuantumException): - message = _("Unable to plug the attachment %(att_id)s into port " \ - "%(port_id)s for network %(net_id)s. The attachment is " \ - "already plugged into port %(att_port_id)s") - - -class QuantumClientException(QuantumException): - - def __init__(self, **kwargs): - message = kwargs.get('message') - if message: - self.message = message - super(QuantumClientException, self).__init__(**kwargs) - - -# NOTE: on the client side, we use different exception types in order -# to allow client library users to handle server exceptions in try...except -# blocks. The actual error message is the one generated on the server side -class NetworkNotFoundClient(QuantumClientException): - pass - - -class PortNotFoundClient(QuantumClientException): - pass - - -class MalformedResponseBody(QuantumException): - message = _("Malformed response body: %(reason)s") - - -class StateInvalidClient(QuantumClientException): - pass - - -class NetworkInUseClient(QuantumClientException): - pass - - -class PortInUseClient(QuantumClientException): - pass - - -class AlreadyAttachedClient(QuantumClientException): - pass - - -class NotAuthorized(QuantumClientException): - pass - - -class QuantumCLIError(QuantumClientException): - """ Exception raised when command line parsing fails """ - pass - - -class ConnectionFailed(QuantumClientException): - message = _("Connection to quantum failed: %(reason)s") - - -class BadInputError(Exception): - """Error resulting from a client sending bad input to a server""" - pass - - -class ProcessExecutionError(IOError): - def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, - description=None): - if description is None: - description = "Unexpected error while running command." - if exit_code is None: - exit_code = '-' - message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( - description, cmd, exit_code, stdout, stderr) - IOError.__init__(self, message) - - -class Error(Exception): - def __init__(self, message=None): - super(Error, self).__init__(message) - - -class ApiError(Error): - def __init__(self, message='Unknown', code='Unknown'): - self.message = message - self.code = code - super(ApiError, self).__init__('%s: %s' % (code, message)) - - -class MalformedRequestBody(QuantumException): - message = _("Malformed request body: %(reason)s") - - -class Duplicate(Error): - pass - - -class NotEmpty(Error): - pass - - -class Invalid(Error): - pass - - -class InvalidContentType(Invalid): - message = _("Invalid content type %(content_type)s.") - - -class MissingArgumentError(Error): - pass - - -class NotImplementedError(Error): - pass - - -def wrap_exception(f): - def _wrap(*args, **kw): - try: - return f(*args, **kw) - except Exception, e: - if not isinstance(e, Error): - #exc_type, exc_value, exc_traceback = sys.exc_info() - logging.exception('Uncaught exception') - #logging.error(traceback.extract_stack(exc_traceback)) - raise Error(str(e)) - raise - _wrap.func_name = f.func_name - return _wrap diff --git a/quantum/common/flags.py b/quantum/common/flags.py deleted file mode 100644 index 16badd332..000000000 --- a/quantum/common/flags.py +++ /dev/null @@ -1,249 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Citrix 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. - -"""Command-line flag library. - -Wraps gflags. -Global flags should be defined here, the rest are defined where they're used. - -""" -import getopt -import gflags -import os -import string -import sys - - -class FlagValues(gflags.FlagValues): - """Extension of gflags.FlagValues that allows undefined and runtime flags. - - Unknown flags will be ignored when parsing the command line, but the - command line will be kept so that it can be replayed if new flags are - defined after the initial parsing. - - """ - - def __init__(self, extra_context=None): - gflags.FlagValues.__init__(self) - self.__dict__['__dirty'] = [] - self.__dict__['__was_already_parsed'] = False - self.__dict__['__stored_argv'] = [] - self.__dict__['__extra_context'] = extra_context - - def __call__(self, argv): - # We're doing some hacky stuff here so that we don't have to copy - # out all the code of the original verbatim and then tweak a few lines. - # We're hijacking the output of getopt so we can still return the - # leftover args at the end - sneaky_unparsed_args = {"value": None} - original_argv = list(argv) - - if self.IsGnuGetOpt(): - orig_getopt = getattr(getopt, 'gnu_getopt') - orig_name = 'gnu_getopt' - else: - orig_getopt = getattr(getopt, 'getopt') - orig_name = 'getopt' - - def _sneaky(*args, **kw): - optlist, unparsed_args = orig_getopt(*args, **kw) - sneaky_unparsed_args['value'] = unparsed_args - return optlist, unparsed_args - - try: - setattr(getopt, orig_name, _sneaky) - args = gflags.FlagValues.__call__(self, argv) - except gflags.UnrecognizedFlagError: - # Undefined args were found, for now we don't care so just - # act like everything went well - # (these three lines are copied pretty much verbatim from the end - # of the __call__ function we are wrapping) - unparsed_args = sneaky_unparsed_args['value'] - if unparsed_args: - if self.IsGnuGetOpt(): - args = argv[:1] + unparsed_args - else: - args = argv[:1] + original_argv[-len(unparsed_args):] - else: - args = argv[:1] - finally: - setattr(getopt, orig_name, orig_getopt) - - # Store the arguments for later, we'll need them for new flags - # added at runtime - self.__dict__['__stored_argv'] = original_argv - self.__dict__['__was_already_parsed'] = True - self.ClearDirty() - return args - - def Reset(self): - gflags.FlagValues.Reset(self) - self.__dict__['__dirty'] = [] - self.__dict__['__was_already_parsed'] = False - self.__dict__['__stored_argv'] = [] - - def SetDirty(self, name): - """Mark a flag as dirty so that accessing it will case a reparse.""" - self.__dict__['__dirty'].append(name) - - def IsDirty(self, name): - return name in self.__dict__['__dirty'] - - def ClearDirty(self): - self.__dict__['__is_dirty'] = [] - - def WasAlreadyParsed(self): - return self.__dict__['__was_already_parsed'] - - def ParseNewFlags(self): - if '__stored_argv' not in self.__dict__: - return - new_flags = FlagValues(self) - for k in self.__dict__['__dirty']: - new_flags[k] = gflags.FlagValues.__getitem__(self, k) - - new_flags(self.__dict__['__stored_argv']) - for k in self.__dict__['__dirty']: - setattr(self, k, getattr(new_flags, k)) - self.ClearDirty() - - def __setitem__(self, name, flag): - gflags.FlagValues.__setitem__(self, name, flag) - if self.WasAlreadyParsed(): - self.SetDirty(name) - - def __getitem__(self, name): - if self.IsDirty(name): - self.ParseNewFlags() - return gflags.FlagValues.__getitem__(self, name) - - def __getattr__(self, name): - if self.IsDirty(name): - self.ParseNewFlags() - val = gflags.FlagValues.__getattr__(self, name) - if type(val) is str: - tmpl = string.Template(val) - context = [self, self.__dict__['__extra_context']] - return tmpl.substitute(StrWrapper(context)) - return val - - -class StrWrapper(object): - """Wrapper around FlagValues objects. - - Wraps FlagValues objects for string.Template so that we're - sure to return strings. - - """ - def __init__(self, context_objs): - self.context_objs = context_objs - - def __getitem__(self, name): - for context in self.context_objs: - val = getattr(context, name, False) - if val: - return str(val) - raise KeyError(name) - - -# Copied from gflags with small mods to get the naming correct. -# Originally gflags checks for the first module that is not gflags that is -# in the call chain, we want to check for the first module that is not gflags -# and not this module. -def _GetCallingModule(): - """Returns the name of the module that's calling into this module. - - We generally use this function to get the name of the module calling a - DEFINE_foo... function. - - """ - # Walk down the stack to find the first globals dict that's not ours. - for depth in range(1, sys.getrecursionlimit()): - if not sys._getframe(depth).f_globals is globals(): - module_name = __GetModuleName(sys._getframe(depth).f_globals) - if module_name == 'gflags': - continue - if module_name is not None: - return module_name - raise AssertionError("No module was found") - - -# Copied from gflags because it is a private function -def __GetModuleName(globals_dict): - """Given a globals dict, returns the name of the module that defines it. - - Args: - globals_dict: A dictionary that should correspond to an environment - providing the values of the globals. - - Returns: - A string (the name of the module) or None (if the module could not - be identified. - - """ - for name, module in sys.modules.iteritems(): - if getattr(module, '__dict__', None) is globals_dict: - if name == '__main__': - return sys.argv[0] - return name - return None - - -def _wrapper(func): - def _wrapped(*args, **kw): - kw.setdefault('flag_values', FLAGS) - func(*args, **kw) - _wrapped.func_name = func.func_name - return _wrapped - - -FLAGS = FlagValues() -gflags.FLAGS = FLAGS -gflags._GetCallingModule = _GetCallingModule - - -DEFINE = _wrapper(gflags.DEFINE) -DEFINE_string = _wrapper(gflags.DEFINE_string) -DEFINE_integer = _wrapper(gflags.DEFINE_integer) -DEFINE_bool = _wrapper(gflags.DEFINE_bool) -DEFINE_boolean = _wrapper(gflags.DEFINE_boolean) -DEFINE_float = _wrapper(gflags.DEFINE_float) -DEFINE_enum = _wrapper(gflags.DEFINE_enum) -DEFINE_list = _wrapper(gflags.DEFINE_list) -DEFINE_spaceseplist = _wrapper(gflags.DEFINE_spaceseplist) -DEFINE_multistring = _wrapper(gflags.DEFINE_multistring) -DEFINE_multi_int = _wrapper(gflags.DEFINE_multi_int) -DEFINE_flag = _wrapper(gflags.DEFINE_flag) -HelpFlag = gflags.HelpFlag -HelpshortFlag = gflags.HelpshortFlag -HelpXMLFlag = gflags.HelpXMLFlag - - -def DECLARE(name, module_string, flag_values=FLAGS): - if module_string not in sys.modules: - __import__(module_string, globals(), locals()) - if name not in flag_values: - raise gflags.UnrecognizedFlag( - "%s not defined by %s" % (name, module_string)) - - -# __GLOBAL FLAGS ONLY__ -# Define any app-specific flags in their own files, docs at: -# http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#a9 - -DEFINE_string('state_path', os.path.join(os.path.dirname(__file__), '../../'), - "Top-level directory for maintaining quantum's state") diff --git a/quantum/common/serializer.py b/quantum/common/serializer.py deleted file mode 100644 index da50c6444..000000000 --- a/quantum/common/serializer.py +++ /dev/null @@ -1,154 +0,0 @@ -from xml.dom import minidom - -from quantum.common import exceptions as exception -from quantum.common import utils - - -class Serializer(object): - """Serializes and deserializes dictionaries to certain MIME types.""" - - def __init__(self, metadata=None, default_xmlns=None): - """Create a serializer based on the given WSGI environment. - - 'metadata' is an optional dict mapping MIME types to information - needed to serialize a dictionary to that type. - - """ - self.metadata = metadata or {} - self.default_xmlns = default_xmlns - - def _get_serialize_handler(self, content_type): - handlers = { - 'application/json': self._to_json, - 'application/xml': self._to_xml, - } - - try: - return handlers[content_type] - except Exception: - raise exception.InvalidContentType(content_type=content_type) - - def serialize(self, data, content_type): - """Serialize a dictionary into the specified content type.""" - return self._get_serialize_handler(content_type)(data) - - def deserialize(self, datastring, content_type): - """Deserialize a string to a dictionary. - - The string must be in the format of a supported MIME type. - - """ - try: - return self.get_deserialize_handler(content_type)(datastring) - except Exception: - raise exception.MalformedResponseBody( - reason="Unable to deserialize response body") - - def get_deserialize_handler(self, content_type): - handlers = { - 'application/json': self._from_json, - 'application/xml': self._from_xml, - } - - try: - return handlers[content_type] - except Exception: - raise exception.InvalidContentType(content_type=content_type) - - def _from_json(self, datastring): - return utils.loads(datastring) - - def _from_xml(self, datastring): - xmldata = self.metadata.get('application/xml', {}) - plurals = set(xmldata.get('plurals', {})) - node = minidom.parseString(datastring).childNodes[0] - return {node.nodeName: self._from_xml_node(node, plurals)} - - def _from_xml_node(self, node, listnames): - """Convert a minidom node to a simple Python type. - - listnames is a collection of names of XML nodes whose subnodes should - be considered list items. - - """ - if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3: - return node.childNodes[0].nodeValue - elif node.nodeName in listnames: - return [self._from_xml_node(n, listnames) - for n in node.childNodes if n.nodeType != node.TEXT_NODE] - else: - result = dict() - for attr in node.attributes.keys(): - result[attr] = node.attributes[attr].nodeValue - for child in node.childNodes: - if child.nodeType != node.TEXT_NODE: - result[child.nodeName] = self._from_xml_node(child, - listnames) - return result - - def _to_json(self, data): - return utils.dumps(data) - - def _to_xml(self, data): - metadata = self.metadata.get('application/xml', {}) - # We expect data to contain a single key which is the XML root. - root_key = data.keys()[0] - doc = minidom.Document() - node = self._to_xml_node(doc, metadata, root_key, data[root_key]) - - xmlns = node.getAttribute('xmlns') - if not xmlns and self.default_xmlns: - node.setAttribute('xmlns', self.default_xmlns) - - return node.toprettyxml(indent='', newl='') - - def _to_xml_node(self, doc, metadata, nodename, data): - """Recursive method to convert data members to XML nodes.""" - result = doc.createElement(nodename) - - # Set the xml namespace if one is specified - # TODO(justinsb): We could also use prefixes on the keys - xmlns = metadata.get('xmlns', None) - if xmlns: - result.setAttribute('xmlns', xmlns) - if type(data) is list: - collections = metadata.get('list_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for item in data: - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(item)) - result.appendChild(node) - return result - singular = metadata.get('plurals', {}).get(nodename, None) - if singular is None: - if nodename.endswith('s'): - singular = nodename[:-1] - else: - singular = 'item' - for item in data: - node = self._to_xml_node(doc, metadata, singular, item) - result.appendChild(node) - elif type(data) is dict: - collections = metadata.get('dict_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for k, v in data.items(): - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(k)) - text = doc.createTextNode(str(v)) - node.appendChild(text) - result.appendChild(node) - return result - attrs = metadata.get('attributes', {}).get(nodename, {}) - for k, v in data.items(): - if k in attrs: - result.setAttribute(k, str(v)) - else: - node = self._to_xml_node(doc, metadata, k, v) - result.appendChild(node) - else: - # Type is atom. - node = doc.createTextNode(str(data)) - result.appendChild(node) - return result diff --git a/quantum/common/test_lib.py b/quantum/common/test_lib.py deleted file mode 100644 index 03578817d..000000000 --- a/quantum/common/test_lib.py +++ /dev/null @@ -1,291 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack, LLC -# 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. - -# Colorizer Code is borrowed from Twisted: -# Copyright (c) 2001-2010 Twisted Matrix Laboratories. -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import gettext -import os -import unittest -import sys -import logging - -from nose import result -from nose import core -from nose import config - - -class _AnsiColorizer(object): - """ - A colorizer is an object that loosely wraps around a stream, allowing - callers to write text to the stream in a particular color. - - Colorizer classes must implement C{supported()} and C{write(text, color)}. - """ - _colors = dict(black=30, red=31, green=32, yellow=33, - blue=34, magenta=35, cyan=36, white=37) - - def __init__(self, stream): - self.stream = stream - - def supported(cls, stream=sys.stdout): - """ - A class method that returns True if the current platform supports - coloring terminal output using this method. Returns False otherwise. - """ - if not stream.isatty(): - return False # auto color only on TTYs - try: - import curses - except ImportError: - return False - else: - try: - try: - return curses.tigetnum("colors") > 2 - except curses.error: - curses.setupterm() - return curses.tigetnum("colors") > 2 - except: - raise - # guess false in case of error - return False - supported = classmethod(supported) - - def write(self, text, color): - """ - Write the given text to the stream in the given color. - - @param text: Text to be written to the stream. - - @param color: A string label for a color. e.g. 'red', 'white'. - """ - color = self._colors[color] - self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) - - -class _Win32Colorizer(object): - """ - See _AnsiColorizer docstring. - """ - def __init__(self, stream): - from win32console import GetStdHandle, STD_OUT_HANDLE, \ - FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \ - FOREGROUND_INTENSITY - red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN, - FOREGROUND_BLUE, FOREGROUND_INTENSITY) - self.stream = stream - self.screenBuffer = GetStdHandle(STD_OUT_HANDLE) - self._colors = { - 'normal': red | green | blue, - 'red': red | bold, - 'green': green | bold, - 'blue': blue | bold, - 'yellow': red | green | bold, - 'magenta': red | blue | bold, - 'cyan': green | blue | bold, - 'white': red | green | blue | bold} - - def supported(cls, stream=sys.stdout): - try: - import win32console - screenBuffer = win32console.GetStdHandle( - win32console.STD_OUT_HANDLE) - except ImportError: - return False - import pywintypes - try: - screenBuffer.SetConsoleTextAttribute( - win32console.FOREGROUND_RED | - win32console.FOREGROUND_GREEN | - win32console.FOREGROUND_BLUE) - except pywintypes.error: - return False - else: - return True - supported = classmethod(supported) - - def write(self, text, color): - color = self._colors[color] - self.screenBuffer.SetConsoleTextAttribute(color) - self.stream.write(text) - self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) - - -class _NullColorizer(object): - """ - See _AnsiColorizer docstring. - """ - def __init__(self, stream): - self.stream = stream - - def supported(cls, stream=sys.stdout): - return True - supported = classmethod(supported) - - def write(self, text, color): - self.stream.write(text) - - -class QuantumTestResult(result.TextTestResult): - def __init__(self, *args, **kw): - result.TextTestResult.__init__(self, *args, **kw) - self._last_case = None - self.colorizer = None - # NOTE(vish, tfukushima): reset stdout for the terminal check - stdout = sys.__stdout__ - sys.stdout = sys.__stdout__ - for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: - if colorizer.supported(): - self.colorizer = colorizer(self.stream) - break - sys.stdout = stdout - - def getDescription(self, test): - return str(test) - - # NOTE(vish, tfukushima): copied from unittest with edit to add color - def addSuccess(self, test): - unittest.TestResult.addSuccess(self, test) - if self.showAll: - self.colorizer.write("OK", 'green') - self.stream.writeln() - elif self.dots: - self.stream.write('.') - self.stream.flush() - - # NOTE(vish, tfukushima): copied from unittest with edit to add color - def addFailure(self, test, err): - unittest.TestResult.addFailure(self, test, err) - if self.showAll: - self.colorizer.write("FAIL", 'red') - self.stream.writeln() - elif self.dots: - self.stream.write('F') - self.stream.flush() - - # NOTE(vish, tfukushima): copied from unittest with edit to add color - def addError(self, test, err): - """Overrides normal addError to add support for errorClasses. - If the exception is a registered class, the error will be added - to the list for that class, not errors. - """ - stream = getattr(self, 'stream', None) - ec, ev, tb = err - try: - exc_info = self._exc_info_to_string(err, test) - except TypeError: - # This is for compatibility with Python 2.3. - exc_info = self._exc_info_to_string(err) - for cls, (storage, label, isfail) in self.errorClasses.items(): - if result.isclass(ec) and issubclass(ec, cls): - if isfail: - test.passwd = False - storage.append((test, exc_info)) - # Might get patched into a streamless result - if stream is not None: - if self.showAll: - message = [label] - detail = result._exception_details(err[1]) - if detail: - message.append(detail) - stream.writeln(": ".join(message)) - elif self.dots: - stream.write(label[:1]) - return - self.errors.append((test, exc_info)) - test.passed = False - if stream is not None: - if self.showAll: - self.colorizer.write("ERROR", 'red') - self.stream.writeln() - elif self.dots: - stream.write('E') - - def startTest(self, test): - unittest.TestResult.startTest(self, test) - current_case = test.test.__class__.__name__ - - if self.showAll: - if current_case != self._last_case: - self.stream.writeln(current_case) - self._last_case = current_case - #NOTE(salvatore-orlando): - #slightly changed in order to print test case class - #together with unit test name - self.stream.write( - ' %s' % str(test.test).ljust(60)) - self.stream.flush() - - -class QuantumTestRunner(core.TextTestRunner): - def _makeResult(self): - return QuantumTestResult(self.stream, - self.descriptions, - self.verbosity, - self.config) - - -def run_tests(c=None): - logger = logging.getLogger() - hdlr = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') - hdlr.setFormatter(formatter) - logger.addHandler(hdlr) - logger.setLevel(logging.DEBUG) - - # NOTE(bgh): I'm not entirely sure why but nose gets confused here when - # calling run_tests from a plugin directory run_tests.py (instead of the - # main run_tests.py). It will call run_tests with no arguments and the - # testing of run_tests will fail (though the plugin tests will pass). For - # now we just return True to let the run_tests test pass. - if not c: - return True - - runner = QuantumTestRunner(stream=c.stream, - verbosity=c.verbosity, - config=c) - return not core.run(config=c, testRunner=runner) - -# describes parameters used by different unit/functional tests -# a plugin-specific testing mechanism should import this dictionary -# and override the values in it if needed (e.g., run_tests.py in -# quantum/plugins/openvswitch/ ) -test_config = { - "plugin_name": "quantum.plugins.sample.SamplePlugin.FakePlugin", - "default_net_op_status": "UP", - "default_port_op_status": "UP", -} diff --git a/quantum/common/utils.py b/quantum/common/utils.py deleted file mode 100644 index acd72b737..000000000 --- a/quantum/common/utils.py +++ /dev/null @@ -1,279 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011, Nicira Networks, 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. -# -# Borrowed from nova code base, more utilities will be added/borrowed as and -# when needed. -# @author: Somik Behera, Nicira Networks, Inc. - -"""Utilities and helper functions.""" - -import ConfigParser -import datetime -import inspect -import logging -import os -import random -import subprocess -import socket -import sys -import base64 -import functools -import json -import re -import string -import struct -import time -import types - -from quantum.common import flags -from quantum.common import exceptions as exception -from quantum.common.exceptions import ProcessExecutionError - - -def env(*vars, **kwargs): - """ - returns the first environment variable set - if none are non-empty, defaults to '' or keyword arg default - """ - for v in vars: - value = os.environ.get(v) - if value: - return value - return kwargs.get('default', '') - - -def import_class(import_str): - """Returns a class from a string including module and class.""" - mod_str, _sep, class_str = import_str.rpartition('.') - try: - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - except (ImportError, ValueError, AttributeError), exc: - print(('Inner Exception: %s'), exc) - raise exception.ClassNotFound(class_name=class_str) - - -def import_object(import_str): - """Returns an object including a module or module and class.""" - try: - __import__(import_str) - return sys.modules[import_str] - except ImportError: - cls = import_class(import_str) - return cls() - - -def to_primitive(value): - if type(value) is type([]) or type(value) is type((None,)): - o = [] - for v in value: - o.append(to_primitive(v)) - return o - elif type(value) is type({}): - o = {} - for k, v in value.iteritems(): - o[k] = to_primitive(v) - return o - elif isinstance(value, datetime.datetime): - return str(value) - elif hasattr(value, 'iteritems'): - return to_primitive(dict(value.iteritems())) - elif hasattr(value, '__iter__'): - return to_primitive(list(value)) - else: - return value - - -def dumps(value): - try: - return json.dumps(value) - except TypeError: - pass - return json.dumps(to_primitive(value)) - - -def loads(s): - return json.loads(s) - -TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" -FLAGS = flags.FLAGS - - -def int_from_bool_as_string(subject): - """ - Interpret a string as a boolean and return either 1 or 0. - - Any string value in: - ('True', 'true', 'On', 'on', '1') - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - return bool_from_string(subject) and 1 or 0 - - -def bool_from_string(subject): - """ - Interpret a string as a boolean. - - Any string value in: - ('True', 'true', 'On', 'on', '1') - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - if type(subject) == type(bool): - return subject - if hasattr(subject, 'startswith'): # str or unicode... - if subject.strip().lower() in ('true', 'on', '1'): - return True - return False - - -def fetchfile(url, target): - logging.debug("Fetching %s" % url) -# c = pycurl.Curl() -# fp = open(target, "wb") -# c.setopt(c.URL, url) -# c.setopt(c.WRITEDATA, fp) -# c.perform() -# c.close() -# fp.close() - execute("curl --fail %s -o %s" % (url, target)) - - -def execute(cmd, process_input=None, addl_env=None, check_exit_code=True): - logging.debug("Running cmd: %s", cmd) - env = os.environ.copy() - if addl_env: - env.update(addl_env) - obj = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) - result = None - if process_input != None: - result = obj.communicate(process_input) - else: - result = obj.communicate() - obj.stdin.close() - if obj.returncode: - logging.debug("Result was %s" % (obj.returncode)) - if check_exit_code and obj.returncode != 0: - (stdout, stderr) = result - raise ProcessExecutionError(exit_code=obj.returncode, - stdout=stdout, - stderr=stderr, - cmd=cmd) - return result - - -def abspath(s): - return os.path.join(os.path.dirname(__file__), s) - - -# TODO(sirp): when/if utils is extracted to common library, we should remove -# the argument's default. -#def default_flagfile(filename='nova.conf'): -def default_flagfile(filename='quantum.conf'): - for arg in sys.argv: - if arg.find('flagfile') != -1: - break - else: - if not os.path.isabs(filename): - # turn relative filename into an absolute path - script_dir = os.path.dirname(inspect.stack()[-1][1]) - filename = os.path.abspath(os.path.join(script_dir, filename)) - if os.path.exists(filename): - sys.argv = \ - sys.argv[:1] + ['--flagfile=%s' % filename] + sys.argv[1:] - - -def debug(arg): - logging.debug('debug in callback: %s', arg) - return arg - - -def runthis(prompt, cmd, check_exit_code=True): - logging.debug("Running %s" % (cmd)) - exit_code = subprocess.call(cmd.split(" ")) - logging.debug(prompt % (exit_code)) - if check_exit_code and exit_code != 0: - raise ProcessExecutionError(exit_code=exit_code, - stdout=None, - stderr=None, - cmd=cmd) - - -def generate_uid(topic, size=8): - return '%s-%s' % (topic, ''.join( - [random.choice('01234567890abcdefghijklmnopqrstuvwxyz') - for x in xrange(size)])) - - -def generate_mac(): - mac = [0x02, 0x16, 0x3e, random.randint(0x00, 0x7f), - random.randint(0x00, 0xff), random.randint(0x00, 0xff)] - return ':'.join(map(lambda x: "%02x" % x, mac)) - - -def last_octet(address): - return int(address.split(".")[-1]) - - -def isotime(at=None): - if not at: - at = datetime.datetime.utcnow() - return at.strftime(TIME_FORMAT) - - -def parse_isotime(timestr): - return datetime.datetime.strptime(timestr, TIME_FORMAT) - - -def get_plugin_from_config(file="config.ini"): - Config = ConfigParser.ConfigParser() - Config.read(file) - return Config.get("PLUGIN", "provider") - - -class LazyPluggable(object): - """A pluggable backend loaded lazily based on some value.""" - - def __init__(self, pivot, **backends): - self.__backends = backends - self.__pivot = pivot - self.__backend = None - - def __get_backend(self): - if not self.__backend: - backend_name = self.__pivot.value - if backend_name not in self.__backends: - raise exception.Error('Invalid backend: %s' % backend_name) - - backend = self.__backends[backend_name] - if type(backend) == type(tuple()): - name = backend[0] - fromlist = backend[1] - else: - name = backend - fromlist = backend - - self.__backend = __import__(name, None, None, fromlist) - logging.info('backend %s', self.__backend) - return self.__backend - - def __getattr__(self, key): - backend = self.__get_backend() - return getattr(backend, key) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..1eabc6f06 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +d2to1>=0.2.10,<0.3 +pbr>=0.5.16,<0.6 +argparse +cliff>=1.4 +httplib2 +iso8601 +prettytable>=0.6,<0.8 +pyparsing>=1.5.6,<2.0 +simplejson diff --git a/setup.cfg b/setup.cfg index f72c0a437..d8224c91f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,36 @@ -[nosetests] -# NOTE(jkoelker) To run the test suite under nose install the following -# coverage http://pypi.python.org/pypi/coverage -# tissue http://pypi.python.org/pypi/tissue (pep8 checker) -# openstack-nose https://github.com/jkoelker/openstack-nose -verbosity=2 -detailed-errors=1 -with-openstack=1 -openstack-red=0.05 -openstack-yellow=0.025 -openstack-show-elapsed=1 +[metadata] +name = python-neutronclient +summary = CLI and Client Library for OpenStack Networking +description-file = + README.rst +author = OpenStack Networking Project +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + Intended Audience :: Developers + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 2.6 +[files] +packages = + neutronclient + +[global] +setup-hooks = + pbr.hooks.setup_hook + +[entry_points] +console_scripts = + neutron = neutronclient.shell:main + +[build_sphinx] +all_files = 1 +build-dir = doc/build +source-dir = doc/source diff --git a/setup.py b/setup.py index 6b3dc8061..80b683420 100644 --- a/setup.py +++ b/setup.py @@ -1,74 +1,23 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Citrix Systems -# All Rights Reserved. +#!/usr/bin/env python +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # -# 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 +# 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 +# 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. - -try: - from setuptools import setup, find_packages -except ImportError: - from ez_setup import use_setuptools - use_setuptools() - from setuptools import setup, find_packages - -import version - -Name = 'python-quantumclient' -Url = "https://launchpad.net/quantum" -Version = version.canonical_version_string() -License = 'Apache License 2.0' -Author = 'Netstack' -AuthorEmail = 'netstack@lists.launchpad.net' -Maintainer = '' -Summary = 'Client functionalities for Quantum' -ShortDescription = Summary -Description = Summary - -requires = [ - 'Paste', - 'PasteDeploy', - 'python-gflags', -] - -EagerResources = [ -] - -ProjectScripts = [ -] - -PackageData = { -} +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +import setuptools -setup( - name=Name, - version=Version, - url=Url, - author=Author, - author_email=AuthorEmail, - description=ShortDescription, - long_description=Description, - license=License, - scripts=ProjectScripts, - install_requires=requires, - include_package_data=False, - packages=["quantum", "quantum.client", "quantum.common"], - package_data=PackageData, - eager_resources=EagerResources, - entry_points={ - 'console_scripts': [ - 'quantum = quantum.client.cli:main' - ] - }, -) +setuptools.setup( + setup_requires=['d2to1>=0.2.10,<0.3', 'pbr>=0.5.10,<0.6'], + d2to1=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..34b353c7c --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,15 @@ +# Install bounded pep8/pyflakes first, then let flake8 install +pep8==1.4.5 +pyflakes==0.7.2 +flake8==2.0 +hacking>=0.5.3,<0.6 + +cliff-tablib>=1.0 +coverage +discover +fixtures>=0.3.12 +mox +python-subunit +sphinx>=1.1.2 +testrepository>=0.0.13 +testtools>=0.9.22 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 000000000..1668497e7 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2013 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 diff --git a/tests/unit/lb/__init__.py b/tests/unit/lb/__init__.py new file mode 100644 index 000000000..1668497e7 --- /dev/null +++ b/tests/unit/lb/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2013 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 diff --git a/tests/unit/lb/test_cli20_healthmonitor.py b/tests/unit/lb/test_cli20_healthmonitor.py new file mode 100644 index 000000000..51bc89429 --- /dev/null +++ b/tests/unit/lb/test_cli20_healthmonitor.py @@ -0,0 +1,219 @@ +# Copyright 2013 Mirantis 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: Ilya Shakhat, Mirantis Inc. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import sys + +import mox + +from neutronclient.neutron.v2_0.lb import healthmonitor +from tests.unit import test_cli20 + + +class CLITestV20LbHealthmonitorJSON(test_cli20.CLITestV20Base): + def test_create_healthmonitor_with_mandatory_params(self): + """lb-healthmonitor-create with mandatory params only.""" + resource = 'health_monitor' + cmd = healthmonitor.CreateHealthMonitor(test_cli20.MyApp(sys.stdout), + None) + admin_state_up = False + delay = '60' + max_retries = '2' + timeout = '10' + type = 'tcp' + tenant_id = 'my-tenant' + my_id = 'my-id' + args = ['--admin-state-down', + '--delay', delay, + '--max-retries', max_retries, + '--timeout', timeout, + '--type', type, + '--tenant-id', tenant_id] + position_names = ['admin_state_up', 'delay', 'max_retries', 'timeout', + 'type', 'tenant_id'] + position_values = [admin_state_up, delay, max_retries, timeout, type, + tenant_id] + self._test_create_resource(resource, cmd, '', my_id, args, + position_names, position_values) + + def test_create_healthmonitor_with_all_params(self): + """lb-healthmonitor-create with all params set.""" + resource = 'health_monitor' + cmd = healthmonitor.CreateHealthMonitor(test_cli20.MyApp(sys.stdout), + None) + admin_state_up = False + delay = '60' + expected_codes = '200-202,204' + http_method = 'HEAD' + max_retries = '2' + timeout = '10' + type = 'tcp' + tenant_id = 'my-tenant' + url_path = '/health' + my_id = 'my-id' + args = ['--admin-state-down', + '--delay', delay, + '--expected-codes', expected_codes, + '--http-method', http_method, + '--max-retries', max_retries, + '--timeout', timeout, + '--type', type, + '--tenant-id', tenant_id, + '--url-path', url_path] + position_names = ['admin_state_up', 'delay', + 'expected_codes', 'http_method', + 'max_retries', 'timeout', + 'type', 'tenant_id', 'url_path'] + position_values = [admin_state_up, delay, + expected_codes, http_method, + max_retries, timeout, + type, tenant_id, url_path] + self._test_create_resource(resource, cmd, '', my_id, args, + position_names, position_values) + + def test_list_healthmonitors(self): + """lb-healthmonitor-list.""" + resources = "health_monitors" + cmd = healthmonitor.ListHealthMonitor(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources(resources, cmd, True) + + def test_list_healthmonitors_pagination(self): + """lb-healthmonitor-list.""" + resources = "health_monitors" + cmd = healthmonitor.ListHealthMonitor(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_healthmonitors_sort(self): + """lb-healthmonitor-list --sort-key name --sort-key id --sort-key asc + --sort-key desc + """ + resources = "health_monitors" + cmd = healthmonitor.ListHealthMonitor(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_healthmonitors_limit(self): + """lb-healthmonitor-list -P.""" + resources = "health_monitors" + cmd = healthmonitor.ListHealthMonitor(test_cli20.MyApp(sys.stdout), + None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_healthmonitor_id(self): + """lb-healthmonitor-show test_id.""" + resource = 'health_monitor' + cmd = healthmonitor.ShowHealthMonitor(test_cli20.MyApp(sys.stdout), + None) + args = ['--fields', 'id', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, args, ['id']) + + def test_show_healthmonitor_id_name(self): + """lb-healthmonitor-show.""" + resource = 'health_monitor' + cmd = healthmonitor.ShowHealthMonitor(test_cli20.MyApp(sys.stdout), + None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id', 'name']) + + def test_update_health_monitor(self): + """lb-healthmonitor-update myid --name myname --tags a b.""" + resource = 'health_monitor' + cmd = healthmonitor.UpdateHealthMonitor(test_cli20.MyApp(sys.stdout), + None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--timeout', '5'], + {'timeout': '5', }) + + def test_delete_healthmonitor(self): + """lb-healthmonitor-delete my-id.""" + resource = 'health_monitor' + cmd = healthmonitor.DeleteHealthMonitor(test_cli20.MyApp(sys.stdout), + None) + my_id = 'my-id' + args = [my_id] + self._test_delete_resource(resource, cmd, my_id, args) + + def test_associate_healthmonitor(self): + cmd = healthmonitor.AssociateHealthMonitor( + test_cli20.MyApp(sys.stdout), + None) + resource = 'health_monitor' + health_monitor_id = 'hm-id' + pool_id = 'p_id' + args = [health_monitor_id, pool_id] + + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + + body = {resource: {'id': health_monitor_id}} + result = {resource: {'id': health_monitor_id}, } + result_str = self.client.serialize(result) + + path = getattr(self.client, + "associate_pool_health_monitors_path") % pool_id + return_tup = (test_cli20.MyResp(200), result_str) + self.client.httpclient.request( + test_cli20.end_url(path), 'POST', + body=test_cli20.MyComparator(body, self.client), + headers=mox.ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(return_tup) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser('test_' + resource) + parsed_args = cmd_parser.parse_args(args) + cmd.run(parsed_args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def test_disassociate_healthmonitor(self): + cmd = healthmonitor.DisassociateHealthMonitor( + test_cli20.MyApp(sys.stdout), + None) + resource = 'health_monitor' + health_monitor_id = 'hm-id' + pool_id = 'p_id' + args = [health_monitor_id, pool_id] + + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + + path = (getattr(self.client, + "disassociate_pool_health_monitors_path") % + {'pool': pool_id, 'health_monitor': health_monitor_id}) + return_tup = (test_cli20.MyResp(204), None) + self.client.httpclient.request( + test_cli20.end_url(path), 'DELETE', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(return_tup) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser('test_' + resource) + parsed_args = cmd_parser.parse_args(args) + cmd.run(parsed_args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + + +class CLITestV20LbHealthmonitorXML(CLITestV20LbHealthmonitorJSON): + format = 'xml' diff --git a/tests/unit/lb/test_cli20_member.py b/tests/unit/lb/test_cli20_member.py new file mode 100644 index 000000000..b280320cb --- /dev/null +++ b/tests/unit/lb/test_cli20_member.py @@ -0,0 +1,134 @@ +# Copyright 2013 Mirantis 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: Ilya Shakhat, Mirantis Inc. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import sys + +from neutronclient.neutron.v2_0.lb import member +from tests.unit import test_cli20 + + +class CLITestV20LbMemberJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20LbMemberJSON, self).setUp(plurals={'tags': 'tag'}) + + def test_create_member(self): + """lb-member-create with mandatory params only.""" + resource = 'member' + cmd = member.CreateMember(test_cli20.MyApp(sys.stdout), None) + address = '10.0.0.1' + port = '8080' + tenant_id = 'my-tenant' + my_id = 'my-id' + pool_id = 'pool-id' + args = ['--address', address, '--protocol-port', port, + '--tenant-id', tenant_id, pool_id] + position_names = ['address', 'protocol_port', 'tenant_id', 'pool_id', + 'admin_state_up'] + position_values = [address, port, tenant_id, pool_id, True] + self._test_create_resource(resource, cmd, None, my_id, args, + position_names, position_values, + admin_state_up=None) + + def test_create_member_all_params(self): + """lb-member-create with all available params.""" + resource = 'member' + cmd = member.CreateMember(test_cli20.MyApp(sys.stdout), None) + address = '10.0.0.1' + admin_state_up = False + port = '8080' + weight = '1' + tenant_id = 'my-tenant' + my_id = 'my-id' + pool_id = 'pool-id' + args = ['--address', address, '--admin-state-down', + '--protocol-port', port, '--weight', weight, + '--tenant-id', tenant_id, pool_id] + position_names = [ + 'address', 'admin_state_up', 'protocol_port', 'weight', + 'tenant_id', 'pool_id' + ] + position_values = [address, admin_state_up, port, weight, + tenant_id, pool_id] + self._test_create_resource(resource, cmd, None, my_id, args, + position_names, position_values, + admin_state_up=None) + + def test_list_members(self): + """lb-member-list.""" + resources = "members" + cmd = member.ListMember(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_members_pagination(self): + """lb-member-list.""" + resources = "members" + cmd = member.ListMember(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_members_sort(self): + """lb-member-list --sort-key name --sort-key id --sort-key asc + --sort-key desc + """ + resources = "members" + cmd = member.ListMember(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_members_limit(self): + """lb-member-list -P.""" + resources = "members" + cmd = member.ListMember(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_member_id(self): + """lb-member-show test_id.""" + resource = 'member' + cmd = member.ShowMember(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, args, ['id']) + + def test_show_member_id_name(self): + """lb-member-show.""" + resource = 'member' + cmd = member.ShowMember(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id', 'name']) + + def test_update_member(self): + """lb-member-update myid --name myname --tags a b.""" + resource = 'member' + cmd = member.UpdateMember(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'myname', + '--tags', 'a', 'b'], + {'name': 'myname', 'tags': ['a', 'b'], }) + + def test_delete_member(self): + """lb-member-delete my-id.""" + resource = 'member' + cmd = member.DeleteMember(test_cli20.MyApp(sys.stdout), None) + my_id = 'my-id' + args = [my_id] + self._test_delete_resource(resource, cmd, my_id, args) + + +class CLITestV20LbMemberXML(CLITestV20LbMemberJSON): + format = 'xml' diff --git a/tests/unit/lb/test_cli20_pool.py b/tests/unit/lb/test_cli20_pool.py new file mode 100644 index 000000000..f54d71dbe --- /dev/null +++ b/tests/unit/lb/test_cli20_pool.py @@ -0,0 +1,171 @@ +# Copyright 2013 Mirantis 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: Ilya Shakhat, Mirantis Inc. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import sys + +import mox + +from neutronclient.neutron.v2_0.lb import pool +from tests.unit import test_cli20 + + +class CLITestV20LbPoolJSON(test_cli20.CLITestV20Base): + + def test_create_pool_with_mandatory_params(self): + """lb-pool-create with mandatory params only.""" + resource = 'pool' + cmd = pool.CreatePool(test_cli20.MyApp(sys.stdout), None) + name = 'my-name' + lb_method = 'round-robin' + protocol = 'http' + subnet_id = 'subnet-id' + tenant_id = 'my-tenant' + my_id = 'my-id' + args = ['--lb-method', lb_method, + '--name', name, + '--protocol', protocol, + '--subnet-id', subnet_id, + '--tenant-id', tenant_id] + position_names = ['admin_state_up', 'lb_method', 'name', + 'protocol', 'subnet_id', 'tenant_id'] + position_values = [True, lb_method, name, + protocol, subnet_id, tenant_id] + self._test_create_resource(resource, cmd, name, my_id, args, + position_names, position_values) + + def test_create_pool_with_all_params(self): + """lb-pool-create with all params set.""" + resource = 'pool' + cmd = pool.CreatePool(test_cli20.MyApp(sys.stdout), None) + name = 'my-name' + description = 'my-desc' + lb_method = 'round-robin' + protocol = 'http' + subnet_id = 'subnet-id' + tenant_id = 'my-tenant' + my_id = 'my-id' + args = ['--admin-state-down', + '--description', description, + '--lb-method', lb_method, + '--name', name, + '--protocol', protocol, + '--subnet-id', subnet_id, + '--tenant-id', tenant_id] + position_names = ['admin_state_up', 'description', 'lb_method', 'name', + 'protocol', 'subnet_id', 'tenant_id'] + position_values = [False, description, lb_method, name, + protocol, subnet_id, tenant_id] + self._test_create_resource(resource, cmd, name, my_id, args, + position_names, position_values) + + def test_list_pools(self): + """lb-pool-list.""" + resources = "pools" + cmd = pool.ListPool(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_pools_pagination(self): + """lb-pool-list.""" + resources = "pools" + cmd = pool.ListPool(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_pools_sort(self): + """lb-pool-list --sort-key name --sort-key id --sort-key asc + --sort-key desc + """ + resources = "pools" + cmd = pool.ListPool(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_pools_limit(self): + """lb-pool-list -P.""" + resources = "pools" + cmd = pool.ListPool(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_pool_id(self): + """lb-pool-show test_id.""" + resource = 'pool' + cmd = pool.ShowPool(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, args, ['id']) + + def test_show_pool_id_name(self): + """lb-pool-show.""" + resource = 'pool' + cmd = pool.ShowPool(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id', 'name']) + + def test_update_pool(self): + """lb-pool-update myid --name newname --tags a b.""" + resource = 'pool' + cmd = pool.UpdatePool(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'newname'], + {'name': 'newname', }) + + def test_delete_pool(self): + """lb-pool-delete my-id.""" + resource = 'pool' + cmd = pool.DeletePool(test_cli20.MyApp(sys.stdout), None) + my_id = 'my-id' + args = [my_id] + self._test_delete_resource(resource, cmd, my_id, args) + + def test_retrieve_pool_stats(self): + """lb-pool-stats test_id.""" + resource = 'pool' + cmd = pool.RetrievePoolStats(test_cli20.MyApp(sys.stdout), None) + my_id = self.test_id + fields = ['bytes_in', 'bytes_out'] + args = ['--fields', 'bytes_in', '--fields', 'bytes_out', my_id] + + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + query = "&".join(["fields=%s" % field for field in fields]) + expected_res = {'stats': {'bytes_in': '1234', 'bytes_out': '4321'}} + resstr = self.client.serialize(expected_res) + path = getattr(self.client, "pool_path_stats") + return_tup = (test_cli20.MyResp(200), resstr) + self.client.httpclient.request( + test_cli20.end_url(path % my_id, query), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(return_tup) + self.mox.ReplayAll() + + cmd_parser = cmd.get_parser("test_" + resource) + parsed_args = cmd_parser.parse_args(args) + cmd.run(parsed_args) + + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertTrue('bytes_in' in _str) + self.assertTrue('bytes_out' in _str) + + +class CLITestV20LbPoolXML(CLITestV20LbPoolJSON): + format = 'xml' diff --git a/tests/unit/lb/test_cli20_vip.py b/tests/unit/lb/test_cli20_vip.py new file mode 100644 index 000000000..f34efd6e9 --- /dev/null +++ b/tests/unit/lb/test_cli20_vip.py @@ -0,0 +1,214 @@ +# Copyright 2013 Mirantis 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: Ilya Shakhat, Mirantis Inc. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import sys + +from neutronclient.neutron.v2_0.lb import vip +from tests.unit import test_cli20 + + +class CLITestV20LbVipJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20LbVipJSON, self).setUp(plurals={'tags': 'tag'}) + + def test_create_vip_with_mandatory_params(self): + """lb-vip-create with all mandatory params.""" + resource = 'vip' + cmd = vip.CreateVip(test_cli20.MyApp(sys.stdout), None) + pool_id = 'my-pool-id' + name = 'my-name' + subnet_id = 'subnet-id' + protocol_port = '1000' + protocol = 'tcp' + tenant_id = 'my-tenant' + my_id = 'my-id' + args = ['--name', name, + '--protocol-port', protocol_port, + '--protocol', protocol, + '--subnet-id', subnet_id, + '--tenant-id', tenant_id, + pool_id] + position_names = ['pool_id', 'name', 'protocol_port', 'protocol', + 'subnet_id', 'tenant_id'] + position_values = [pool_id, name, protocol_port, protocol, + subnet_id, tenant_id] + self._test_create_resource(resource, cmd, name, my_id, args, + position_names, position_values, + admin_state_up=True) + + def test_create_vip_with_all_params(self): + """lb-vip-create with all params.""" + resource = 'vip' + cmd = vip.CreateVip(test_cli20.MyApp(sys.stdout), None) + pool_id = 'my-pool-id' + name = 'my-name' + description = 'my-desc' + address = '10.0.0.2' + admin_state = False + connection_limit = '1000' + subnet_id = 'subnet-id' + protocol_port = '80' + protocol = 'tcp' + tenant_id = 'my-tenant' + my_id = 'my-id' + args = ['--name', name, + '--description', description, + '--address', address, + '--admin-state-down', + '--connection-limit', connection_limit, + '--protocol-port', protocol_port, + '--protocol', protocol, + '--subnet-id', subnet_id, + '--tenant-id', tenant_id, + pool_id] + position_names = ['pool_id', 'name', 'description', 'address', + 'admin_state_up', 'connection_limit', + 'protocol_port', 'protocol', 'subnet_id', + 'tenant_id'] + position_values = [pool_id, name, description, address, + admin_state, connection_limit, protocol_port, + protocol, subnet_id, + tenant_id] + self._test_create_resource(resource, cmd, name, my_id, args, + position_names, position_values) + + def test_create_vip_with_session_persistence_params(self): + """lb-vip-create with mandatory and session-persistence params.""" + resource = 'vip' + cmd = vip.CreateVip(test_cli20.MyApp(sys.stdout), None) + pool_id = 'my-pool-id' + name = 'my-name' + subnet_id = 'subnet-id' + protocol_port = '1000' + protocol = 'tcp' + tenant_id = 'my-tenant' + my_id = 'my-id' + args = ['--name', name, + '--protocol-port', protocol_port, + '--protocol', protocol, + '--subnet-id', subnet_id, + '--tenant-id', tenant_id, + pool_id, + '--session-persistence', 'type=dict', + 'type=cookie,cookie_name=pie', + '--optional-param', 'any'] + position_names = ['pool_id', 'name', 'protocol_port', 'protocol', + 'subnet_id', 'tenant_id', 'optional_param'] + position_values = [pool_id, name, protocol_port, protocol, + subnet_id, tenant_id, 'any'] + extra_body = { + 'session_persistence': { + 'type': 'cookie', + 'cookie_name': 'pie', + }, + } + self._test_create_resource(resource, cmd, name, my_id, args, + position_names, position_values, + admin_state_up=True, extra_body=extra_body) + + def test_list_vips(self): + """lb-vip-list.""" + resources = "vips" + cmd = vip.ListVip(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_vips_pagination(self): + """lb-vip-list.""" + resources = "vips" + cmd = vip.ListVip(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_vips_sort(self): + """lb-vip-list --sort-key name --sort-key id --sort-key asc + --sort-key desc + """ + resources = "vips" + cmd = vip.ListVip(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_vips_limit(self): + """lb-vip-list -P.""" + resources = "vips" + cmd = vip.ListVip(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_vip_id(self): + """lb-vip-show test_id.""" + resource = 'vip' + cmd = vip.ShowVip(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, args, ['id']) + + def test_show_vip_id_name(self): + """lb-vip-show.""" + resource = 'vip' + cmd = vip.ShowVip(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id', 'name']) + + def test_update_vip(self): + """lb-vip-update myid --name myname --tags a b.""" + resource = 'vip' + cmd = vip.UpdateVip(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'myname', + '--tags', 'a', 'b'], + {'name': 'myname', 'tags': ['a', 'b'], }) + + def test_update_vip_with_session_persistence(self): + resource = 'vip' + cmd = vip.UpdateVip(test_cli20.MyApp(sys.stdout), None) + body = { + 'session_persistence': { + 'type': 'source', + }, + } + args = ['myid', '--session-persistence', 'type=dict', + 'type=source'] + self._test_update_resource(resource, cmd, 'myid', args, body) + + def test_update_vip_with_session_persistence_and_name(self): + resource = 'vip' + cmd = vip.UpdateVip(test_cli20.MyApp(sys.stdout), None) + body = { + 'name': 'newname', + 'session_persistence': { + 'type': 'cookie', + 'cookie_name': 'pie', + }, + } + args = ['myid', '--name', 'newname', + '--session-persistence', 'type=dict', + 'type=cookie,cookie_name=pie'] + self._test_update_resource(resource, cmd, 'myid', args, body) + + def test_delete_vip(self): + """lb-vip-delete my-id.""" + resource = 'vip' + cmd = vip.DeleteVip(test_cli20.MyApp(sys.stdout), None) + my_id = 'my-id' + args = [my_id] + self._test_delete_resource(resource, cmd, my_id, args) + + +class CLITestV20LbVipXML(CLITestV20LbVipJSON): + format = 'xml' diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py new file mode 100644 index 000000000..3929ad4b8 --- /dev/null +++ b/tests/unit/test_auth.py @@ -0,0 +1,359 @@ +# Copyright 2012 NEC Corporation +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import copy +import httplib2 +import json +import uuid + +import mox +import testtools + +from neutronclient import client +from neutronclient.common import exceptions + + +USERNAME = 'testuser' +TENANT_NAME = 'testtenant' +TENANT_ID = 'testtenantid' +PASSWORD = 'password' +AUTH_URL = 'authurl' +ENDPOINT_URL = 'localurl' +TOKEN = 'tokentoken' +REGION = 'RegionTest' + +KS_TOKEN_RESULT = { + 'access': { + 'token': {'id': TOKEN, + 'expires': '2012-08-11T07:49:01Z', + 'tenant': {'id': str(uuid.uuid1())}}, + 'user': {'id': str(uuid.uuid1())}, + 'serviceCatalog': [ + {'endpoints_links': [], + 'endpoints': [{'adminURL': ENDPOINT_URL, + 'internalURL': ENDPOINT_URL, + 'publicURL': ENDPOINT_URL, + 'region': REGION}], + 'type': 'network', + 'name': 'Neutron Service'} + ] + } +} + +ENDPOINTS_RESULT = { + 'endpoints': [{ + 'type': 'network', + 'name': 'Neutron Service', + 'region': REGION, + 'adminURL': ENDPOINT_URL, + 'internalURL': ENDPOINT_URL, + 'publicURL': ENDPOINT_URL + }] +} + + +class CLITestAuthKeystone(testtools.TestCase): + + # Auth Body expected when using tenant name + auth_type = 'tenantName' + + def setUp(self): + """Prepare the test environment.""" + super(CLITestAuthKeystone, self).setUp() + self.mox = mox.Mox() + self.client = client.HTTPClient(username=USERNAME, + tenant_name=TENANT_NAME, + password=PASSWORD, + auth_url=AUTH_URL, + region_name=REGION) + self.addCleanup(self.mox.VerifyAll) + self.addCleanup(self.mox.UnsetStubs) + + def test_get_token(self): + self.mox.StubOutWithMock(self.client, "request") + + res200 = self.mox.CreateMock(httplib2.Response) + res200.status = 200 + + self.client.request( + AUTH_URL + '/tokens', 'POST', + body=mox.StrContains(self.auth_type), headers=mox.IsA(dict) + ).AndReturn((res200, json.dumps(KS_TOKEN_RESULT))) + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res200, '')) + self.mox.ReplayAll() + + self.client.do_request('/resource', 'GET') + self.assertEqual(self.client.endpoint_url, ENDPOINT_URL) + self.assertEqual(self.client.auth_token, TOKEN) + + def test_refresh_token(self): + self.mox.StubOutWithMock(self.client, "request") + + self.client.auth_token = TOKEN + self.client.endpoint_url = ENDPOINT_URL + + res200 = self.mox.CreateMock(httplib2.Response) + res200.status = 200 + res401 = self.mox.CreateMock(httplib2.Response) + res401.status = 401 + + # If a token is expired, neutron server retruns 401 + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res401, '')) + self.client.request( + AUTH_URL + '/tokens', 'POST', + body=mox.IsA(str), headers=mox.IsA(dict) + ).AndReturn((res200, json.dumps(KS_TOKEN_RESULT))) + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res200, '')) + self.mox.ReplayAll() + self.client.do_request('/resource', 'GET') + + def test_get_endpoint_url(self): + self.mox.StubOutWithMock(self.client, "request") + + self.client.auth_token = TOKEN + + res200 = self.mox.CreateMock(httplib2.Response) + res200.status = 200 + + self.client.request( + mox.StrContains(AUTH_URL + '/tokens/%s/endpoints' % TOKEN), 'GET', + headers=mox.IsA(dict) + ).AndReturn((res200, json.dumps(ENDPOINTS_RESULT))) + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res200, '')) + self.mox.ReplayAll() + self.client.do_request('/resource', 'GET') + + def test_get_endpoint_url_other(self): + self.client = client.HTTPClient( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, endpoint_type='otherURL') + self.mox.StubOutWithMock(self.client, "request") + + self.client.auth_token = TOKEN + + res200 = self.mox.CreateMock(httplib2.Response) + res200.status = 200 + + self.client.request( + mox.StrContains(AUTH_URL + '/tokens/%s/endpoints' % TOKEN), 'GET', + headers=mox.IsA(dict) + ).AndReturn((res200, json.dumps(ENDPOINTS_RESULT))) + self.mox.ReplayAll() + self.assertRaises(exceptions.EndpointTypeNotFound, + self.client.do_request, + '/resource', + 'GET') + + def test_get_endpoint_url_failed(self): + self.mox.StubOutWithMock(self.client, "request") + + self.client.auth_token = TOKEN + + res200 = self.mox.CreateMock(httplib2.Response) + res200.status = 200 + res401 = self.mox.CreateMock(httplib2.Response) + res401.status = 401 + + self.client.request( + mox.StrContains(AUTH_URL + '/tokens/%s/endpoints' % TOKEN), 'GET', + headers=mox.IsA(dict) + ).AndReturn((res401, '')) + self.client.request( + AUTH_URL + '/tokens', 'POST', + body=mox.IsA(str), headers=mox.IsA(dict) + ).AndReturn((res200, json.dumps(KS_TOKEN_RESULT))) + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res200, '')) + self.mox.ReplayAll() + self.client.do_request('/resource', 'GET') + + def test_url_for(self): + resources = copy.deepcopy(KS_TOKEN_RESULT) + + endpoints = resources['access']['serviceCatalog'][0]['endpoints'][0] + endpoints['publicURL'] = 'public' + endpoints['internalURL'] = 'internal' + endpoints['adminURL'] = 'admin' + catalog = client.ServiceCatalog(resources) + + # endpoint_type not specified + url = catalog.url_for(attr='region', + filter_value=REGION) + self.assertEqual('public', url) + + # endpoint type specified (3 cases) + url = catalog.url_for(attr='region', + filter_value=REGION, + endpoint_type='adminURL') + self.assertEqual('admin', url) + + url = catalog.url_for(attr='region', + filter_value=REGION, + endpoint_type='publicURL') + self.assertEqual('public', url) + + url = catalog.url_for(attr='region', + filter_value=REGION, + endpoint_type='internalURL') + self.assertEqual('internal', url) + + # endpoint_type requested does not exist. + self.assertRaises(exceptions.EndpointTypeNotFound, + catalog.url_for, + attr='region', + filter_value=REGION, + endpoint_type='privateURL') + + # Test scenario with url_for when the service catalog only has publicURL. + def test_url_for_only_public_url(self): + resources = copy.deepcopy(KS_TOKEN_RESULT) + catalog = client.ServiceCatalog(resources) + + # Remove endpoints from the catalog. + endpoints = resources['access']['serviceCatalog'][0]['endpoints'][0] + del endpoints['internalURL'] + del endpoints['adminURL'] + endpoints['publicURL'] = 'public' + + # Use publicURL when specified explicitly. + url = catalog.url_for(attr='region', + filter_value=REGION, + endpoint_type='publicURL') + self.assertEqual('public', url) + + # Use publicURL when specified explicitly. + url = catalog.url_for(attr='region', + filter_value=REGION) + self.assertEqual('public', url) + + # Test scenario with url_for when the service catalog only has adminURL. + def test_url_for_only_admin_url(self): + resources = copy.deepcopy(KS_TOKEN_RESULT) + catalog = client.ServiceCatalog(resources) + endpoints = resources['access']['serviceCatalog'][0]['endpoints'][0] + del endpoints['internalURL'] + del endpoints['publicURL'] + endpoints['adminURL'] = 'admin' + + # Use publicURL when specified explicitly. + url = catalog.url_for(attr='region', + filter_value=REGION, + endpoint_type='adminURL') + self.assertEqual('admin', url) + + # But not when nothing is specified. + self.assertRaises(exceptions.EndpointTypeNotFound, + catalog.url_for, + attr='region', + filter_value=REGION) + + def test_endpoint_type(self): + resources = copy.deepcopy(KS_TOKEN_RESULT) + endpoints = resources['access']['serviceCatalog'][0]['endpoints'][0] + endpoints['internalURL'] = 'internal' + endpoints['adminURL'] = 'admin' + endpoints['publicURL'] = 'public' + + # Test default behavior is to choose public. + self.client = client.HTTPClient( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION) + + self.client._extract_service_catalog(resources) + self.assertEqual(self.client.endpoint_url, 'public') + + # Test admin url + self.client = client.HTTPClient( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, endpoint_type='adminURL') + + self.client._extract_service_catalog(resources) + self.assertEqual(self.client.endpoint_url, 'admin') + + # Test public url + self.client = client.HTTPClient( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, endpoint_type='publicURL') + + self.client._extract_service_catalog(resources) + self.assertEqual(self.client.endpoint_url, 'public') + + # Test internal url + self.client = client.HTTPClient( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, endpoint_type='internalURL') + + self.client._extract_service_catalog(resources) + self.assertEqual(self.client.endpoint_url, 'internal') + + # Test url that isn't found in the service catalog + self.client = client.HTTPClient( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, endpoint_type='privateURL') + + self.assertRaises(exceptions.EndpointTypeNotFound, + self.client._extract_service_catalog, + resources) + + +class CLITestAuthKeystoneWithId(CLITestAuthKeystone): + + # Auth Body expected when using tenant Id + auth_type = 'tenantId' + + def setUp(self): + """Prepare the test environment.""" + super(CLITestAuthKeystone, self).setUp() + self.mox = mox.Mox() + self.client = client.HTTPClient(username=USERNAME, + tenant_id=TENANT_ID, + password=PASSWORD, + auth_url=AUTH_URL, + region_name=REGION) + self.addCleanup(self.mox.VerifyAll) + self.addCleanup(self.mox.UnsetStubs) + + +class CLITestAuthKeystoneWithIdandName(CLITestAuthKeystone): + + # Auth Body expected when using tenant Id + auth_type = 'tenantId' + + def setUp(self): + """Prepare the test environment.""" + super(CLITestAuthKeystone, self).setUp() + self.mox = mox.Mox() + self.client = client.HTTPClient(username=USERNAME, + tenant_id=TENANT_ID, + tenant_name=TENANT_NAME, + password=PASSWORD, + auth_url=AUTH_URL, + region_name=REGION) diff --git a/tests/unit/test_casual_args.py b/tests/unit/test_casual_args.py new file mode 100644 index 000000000..1722e683d --- /dev/null +++ b/tests/unit/test_casual_args.py @@ -0,0 +1,99 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import testtools + +from neutronclient.common import exceptions +from neutronclient.neutron import v2_0 as neutronV20 + + +class CLITestArgs(testtools.TestCase): + + def test_empty(self): + _mydict = neutronV20.parse_args_to_dict([]) + self.assertEqual({}, _mydict) + + def test_default_bool(self): + _specs = ['--my_bool', '--arg1', 'value1'] + _mydict = neutronV20.parse_args_to_dict(_specs) + self.assertTrue(_mydict['my_bool']) + + def test_bool_true(self): + _specs = ['--my-bool', 'type=bool', 'true', '--arg1', 'value1'] + _mydict = neutronV20.parse_args_to_dict(_specs) + self.assertTrue(_mydict['my_bool']) + + def test_bool_false(self): + _specs = ['--my_bool', 'type=bool', 'false', '--arg1', 'value1'] + _mydict = neutronV20.parse_args_to_dict(_specs) + self.assertFalse(_mydict['my_bool']) + + def test_nargs(self): + _specs = ['--tag', 'x', 'y', '--arg1', 'value1'] + _mydict = neutronV20.parse_args_to_dict(_specs) + self.assertTrue('x' in _mydict['tag']) + self.assertTrue('y' in _mydict['tag']) + + def test_badarg(self): + _specs = ['--tag=t', 'x', 'y', '--arg1', 'value1'] + self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + + def test_badarg_with_minus(self): + _specs = ['--arg1', 'value1', '-D'] + self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + + def test_goodarg_with_minus_number(self): + _specs = ['--arg1', 'value1', '-1', '-1.0'] + _mydict = neutronV20.parse_args_to_dict(_specs) + self.assertEqual(['value1', '-1', '-1.0'], + _mydict['arg1']) + + def test_badarg_duplicate(self): + _specs = ['--tag=t', '--arg1', 'value1', '--arg1', 'value1'] + self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + + def test_badarg_early_type_specification(self): + _specs = ['type=dict', 'key=value'] + self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + + def test_arg(self): + _specs = ['--tag=t', '--arg1', 'value1'] + self.assertEqual('value1', + neutronV20.parse_args_to_dict(_specs)['arg1']) + + def test_dict_arg(self): + _specs = ['--tag=t', '--arg1', 'type=dict', 'key1=value1,key2=value2'] + arg1 = neutronV20.parse_args_to_dict(_specs)['arg1'] + self.assertEqual('value1', arg1['key1']) + self.assertEqual('value2', arg1['key2']) + + def test_dict_arg_with_attribute_named_type(self): + _specs = ['--tag=t', '--arg1', 'type=dict', 'type=value1,key2=value2'] + arg1 = neutronV20.parse_args_to_dict(_specs)['arg1'] + self.assertEqual('value1', arg1['type']) + self.assertEqual('value2', arg1['key2']) + + def test_list_of_dict_arg(self): + _specs = ['--tag=t', '--arg1', 'type=dict', + 'list=true', 'key1=value1,key2=value2'] + arg1 = neutronV20.parse_args_to_dict(_specs)['arg1'] + self.assertEqual('value1', arg1[0]['key1']) + self.assertEqual('value2', arg1[0]['key2']) diff --git a/tests/unit/test_cli20.py b/tests/unit/test_cli20.py new file mode 100644 index 000000000..b1b649093 --- /dev/null +++ b/tests/unit/test_cli20.py @@ -0,0 +1,504 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import urllib + +import fixtures +import mox +import testtools + +from neutronclient.common import constants +from neutronclient import shell +from neutronclient.v2_0 import client + + +API_VERSION = "2.0" +FORMAT = 'json' +TOKEN = 'testtoken' +ENDURL = 'localurl' + + +class FakeStdout: + + def __init__(self): + self.content = [] + + def write(self, text): + self.content.append(text) + + def make_string(self): + result = '' + for line in self.content: + result = result + line + return result + + +class MyResp(object): + def __init__(self, status): + self.status = status + + +class MyApp(object): + def __init__(self, _stdout): + self.stdout = _stdout + + +def end_url(path, query=None, format=FORMAT): + _url_str = ENDURL + "/v" + API_VERSION + path + "." + format + return query and _url_str + "?" + query or _url_str + + +class MyUrlComparator(mox.Comparator): + def __init__(self, lhs, client): + self.lhs = lhs + self.client = client + + def equals(self, rhs): + return str(self) == rhs + + def __str__(self): + if self.client and self.client.format != FORMAT: + lhs_parts = self.lhs.split("?", 1) + if len(lhs_parts) == 2: + lhs = ("%s.%s?%s" % (lhs_parts[0][:-4], + self.client.format, + lhs_parts[1])) + else: + lhs = ("%s.%s" % (lhs_parts[0][:-4], + self.client.format)) + return lhs + return self.lhs + + def __repr__(self): + return str(self) + + +class MyComparator(mox.Comparator): + def __init__(self, lhs, client): + self.lhs = lhs + self.client = client + + def _com_dict(self, lhs, rhs): + if len(lhs) != len(rhs): + return False + for key, value in lhs.iteritems(): + if key not in rhs: + return False + rhs_value = rhs[key] + if not self._com(value, rhs_value): + return False + return True + + def _com_list(self, lhs, rhs): + if len(lhs) != len(rhs): + return False + for lhs_value in lhs: + if lhs_value not in rhs: + return False + return True + + def _com(self, lhs, rhs): + if lhs is None: + return rhs is None + if isinstance(lhs, dict): + if not isinstance(rhs, dict): + return False + return self._com_dict(lhs, rhs) + if isinstance(lhs, list): + if not isinstance(rhs, list): + return False + return self._com_list(lhs, rhs) + if isinstance(lhs, tuple): + if not isinstance(rhs, tuple): + return False + return self._com_list(lhs, rhs) + return lhs == rhs + + def equals(self, rhs): + if self.client: + rhs = self.client.deserialize(rhs, 200) + return self._com(self.lhs, rhs) + + def __repr__(self): + if self.client: + return self.client.serialize(self.lhs) + return str(self.lhs) + + +class CLITestV20Base(testtools.TestCase): + + format = 'json' + test_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + id_field = 'id' + + def _find_resourceid(self, client, resource, name_or_id): + return name_or_id + + def _get_attr_metadata(self): + return self.metadata + client.Client.EXTED_PLURALS.update(constants.PLURALS) + client.Client.EXTED_PLURALS.update({'tags': 'tag'}) + return {'plurals': client.Client.EXTED_PLURALS, + 'xmlns': constants.XML_NS_V20, + constants.EXT_NS: {'prefix': 'http://xxxx.yy.com'}} + + def setUp(self, plurals={}): + """Prepare the test environment.""" + super(CLITestV20Base, self).setUp() + client.Client.EXTED_PLURALS.update(constants.PLURALS) + client.Client.EXTED_PLURALS.update(plurals) + self.metadata = {'plurals': client.Client.EXTED_PLURALS, + 'xmlns': constants.XML_NS_V20, + constants.EXT_NS: {'prefix': + 'http://xxxx.yy.com'}} + self.mox = mox.Mox() + self.endurl = ENDURL + self.fake_stdout = FakeStdout() + self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.fake_stdout)) + self.useFixture(fixtures.MonkeyPatch( + 'neutronclient.neutron.v2_0.find_resourceid_by_name_or_id', + self._find_resourceid)) + self.useFixture(fixtures.MonkeyPatch( + 'neutronclient.v2_0.client.Client.get_attr_metadata', + self._get_attr_metadata)) + self.client = client.Client(token=TOKEN, endpoint_url=self.endurl) + + def _test_create_resource(self, resource, cmd, + name, myid, args, + position_names, position_values, tenant_id=None, + tags=None, admin_state_up=True, shared=False, + extra_body=None, **kwargs): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + non_admin_status_resources = ['subnet', 'floatingip', 'security_group', + 'security_group_rule', 'qos_queue', + 'network_gateway'] + if (resource in non_admin_status_resources): + body = {resource: {}, } + else: + body = {resource: {'admin_state_up': admin_state_up, }, } + if tenant_id: + body[resource].update({'tenant_id': tenant_id}) + if tags: + body[resource].update({'tags': tags}) + if shared: + body[resource].update({'shared': shared}) + if extra_body: + body[resource].update(extra_body) + body[resource].update(kwargs) + + for i in xrange(len(position_names)): + body[resource].update({position_names[i]: position_values[i]}) + ress = {resource: + {self.id_field: myid}, } + if name: + ress[resource].update({'name': name}) + self.client.format = self.format + resstr = self.client.serialize(ress) + # url method body + path = getattr(self.client, resource + "s_path") + self.client.httpclient.request( + end_url(path, format=self.format), 'POST', + body=MyComparator(body, self.client), + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser('create_' + resource) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertTrue(myid in _str) + if name: + self.assertTrue(name in _str) + + def _test_list_columns(self, cmd, resources_collection, + resources_out, args=['-f', 'json']): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + self.client.format = self.format + resstr = self.client.serialize(resources_out) + + path = getattr(self.client, resources_collection + "_path") + self.client.httpclient.request( + end_url(path, format=self.format), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources_collection) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_list_resources(self, resources, cmd, detail=False, tags=[], + fields_1=[], fields_2=[], page_size=None, + sort_key=[], sort_dir=[], response_contents=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + if response_contents is None: + contents = [{self.id_field: 'myid1', }, + {self.id_field: 'myid2', }, ] + else: + contents = response_contents + reses = {resources: contents} + self.client.format = self.format + resstr = self.client.serialize(reses) + # url method body + query = "" + args = detail and ['-D', ] or [] + args.extend(['--request-format', self.format]) + if fields_1: + for field in fields_1: + args.append('--fields') + args.append(field) + + if tags: + args.append('--') + args.append("--tag") + for tag in tags: + args.append(tag) + if isinstance(tag, unicode): + tag = urllib.quote(tag.encode('utf-8')) + if query: + query += "&tag=" + tag + else: + query = "tag=" + tag + if (not tags) and fields_2: + args.append('--') + if fields_2: + args.append("--fields") + for field in fields_2: + args.append(field) + if detail: + query = query and query + '&verbose=True' or 'verbose=True' + fields_1.extend(fields_2) + for field in fields_1: + if query: + query += "&fields=" + field + else: + query = "fields=" + field + if page_size: + args.append("--page-size") + args.append(str(page_size)) + if query: + query += "&limit=%s" % page_size + else: + query = "limit=%s" % page_size + if sort_key: + for key in sort_key: + args.append('--sort-key') + args.append(key) + if query: + query += '&' + query += 'sort_key=%s' % key + if sort_dir: + len_diff = len(sort_key) - len(sort_dir) + if len_diff > 0: + sort_dir += ['asc'] * len_diff + elif len_diff < 0: + sort_dir = sort_dir[:len(sort_key)] + for dir in sort_dir: + args.append('--sort-dir') + args.append(dir) + if query: + query += '&' + query += 'sort_dir=%s' % dir + path = getattr(self.client, resources + "_path") + self.client.httpclient.request( + MyUrlComparator(end_url(path, query, format=self.format), + self.client), + 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + if response_contents is None: + self.assertTrue('myid1' in _str) + return _str + + def _test_list_resources_with_pagination(self, resources, cmd): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + path = getattr(self.client, resources + "_path") + fake_query = "marker=myid2&limit=2" + reses1 = {resources: [{'id': 'myid1', }, + {'id': 'myid2', }], + '%s_links' % resources: [{'href': end_url(path, fake_query), + 'rel': 'next'}]} + reses2 = {resources: [{'id': 'myid3', }, + {'id': 'myid4', }]} + self.client.format = self.format + resstr1 = self.client.serialize(reses1) + resstr2 = self.client.serialize(reses2) + self.client.httpclient.request( + end_url(path, "", format=self.format), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr1)) + self.client.httpclient.request( + end_url(path, fake_query, format=self.format), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr2)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + args = ['--request-format', self.format] + shell.run_command(cmd, cmd_parser, args) + #parsed_args = cmd_parser.parse_args("") + #cmd.run(parsed_args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_update_resource(self, resource, cmd, myid, args, extrafields): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + body = {resource: extrafields} + path = getattr(self.client, resource + "_path") + self.client.httpclient.request( + MyUrlComparator(end_url(path % myid, format=self.format), + self.client), + 'PUT', + body=MyComparator(body, self.client), + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), None)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("update_" + resource) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertTrue(myid in _str) + + def _test_show_resource(self, resource, cmd, myid, args, fields=[]): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + query = "&".join(["fields=%s" % field for field in fields]) + expected_res = {resource: + {self.id_field: myid, + 'name': 'myname', }, } + self.client.format = self.format + resstr = self.client.serialize(expected_res) + path = getattr(self.client, resource + "_path") + self.client.httpclient.request( + end_url(path % myid, query, format=self.format), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("show_" + resource) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertTrue(myid in _str) + self.assertTrue('myname' in _str) + + def _test_delete_resource(self, resource, cmd, myid, args): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + path = getattr(self.client, resource + "_path") + self.client.httpclient.request( + end_url(path % myid, format=self.format), 'DELETE', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), None)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("delete_" + resource) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertTrue(myid in _str) + + def _test_update_resource_action(self, resource, cmd, myid, action, args, + body, retval=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + path = getattr(self.client, resource + "_path") + path_action = '%s/%s' % (myid, action) + self.client.httpclient.request( + end_url(path % path_action, format=self.format), 'PUT', + body=MyComparator(body, self.client), + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), retval)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("delete_" + resource) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertTrue(myid in _str) + + +class ClientV2UnicodeTestJson(CLITestV20Base): + def test_do_request(self): + self.client.format = self.format + self.mox.StubOutWithMock(self.client.httpclient, "request") + unicode_text = u'\u7f51\u7edc' + # url with unicode + action = u'/test' + expected_action = action.encode('utf-8') + # query string with unicode + params = {'test': unicode_text} + expect_query = urllib.urlencode({'test': + unicode_text.encode('utf-8')}) + # request body with unicode + body = params + expect_body = self.client.serialize(body) + # headers with unicode + self.client.httpclient.auth_token = unicode_text + expected_auth_token = unicode_text.encode('utf-8') + + self.client.httpclient.request( + end_url(expected_action, query=expect_query, format=self.format), + 'PUT', body=expect_body, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', + expected_auth_token)).AndReturn((MyResp(200), expect_body)) + + self.mox.ReplayAll() + res_body = self.client.do_request('PUT', action, body=body, + params=params) + self.mox.VerifyAll() + self.mox.UnsetStubs() + + # test response with unicode + self.assertEqual(res_body, body) + + +class ClientV2UnicodeTestXML(ClientV2UnicodeTestJson): + format = 'xml' diff --git a/tests/unit/test_cli20_extensions.py b/tests/unit/test_cli20_extensions.py new file mode 100644 index 000000000..16664aca6 --- /dev/null +++ b/tests/unit/test_cli20_extensions.py @@ -0,0 +1,49 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 NEC Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +from neutronclient.neutron.v2_0.extension import ListExt +from neutronclient.neutron.v2_0.extension import ShowExt +from tests.unit.test_cli20 import CLITestV20Base +from tests.unit.test_cli20 import MyApp + + +class CLITestV20Extension(CLITestV20Base): + id_field = 'alias' + + def test_list_extensions(self): + resources = 'extensions' + cmd = ListExt(MyApp(sys.stdout), None) + contents = [{'alias': 'ext1', 'name': 'name1', 'other': 'other1'}, + {'alias': 'ext2', 'name': 'name2', 'other': 'other2'}] + ret = self._test_list_resources(resources, cmd, + response_contents=contents) + ret_words = set(ret.split()) + # Check only the default columns are shown. + self.assertTrue('name' in ret_words) + self.assertTrue('alias' in ret_words) + self.assertFalse('other' in ret_words) + + def test_show_extension(self): + # -F option does not work for ext-show at the moment, so -F option + # is not passed in the commandline args as other tests do. + resource = 'extension' + cmd = ShowExt(MyApp(sys.stdout), None) + args = [self.test_id] + ext_alias = self.test_id + self._test_show_resource(resource, cmd, ext_alias, args, fields=[]) diff --git a/tests/unit/test_cli20_floatingips.py b/tests/unit/test_cli20_floatingips.py new file mode 100644 index 000000000..8e7e5882f --- /dev/null +++ b/tests/unit/test_cli20_floatingips.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Red Hat +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +from neutronclient.neutron.v2_0 import floatingip as fip +from tests.unit import test_cli20 + + +class CLITestV20FloatingIpsJSON(test_cli20.CLITestV20Base): + def test_create_floatingip(self): + """Create floatingip: fip1.""" + resource = 'floatingip' + cmd = fip.CreateFloatingIP(test_cli20.MyApp(sys.stdout), None) + name = 'fip1' + myid = 'myid' + args = [name] + position_names = ['floating_network_id'] + position_values = [name] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_floatingip_and_port(self): + """Create floatingip: fip1.""" + resource = 'floatingip' + cmd = fip.CreateFloatingIP(test_cli20.MyApp(sys.stdout), None) + name = 'fip1' + myid = 'myid' + pid = 'mypid' + args = [name, '--port_id', pid] + position_names = ['floating_network_id', 'port_id'] + position_values = [name, pid] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + # Test dashed options + args = [name, '--port-id', pid] + position_names = ['floating_network_id', 'port_id'] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_floatingip_and_port_and_address(self): + """Create floatingip: fip1 with a given port and address.""" + resource = 'floatingip' + cmd = fip.CreateFloatingIP(test_cli20.MyApp(sys.stdout), None) + name = 'fip1' + myid = 'myid' + pid = 'mypid' + addr = '10.0.0.99' + args = [name, '--port_id', pid, '--fixed_ip_address', addr] + position_names = ['floating_network_id', 'port_id', 'fixed_ip_address'] + position_values = [name, pid, addr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + # Test dashed options + args = [name, '--port-id', pid, '--fixed-ip-address', addr] + position_names = ['floating_network_id', 'port_id', 'fixed_ip_address'] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_list_floatingips(self): + """list floatingips: -D.""" + resources = 'floatingips' + cmd = fip.ListFloatingIP(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_floatingips_pagination(self): + resources = 'floatingips' + cmd = fip.ListFloatingIP(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_floatingips_sort(self): + """list floatingips: --sort-key name --sort-key id --sort-key asc + --sort-key desc + """ + resources = 'floatingips' + cmd = fip.ListFloatingIP(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_floatingips_limit(self): + """list floatingips: -P.""" + resources = 'floatingips' + cmd = fip.ListFloatingIP(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_delete_floatingip(self): + """Delete floatingip: fip1.""" + resource = 'floatingip' + cmd = fip.DeleteFloatingIP(test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + args = [myid] + self._test_delete_resource(resource, cmd, myid, args) + + def test_show_floatingip(self): + """Show floatingip: --fields id.""" + resource = 'floatingip' + cmd = fip.ShowFloatingIP(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id']) + + def test_disassociate_ip(self): + """Disassociate floating IP: myid.""" + resource = 'floatingip' + cmd = fip.DisassociateFloatingIP(test_cli20.MyApp(sys.stdout), None) + args = ['myid'] + self._test_update_resource(resource, cmd, 'myid', + args, {"port_id": None} + ) + + def test_associate_ip(self): + """Associate floating IP: myid portid.""" + resource = 'floatingip' + cmd = fip.AssociateFloatingIP(test_cli20.MyApp(sys.stdout), None) + args = ['myid', 'portid'] + self._test_update_resource(resource, cmd, 'myid', + args, {"port_id": "portid"} + ) + + +class CLITestV20FloatingIpsXML(CLITestV20FloatingIpsJSON): + format = 'xml' diff --git a/tests/unit/test_cli20_network.py b/tests/unit/test_cli20_network.py new file mode 100644 index 000000000..4fc8235bd --- /dev/null +++ b/tests/unit/test_cli20_network.py @@ -0,0 +1,533 @@ +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import sys + +import mox + +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron.v2_0 import network +from neutronclient import shell +from tests.unit import test_cli20 + + +class CLITestV20NetworkJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20NetworkJSON, self).setUp(plurals={'tags': 'tag'}) + + def test_create_network(self): + """Create net: myname.""" + resource = 'network' + cmd = network.CreateNetwork(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + args = [name, ] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_network_with_unicode(self): + """Create net: u'\u7f51\u7edc'.""" + resource = 'network' + cmd = network.CreateNetwork(test_cli20.MyApp(sys.stdout), None) + name = u'\u7f51\u7edc' + myid = 'myid' + args = [name, ] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_network_tenant(self): + """Create net: --tenant_id tenantid myname.""" + resource = 'network' + cmd = network.CreateNetwork(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + args = ['--tenant_id', 'tenantid', name] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + # Test dashed options + args = ['--tenant-id', 'tenantid', name] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_network_tags(self): + """Create net: myname --tags a b.""" + resource = 'network' + cmd = network.CreateNetwork(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + args = [name, '--tags', 'a', 'b'] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tags=['a', 'b']) + + def test_create_network_state(self): + """Create net: --admin_state_down myname.""" + resource = 'network' + cmd = network.CreateNetwork(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + args = ['--admin_state_down', name, ] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + admin_state_up=False) + + # Test dashed options + args = ['--admin-state-down', name, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + admin_state_up=False) + + def test_list_nets_empty_with_column(self): + resources = "networks" + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + self.mox.StubOutWithMock(network.ListNetwork, "extend_list") + network.ListNetwork.extend_list(mox.IsA(list), mox.IgnoreArg()) + cmd.get_client().MultipleTimes().AndReturn(self.client) + reses = {resources: []} + resstr = self.client.serialize(reses) + # url method body + query = "id=myfakeid" + args = ['-c', 'id', '--', '--id', 'myfakeid'] + path = getattr(self.client, resources + "_path") + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', + test_cli20.TOKEN)).AndReturn( + (test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertEquals('\n', _str) + + def _test_list_networks(self, cmd, detail=False, tags=[], + fields_1=[], fields_2=[], page_size=None, + sort_key=[], sort_dir=[]): + resources = "networks" + self.mox.StubOutWithMock(network.ListNetwork, "extend_list") + network.ListNetwork.extend_list(mox.IsA(list), mox.IgnoreArg()) + self._test_list_resources(resources, cmd, detail, tags, + fields_1, fields_2, page_size=page_size, + sort_key=sort_key, sort_dir=sort_dir) + + def test_list_nets_pagination(self): + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(network.ListNetwork, "extend_list") + network.ListNetwork.extend_list(mox.IsA(list), mox.IgnoreArg()) + self._test_list_resources_with_pagination("networks", cmd) + + def test_list_nets_sort(self): + """list nets: --sort-key name --sort-key id --sort-dir asc + --sort-dir desc + """ + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_networks(cmd, sort_key=['name', 'id'], + sort_dir=['asc', 'desc']) + + def test_list_nets_sort_with_keys_more_than_dirs(self): + """list nets: --sort-key name --sort-key id --sort-dir desc + """ + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_networks(cmd, sort_key=['name', 'id'], + sort_dir=['desc']) + + def test_list_nets_sort_with_dirs_more_than_keys(self): + """list nets: --sort-key name --sort-dir desc --sort-dir asc + """ + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_networks(cmd, sort_key=['name'], + sort_dir=['desc', 'asc']) + + def test_list_nets_limit(self): + """list nets: -P.""" + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_networks(cmd, page_size=1000) + + def test_list_nets_detail(self): + """list nets: -D.""" + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_networks(cmd, True) + + def test_list_nets_tags(self): + """List nets: -- --tags a b.""" + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_networks(cmd, tags=['a', 'b']) + + def test_list_nets_tags_with_unicode(self): + """List nets: -- --tags u'\u7f51\u7edc'.""" + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_networks(cmd, tags=[u'\u7f51\u7edc']) + + def test_list_nets_detail_tags(self): + """List nets: -D -- --tags a b.""" + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_networks(cmd, detail=True, tags=['a', 'b']) + + def _test_list_nets_extend_subnets(self, data, expected): + def setup_list_stub(resources, data, query): + reses = {resources: data} + resstr = self.client.serialize(reses) + resp = (test_cli20.MyResp(200), resstr) + path = getattr(self.client, resources + '_path') + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(resp) + + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(cmd, 'get_client') + self.mox.StubOutWithMock(self.client.httpclient, 'request') + cmd.get_client().AndReturn(self.client) + setup_list_stub('networks', data, '') + cmd.get_client().AndReturn(self.client) + filters = '' + for n in data: + for s in n['subnets']: + filters = filters + "&id=%s" % s + setup_list_stub('subnets', + [{'id': 'mysubid1', 'cidr': '192.168.1.0/24'}, + {'id': 'mysubid2', 'cidr': '172.16.0.0/24'}, + {'id': 'mysubid3', 'cidr': '10.1.1.0/24'}], + query='fields=id&fields=cidr' + filters) + self.mox.ReplayAll() + + args = [] + cmd_parser = cmd.get_parser('list_networks') + parsed_args = cmd_parser.parse_args(args) + result = cmd.get_data(parsed_args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _result = [x for x in result[1]] + self.assertEqual(len(_result), len(expected)) + for res, exp in zip(_result, expected): + self.assertEqual(len(res), len(exp)) + for a, b in zip(res, exp): + self.assertEqual(a, b) + + def test_list_nets_extend_subnets(self): + data = [{'id': 'netid1', 'name': 'net1', 'subnets': ['mysubid1']}, + {'id': 'netid2', 'name': 'net2', 'subnets': ['mysubid2', + 'mysubid3']}] + # id, name, subnets + expected = [('netid1', 'net1', 'mysubid1 192.168.1.0/24'), + ('netid2', 'net2', + 'mysubid2 172.16.0.0/24\nmysubid3 10.1.1.0/24')] + self._test_list_nets_extend_subnets(data, expected) + + def test_list_nets_extend_subnets_no_subnet(self): + data = [{'id': 'netid1', 'name': 'net1', 'subnets': ['mysubid1']}, + {'id': 'netid2', 'name': 'net2', 'subnets': ['mysubid4']}] + # id, name, subnets + expected = [('netid1', 'net1', 'mysubid1 192.168.1.0/24'), + ('netid2', 'net2', 'mysubid4 ')] + self._test_list_nets_extend_subnets(data, expected) + + def test_list_nets_fields(self): + """List nets: --fields a --fields b -- --fields c d.""" + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_networks(cmd, + fields_1=['a', 'b'], fields_2=['c', 'd']) + + def _test_list_nets_columns(self, cmd, returned_body, + args=['-f', 'json']): + resources = 'networks' + self.mox.StubOutWithMock(network.ListNetwork, "extend_list") + network.ListNetwork.extend_list(mox.IsA(list), mox.IgnoreArg()) + self._test_list_columns(cmd, resources, returned_body, args=args) + + def test_list_nets_defined_column(self): + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + returned_body = {"networks": [{"name": "buildname3", + "id": "id3", + "tenant_id": "tenant_3", + "subnets": []}]} + self._test_list_nets_columns(cmd, returned_body, + args=['-f', 'json', '-c', 'id']) + _str = self.fake_stdout.make_string() + returned_networks = utils.loads(_str) + self.assertEquals(1, len(returned_networks)) + net = returned_networks[0] + self.assertEquals(1, len(net)) + self.assertEquals("id", net.keys()[0]) + + def test_list_nets_with_default_column(self): + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + returned_body = {"networks": [{"name": "buildname3", + "id": "id3", + "tenant_id": "tenant_3", + "subnets": []}]} + self._test_list_nets_columns(cmd, returned_body) + _str = self.fake_stdout.make_string() + returned_networks = utils.loads(_str) + self.assertEquals(1, len(returned_networks)) + net = returned_networks[0] + self.assertEquals(3, len(net)) + self.assertEquals(0, len(set(net) ^ set(cmd.list_columns))) + + def test_list_external_nets_empty_with_column(self): + resources = "networks" + cmd = network.ListExternalNetwork(test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + self.mox.StubOutWithMock(network.ListNetwork, "extend_list") + network.ListNetwork.extend_list(mox.IsA(list), mox.IgnoreArg()) + cmd.get_client().MultipleTimes().AndReturn(self.client) + reses = {resources: []} + resstr = self.client.serialize(reses) + # url method body + query = "router%3Aexternal=True&id=myfakeid" + args = ['-c', 'id', '--', '--id', 'myfakeid'] + path = getattr(self.client, resources + "_path") + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', + test_cli20.TOKEN)).AndReturn( + (test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertEquals('\n', _str) + + def _test_list_external_nets(self, resources, cmd, + detail=False, tags=[], + fields_1=[], fields_2=[]): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + self.mox.StubOutWithMock(network.ListNetwork, "extend_list") + network.ListNetwork.extend_list(mox.IsA(list), mox.IgnoreArg()) + cmd.get_client().MultipleTimes().AndReturn(self.client) + reses = {resources: [{'id': 'myid1', }, + {'id': 'myid2', }, ], } + + resstr = self.client.serialize(reses) + + # url method body + query = "" + args = detail and ['-D', ] or [] + if fields_1: + for field in fields_1: + args.append('--fields') + args.append(field) + if tags: + args.append('--') + args.append("--tag") + for tag in tags: + args.append(tag) + if (not tags) and fields_2: + args.append('--') + if fields_2: + args.append("--fields") + for field in fields_2: + args.append(field) + fields_1.extend(fields_2) + for field in fields_1: + if query: + query += "&fields=" + field + else: + query = "fields=" + field + if query: + query += '&router%3Aexternal=True' + else: + query += 'router%3Aexternal=True' + for tag in tags: + if query: + query += "&tag=" + tag + else: + query = "tag=" + tag + if detail: + query = query and query + '&verbose=True' or 'verbose=True' + path = getattr(self.client, resources + "_path") + + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=mox.ContainsKeyValue('X-Auth-Token', test_cli20.TOKEN) + ).AndReturn((test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + + self.assertTrue('myid1' in _str) + + def test_list_external_nets_detail(self): + """list external nets: -D.""" + resources = "networks" + cmd = network.ListExternalNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_external_nets(resources, cmd, True) + + def test_list_external_nets_tags(self): + """List external nets: -- --tags a b.""" + resources = "networks" + cmd = network.ListExternalNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_external_nets(resources, + cmd, tags=['a', 'b']) + + def test_list_external_nets_detail_tags(self): + """List external nets: -D -- --tags a b.""" + resources = "networks" + cmd = network.ListExternalNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_external_nets(resources, cmd, + detail=True, tags=['a', 'b']) + + def test_list_externel_nets_fields(self): + """List external nets: --fields a --fields b -- --fields c d.""" + resources = "networks" + cmd = network.ListExternalNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_list_external_nets(resources, cmd, + fields_1=['a', 'b'], + fields_2=['c', 'd']) + + def test_update_network_exception(self): + """Update net: myid.""" + resource = 'network' + cmd = network.UpdateNetwork(test_cli20.MyApp(sys.stdout), None) + self.assertRaises(exceptions.CommandError, self._test_update_resource, + resource, cmd, 'myid', ['myid'], {}) + + def test_update_network(self): + """Update net: myid --name myname --tags a b.""" + resource = 'network' + cmd = network.UpdateNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'myname', + '--tags', 'a', 'b'], + {'name': 'myname', 'tags': ['a', 'b'], } + ) + + def test_update_network_with_unicode(self): + """Update net: myid --name u'\u7f51\u7edc' --tags a b.""" + resource = 'network' + cmd = network.UpdateNetwork(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', u'\u7f51\u7edc', + '--tags', 'a', 'b'], + {'name': u'\u7f51\u7edc', + 'tags': ['a', 'b'], } + ) + + def test_show_network(self): + """Show net: --fields id --fields name myid.""" + resource = 'network' + cmd = network.ShowNetwork(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, args, + ['id', 'name']) + + def test_delete_network(self): + """Delete net: myid.""" + resource = 'network' + cmd = network.DeleteNetwork(test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + args = [myid] + self._test_delete_resource(resource, cmd, myid, args) + + def _test_extend_list(self, mox_calls): + data = [{'id': 'netid%d' % i, 'name': 'net%d' % i, + 'subnets': ['mysubid%d' % i]} + for i in range(0, 10)] + self.mox.StubOutWithMock(self.client.httpclient, "request") + path = getattr(self.client, 'subnets_path') + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(cmd, "get_client") + cmd.get_client().MultipleTimes().AndReturn(self.client) + mox_calls(path, data) + self.mox.ReplayAll() + known_args, _vs = cmd.get_parser('create_subnets').parse_known_args() + cmd.extend_list(data, known_args) + self.mox.VerifyAll() + + def _build_test_data(self, data): + subnet_ids = [] + response = [] + filters = "" + for n in data: + if 'subnets' in n: + subnet_ids.extend(n['subnets']) + for subnet_id in n['subnets']: + filters = "%s&id=%s" % (filters, subnet_id) + response.append({'id': subnet_id, + 'cidr': '192.168.0.0/16'}) + resp_str = self.client.serialize({'subnets': response}) + resp = (test_cli20.MyResp(200), resp_str) + return filters, resp + + def test_extend_list(self): + def mox_calls(path, data): + filters, response = self._build_test_data(data) + self.client.httpclient.request( + test_cli20.end_url(path, 'fields=id&fields=cidr' + filters), + 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(response) + + self._test_extend_list(mox_calls) + + def test_extend_list_exceed_max_uri_len(self): + def mox_calls(path, data): + sub_data_lists = [data[:len(data) - 1], data[len(data) - 1:]] + filters, response = self._build_test_data(data) + + # 1 char of extra URI len will cause a split in 2 requests + self.mox.StubOutWithMock(self.client, "_check_uri_length") + self.client._check_uri_length(mox.IgnoreArg()).AndRaise( + exceptions.RequestURITooLong(excess=1)) + + for data in sub_data_lists: + filters, response = self._build_test_data(data) + self.client._check_uri_length(mox.IgnoreArg()).AndReturn(None) + self.client.httpclient.request( + test_cli20.end_url(path, + 'fields=id&fields=cidr%s' % filters), + 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(response) + + self._test_extend_list(mox_calls) + + +class CLITestV20NetworkXML(CLITestV20NetworkJSON): + format = 'xml' diff --git a/tests/unit/test_cli20_nvp_queue.py b/tests/unit/test_cli20_nvp_queue.py new file mode 100644 index 000000000..f74552639 --- /dev/null +++ b/tests/unit/test_cli20_nvp_queue.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Nicira 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. + +import sys + +from neutronclient.neutron.v2_0 import nvp_qos_queue as qos +from tests.unit import test_cli20 + + +class CLITestV20NvpQosQueueJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20NvpQosQueueJSON, self).setUp( + plurals={'qos_queues': 'qos_queue'}) + + def test_create_qos_queue(self): + """Create a qos queue.""" + resource = 'qos_queue' + cmd = qos.CreateQoSQueue( + test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + name = 'my_queue' + default = False + args = ['--default', default, name] + position_names = ['name', 'default'] + position_values = [name, default] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_qos_queue_all_values(self): + """Create a qos queue.""" + resource = 'qos_queue' + cmd = qos.CreateQoSQueue( + test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + name = 'my_queue' + default = False + min = '10' + max = '40' + qos_marking = 'untrusted' + dscp = '0' + args = ['--default', default, '--min', min, '--max', max, + '--qos-marking', qos_marking, '--dscp', dscp, name] + position_names = ['name', 'default', 'min', 'max', 'qos_marking', + 'dscp'] + position_values = [name, default, min, max, qos_marking, dscp] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_list_qos_queue(self): + resources = "qos_queues" + cmd = qos.ListQoSQueue( + test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_show_qos_queue_id(self): + resource = 'qos_queue' + cmd = qos.ShowQoSQueue( + test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id']) + + def test_delete_qos_queue(self): + resource = 'qos_queue' + cmd = qos.DeleteQoSQueue( + test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + args = [myid] + self._test_delete_resource(resource, cmd, myid, args) + + +class CLITestV20NvpQosQueueXML(CLITestV20NvpQosQueueJSON): + format = 'xml' diff --git a/tests/unit/test_cli20_nvpnetworkgateway.py b/tests/unit/test_cli20_nvpnetworkgateway.py new file mode 100644 index 000000000..81185aa10 --- /dev/null +++ b/tests/unit/test_cli20_nvpnetworkgateway.py @@ -0,0 +1,114 @@ +# Copyright 2012 Nicira, 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import sys + +from neutronclient.neutron.v2_0 import nvpnetworkgateway as nwgw +from tests.unit import test_cli20 + + +class CLITestV20NetworkGatewayJSON(test_cli20.CLITestV20Base): + + resource = "network_gateway" + + def setUp(self): + super(CLITestV20NetworkGatewayJSON, self).setUp( + plurals={'devices': 'device', + 'network_gateways': 'network_gateway'}) + + def test_create_gateway(self): + cmd = nwgw.CreateNetworkGateway(test_cli20.MyApp(sys.stdout), None) + name = 'gw-test' + myid = 'myid' + args = [name, ] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(self.resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_gateway_with_tenant(self): + cmd = nwgw.CreateNetworkGateway(test_cli20.MyApp(sys.stdout), None) + name = 'gw-test' + myid = 'myid' + args = ['--tenant_id', 'tenantid', name] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(self.resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_gateway_with_device(self): + cmd = nwgw.CreateNetworkGateway(test_cli20.MyApp(sys.stdout), None) + name = 'gw-test' + myid = 'myid' + args = ['--device', 'device_id=test', name, ] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(self.resource, cmd, name, myid, args, + position_names, position_values, + devices=[{'device_id': 'test'}]) + + def test_list_gateways(self): + resources = '%ss' % self.resource + cmd = nwgw.ListNetworkGateway(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_update_gateway(self): + cmd = nwgw.UpdateNetworkGateway(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(self.resource, cmd, 'myid', + ['myid', '--name', 'cavani'], + {'name': 'cavani'}) + + def test_delete_gateway(self): + cmd = nwgw.DeleteNetworkGateway(test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + args = [myid] + self._test_delete_resource(self.resource, cmd, myid, args) + + def test_show_gateway(self): + cmd = nwgw.ShowNetworkGateway(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(self.resource, cmd, self.test_id, args, + ['id', 'name']) + + def test_connect_network_to_gateway(self): + cmd = nwgw.ConnectNetworkGateway(test_cli20.MyApp(sys.stdout), None) + args = ['gw_id', 'net_id', + '--segmentation-type', 'edi', + '--segmentation-id', '7'] + self._test_update_resource_action(self.resource, cmd, 'gw_id', + 'connect_network', + args, + {'network_id': 'net_id', + 'segmentation_type': 'edi', + 'segmentation_id': '7'}) + + def test_disconnect_network_from_gateway(self): + cmd = nwgw.DisconnectNetworkGateway(test_cli20.MyApp(sys.stdout), None) + args = ['gw_id', 'net_id', + '--segmentation-type', 'edi', + '--segmentation-id', '7'] + self._test_update_resource_action(self.resource, cmd, 'gw_id', + 'disconnect_network', + args, + {'network_id': 'net_id', + 'segmentation_type': 'edi', + 'segmentation_id': '7'}) + + +class CLITestV20NetworkGatewayXML(CLITestV20NetworkGatewayJSON): + format = 'xml' diff --git a/tests/unit/test_cli20_port.py b/tests/unit/test_cli20_port.py new file mode 100644 index 000000000..150cf8f34 --- /dev/null +++ b/tests/unit/test_cli20_port.py @@ -0,0 +1,304 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import sys + +import mox + +from neutronclient.neutron.v2_0 import port +from neutronclient import shell +from tests.unit import test_cli20 + + +class CLITestV20PortJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20PortJSON, self).setUp(plurals={'tags': 'tag'}) + + def test_create_port(self): + """Create port: netid.""" + resource = 'port' + cmd = port.CreatePort(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + args = [netid] + position_names = ['network_id'] + position_values = [] + position_values.extend([netid]) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_port_full(self): + """Create port: --mac_address mac --device_id deviceid netid.""" + resource = 'port' + cmd = port.CreatePort(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + args = ['--mac_address', 'mac', '--device_id', 'deviceid', netid] + position_names = ['network_id', 'mac_address', 'device_id'] + position_values = [netid, 'mac', 'deviceid'] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + # Test dashed options + args = ['--mac-address', 'mac', '--device-id', 'deviceid', netid] + position_names = ['network_id', 'mac_address', 'device_id'] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_port_tenant(self): + """Create port: --tenant_id tenantid netid.""" + resource = 'port' + cmd = port.CreatePort(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + args = ['--tenant_id', 'tenantid', netid, ] + position_names = ['network_id'] + position_values = [] + position_values.extend([netid]) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + # Test dashed options + args = ['--tenant-id', 'tenantid', netid, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_port_tags(self): + """Create port: netid mac_address device_id --tags a b.""" + resource = 'port' + cmd = port.CreatePort(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + args = [netid, '--tags', 'a', 'b'] + position_names = ['network_id'] + position_values = [] + position_values.extend([netid]) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tags=['a', 'b']) + + def test_create_port_secgroup(self): + """Create port: --security-group sg1_id netid.""" + resource = 'port' + cmd = port.CreatePort(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + args = ['--security-group', 'sg1_id', netid] + position_names = ['network_id', 'security_groups'] + position_values = [netid, ['sg1_id']] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_port_secgroups(self): + """Create port: netid + + The are + --security-group sg1_id --security-group sg2_id + """ + resource = 'port' + cmd = port.CreatePort(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + args = ['--security-group', 'sg1_id', + '--security-group', 'sg2_id', + netid] + position_names = ['network_id', 'security_groups'] + position_values = [netid, ['sg1_id', 'sg2_id']] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_list_ports(self): + """List ports: -D.""" + resources = "ports" + cmd = port.ListPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_ports_pagination(self): + resources = "ports" + cmd = port.ListPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_ports_sort(self): + """list ports: --sort-key name --sort-key id --sort-key asc + --sort-key desc + """ + resources = "ports" + cmd = port.ListPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_ports_limit(self): + """list ports: -P.""" + resources = "ports" + cmd = port.ListPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_list_ports_tags(self): + """List ports: -- --tags a b.""" + resources = "ports" + cmd = port.ListPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, tags=['a', 'b']) + + def test_list_ports_detail_tags(self): + """List ports: -D -- --tags a b.""" + resources = "ports" + cmd = port.ListPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, detail=True, tags=['a', 'b']) + + def test_list_ports_fields(self): + """List ports: --fields a --fields b -- --fields c d.""" + resources = "ports" + cmd = port.ListPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + fields_1=['a', 'b'], fields_2=['c', 'd']) + + def _test_list_router_port(self, resources, cmd, + myid, detail=False, tags=[], + fields_1=[], fields_2=[]): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + reses = {resources: [{'id': 'myid1', }, + {'id': 'myid2', }, ], } + + resstr = self.client.serialize(reses) + + # url method body + query = "" + args = detail and ['-D', ] or [] + + if fields_1: + for field in fields_1: + args.append('--fields') + args.append(field) + args.append(myid) + if tags: + args.append('--') + args.append("--tag") + for tag in tags: + args.append(tag) + if (not tags) and fields_2: + args.append('--') + if fields_2: + args.append("--fields") + for field in fields_2: + args.append(field) + fields_1.extend(fields_2) + for field in fields_1: + if query: + query += "&fields=" + field + else: + query = "fields=" + field + + for tag in tags: + if query: + query += "&tag=" + tag + else: + query = "tag=" + tag + if detail: + query = query and query + '&verbose=True' or 'verbose=True' + query = query and query + '&device_id=%s' or 'device_id=%s' + path = getattr(self.client, resources + "_path") + self.client.httpclient.request( + test_cli20.end_url(path, query % myid), 'GET', + body=None, + headers=mox.ContainsKeyValue('X-Auth-Token', test_cli20.TOKEN) + ).AndReturn((test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + + self.assertTrue('myid1' in _str) + + def test_list_router_ports(self): + """List router ports: -D.""" + resources = "ports" + cmd = port.ListRouterPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, + self.test_id, True) + + def test_list_router_ports_tags(self): + """List router ports: -- --tags a b.""" + resources = "ports" + cmd = port.ListRouterPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, + self.test_id, tags=['a', 'b']) + + def test_list_router_ports_detail_tags(self): + """List router ports: -D -- --tags a b.""" + resources = "ports" + cmd = port.ListRouterPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, self.test_id, + detail=True, tags=['a', 'b']) + + def test_list_router_ports_fields(self): + """List ports: --fields a --fields b -- --fields c d.""" + resources = "ports" + cmd = port.ListRouterPort(test_cli20.MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, self.test_id, + fields_1=['a', 'b'], + fields_2=['c', 'd']) + + def test_update_port(self): + """Update port: myid --name myname --tags a b.""" + resource = 'port' + cmd = port.UpdatePort(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'myname', + '--tags', 'a', 'b'], + {'name': 'myname', 'tags': ['a', 'b'], } + ) + + def test_update_port_security_group_off(self): + """Update port: --no-security-groups myid.""" + resource = 'port' + cmd = port.UpdatePort(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['--no-security-groups', 'myid'], + {'security_groups': None}) + + def test_show_port(self): + """Show port: --fields id --fields name myid.""" + resource = 'port' + cmd = port.ShowPort(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id', 'name']) + + def test_delete_port(self): + """Delete port: myid.""" + resource = 'port' + cmd = port.DeletePort(test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + args = [myid] + self._test_delete_resource(resource, cmd, myid, args) + + +class CLITestV20PortXML(CLITestV20PortJSON): + format = 'xml' diff --git a/tests/unit/test_cli20_router.py b/tests/unit/test_cli20_router.py new file mode 100644 index 000000000..c8ef88284 --- /dev/null +++ b/tests/unit/test_cli20_router.py @@ -0,0 +1,197 @@ +# Copyright 2012 Nicira, 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import sys + +from neutronclient.common import exceptions +from neutronclient.neutron.v2_0 import router +from tests.unit import test_cli20 + + +class CLITestV20RouterJSON(test_cli20.CLITestV20Base): + def test_create_router(self): + """Create router: router1.""" + resource = 'router' + cmd = router.CreateRouter(test_cli20.MyApp(sys.stdout), None) + name = 'router1' + myid = 'myid' + args = [name, ] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_router_tenant(self): + """Create router: --tenant_id tenantid myname.""" + resource = 'router' + cmd = router.CreateRouter(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + args = ['--tenant_id', 'tenantid', name] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_router_admin_state(self): + """Create router: --admin_state_down myname.""" + resource = 'router' + cmd = router.CreateRouter(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + args = ['--admin_state_down', name, ] + position_names = ['name', ] + position_values = [name, ] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + admin_state_up=False) + + def test_list_routers_detail(self): + """list routers: -D.""" + resources = "routers" + cmd = router.ListRouter(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_routers_pagination(self): + resources = "routers" + cmd = router.ListRouter(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_routers_sort(self): + """list routers: --sort-key name --sort-key id --sort-key asc + --sort-key desc + """ + resources = "routers" + cmd = router.ListRouter(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_routers_limit(self): + """list routers: -P.""" + resources = "routers" + cmd = router.ListRouter(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_update_router_exception(self): + """Update router: myid.""" + resource = 'router' + cmd = router.UpdateRouter(test_cli20.MyApp(sys.stdout), None) + self.assertRaises(exceptions.CommandError, self._test_update_resource, + resource, cmd, 'myid', ['myid'], {}) + + def test_update_router(self): + """Update router: myid --name myname --tags a b.""" + resource = 'router' + cmd = router.UpdateRouter(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'myname'], + {'name': 'myname'} + ) + + def test_delete_router(self): + """Delete router: myid.""" + resource = 'router' + cmd = router.DeleteRouter(test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + args = [myid] + self._test_delete_resource(resource, cmd, myid, args) + + def test_show_router(self): + """Show router: myid.""" + resource = 'router' + cmd = router.ShowRouter(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, args, + ['id', 'name']) + + def _test_add_remove_interface(self, action, mode, cmd, args): + resource = 'router' + subcmd = '%s_router_interface' % action + if mode == 'port': + body = {'port_id': 'portid'} + else: + body = {'subnet_id': 'subnetid'} + if action == 'add': + retval = {'subnet_id': 'subnetid', 'port_id': 'portid'} + else: + retval = None + self._test_update_resource_action(resource, cmd, 'myid', + subcmd, args, + body, retval) + + def test_add_interface_compat(self): + """Add interface to router: myid subnetid.""" + cmd = router.AddInterfaceRouter(test_cli20.MyApp(sys.stdout), None) + args = ['myid', 'subnetid'] + self._test_add_remove_interface('add', 'subnet', cmd, args) + + def test_add_interface_by_subnet(self): + """Add interface to router: myid subnet=subnetid.""" + cmd = router.AddInterfaceRouter(test_cli20.MyApp(sys.stdout), None) + args = ['myid', 'subnet=subnetid'] + self._test_add_remove_interface('add', 'subnet', cmd, args) + + def test_add_interface_by_port(self): + """Add interface to router: myid port=portid.""" + cmd = router.AddInterfaceRouter(test_cli20.MyApp(sys.stdout), None) + args = ['myid', 'port=portid'] + self._test_add_remove_interface('add', 'port', cmd, args) + + def test_del_interface_compat(self): + """Delete interface from router: myid subnetid.""" + cmd = router.RemoveInterfaceRouter(test_cli20.MyApp(sys.stdout), None) + args = ['myid', 'subnetid'] + self._test_add_remove_interface('remove', 'subnet', cmd, args) + + def test_del_interface_by_subnet(self): + """Delete interface from router: myid subnet=subnetid.""" + cmd = router.RemoveInterfaceRouter(test_cli20.MyApp(sys.stdout), None) + args = ['myid', 'subnet=subnetid'] + self._test_add_remove_interface('remove', 'subnet', cmd, args) + + def test_del_interface_by_port(self): + """Delete interface from router: myid port=portid.""" + cmd = router.RemoveInterfaceRouter(test_cli20.MyApp(sys.stdout), None) + args = ['myid', 'port=portid'] + self._test_add_remove_interface('remove', 'port', cmd, args) + + def test_set_gateway(self): + """Set external gateway for router: myid externalid.""" + resource = 'router' + cmd = router.SetGatewayRouter(test_cli20.MyApp(sys.stdout), None) + args = ['myid', 'externalid'] + self._test_update_resource(resource, cmd, 'myid', + args, + {"external_gateway_info": + {"network_id": "externalid", + "enable_snat": True}} + ) + + def test_remove_gateway(self): + """Remove external gateway from router: externalid.""" + resource = 'router' + cmd = router.RemoveGatewayRouter(test_cli20.MyApp(sys.stdout), None) + args = ['externalid'] + self._test_update_resource(resource, cmd, 'externalid', + args, {"external_gateway_info": {}} + ) + + +class CLITestV20RouterXML(CLITestV20RouterJSON): + format = 'xml' diff --git a/tests/unit/test_cli20_securitygroup.py b/tests/unit/test_cli20_securitygroup.py new file mode 100644 index 000000000..b7bea09fc --- /dev/null +++ b/tests/unit/test_cli20_securitygroup.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Red Hat +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +import mox + +from neutronclient.neutron.v2_0 import securitygroup +from tests.unit import test_cli20 + + +class CLITestV20SecurityGroupsJSON(test_cli20.CLITestV20Base): + def test_create_security_group(self): + """Create security group: webservers.""" + resource = 'security_group' + cmd = securitygroup.CreateSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + name = 'webservers' + myid = 'myid' + args = [name, ] + position_names = ['name'] + position_values = [name] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_security_group_tenant(self): + """Create security group: webservers.""" + resource = 'security_group' + cmd = securitygroup.CreateSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + name = 'webservers' + description = 'my webservers' + myid = 'myid' + args = ['--tenant_id', 'tenant_id', '--description', description, name] + position_names = ['name', 'description'] + position_values = [name, description] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenant_id') + + def test_create_security_group_with_description(self): + """Create security group: webservers.""" + resource = 'security_group' + cmd = securitygroup.CreateSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + name = 'webservers' + description = 'my webservers' + myid = 'myid' + args = [name, '--description', description] + position_names = ['name', 'description'] + position_values = [name, description] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_list_security_groups(self): + resources = "security_groups" + cmd = securitygroup.ListSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_security_groups_pagination(self): + resources = "security_groups" + cmd = securitygroup.ListSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_security_groups_sort(self): + resources = "security_groups" + cmd = securitygroup.ListSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_security_groups_limit(self): + resources = "security_groups" + cmd = securitygroup.ListSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_security_group_id(self): + resource = 'security_group' + cmd = securitygroup.ShowSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id']) + + def test_show_security_group_id_name(self): + resource = 'security_group' + cmd = securitygroup.ShowSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id', 'name']) + + def test_delete_security_group(self): + """Delete security group: myid.""" + resource = 'security_group' + cmd = securitygroup.DeleteSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + args = [myid] + self._test_delete_resource(resource, cmd, myid, args) + + def test_update_security_group(self): + """Update security group: myid --name myname --description desc.""" + resource = 'security_group' + cmd = securitygroup.UpdateSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'myname', + '--description', 'mydescription'], + {'name': 'myname', + 'description': 'mydescription'} + ) + + def test_update_security_group_with_unicode(self): + resource = 'security_group' + cmd = securitygroup.UpdateSecurityGroup( + test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', u'\u7f51\u7edc', + '--description', u'\u7f51\u7edc'], + {'name': u'\u7f51\u7edc', + 'description': u'\u7f51\u7edc'} + ) + + def test_create_security_group_rule_full(self): + """Create security group rule.""" + resource = 'security_group_rule' + cmd = securitygroup.CreateSecurityGroupRule( + test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + direction = 'ingress' + ethertype = 'IPv4' + protocol = 'tcp' + port_range_min = '22' + port_range_max = '22' + remote_ip_prefix = '10.0.0.0/24' + security_group_id = '1' + remote_group_id = '1' + args = ['--remote_ip_prefix', remote_ip_prefix, '--direction', + direction, '--ethertype', ethertype, '--protocol', protocol, + '--port_range_min', port_range_min, '--port_range_max', + port_range_max, '--remote_group_id', remote_group_id, + security_group_id] + position_names = ['remote_ip_prefix', 'direction', 'ethertype', + 'protocol', 'port_range_min', 'port_range_max', + 'remote_group_id', 'security_group_id'] + position_values = [remote_ip_prefix, direction, ethertype, protocol, + port_range_min, port_range_max, remote_group_id, + security_group_id] + self._test_create_resource(resource, cmd, None, myid, args, + position_names, position_values) + + def test_delete_security_group_rule(self): + """Delete security group rule: myid.""" + resource = 'security_group_rule' + cmd = securitygroup.DeleteSecurityGroupRule( + test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + args = [myid] + self._test_delete_resource(resource, cmd, myid, args) + + def test_list_security_group_rules(self): + resources = "security_group_rules" + cmd = securitygroup.ListSecurityGroupRule( + test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(securitygroup.ListSecurityGroupRule, + "extend_list") + securitygroup.ListSecurityGroupRule.extend_list(mox.IsA(list), + mox.IgnoreArg()) + self._test_list_resources(resources, cmd, True) + + def test_list_security_group_rules_pagination(self): + resources = "security_group_rules" + cmd = securitygroup.ListSecurityGroupRule( + test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(securitygroup.ListSecurityGroupRule, + "extend_list") + securitygroup.ListSecurityGroupRule.extend_list(mox.IsA(list), + mox.IgnoreArg()) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_security_group_rules_sort(self): + resources = "security_group_rules" + cmd = securitygroup.ListSecurityGroupRule( + test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(securitygroup.ListSecurityGroupRule, + "extend_list") + securitygroup.ListSecurityGroupRule.extend_list(mox.IsA(list), + mox.IgnoreArg()) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_security_group_rules_limit(self): + resources = "security_group_rules" + cmd = securitygroup.ListSecurityGroupRule( + test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(securitygroup.ListSecurityGroupRule, + "extend_list") + securitygroup.ListSecurityGroupRule.extend_list(mox.IsA(list), + mox.IgnoreArg()) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_show_security_group_rule(self): + resource = 'security_group_rule' + cmd = securitygroup.ShowSecurityGroupRule( + test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id']) + + def _test_list_security_group_rules_extend(self, data=None, expected=None, + args=[], conv=True, + query_field=False): + def setup_list_stub(resources, data, query): + reses = {resources: data} + resstr = self.client.serialize(reses) + resp = (test_cli20.MyResp(200), resstr) + path = getattr(self.client, resources + '_path') + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(resp) + + # Setup the default data + _data = {'cols': ['id', 'security_group_id', 'remote_group_id'], + 'data': [('ruleid1', 'myid1', 'myid1'), + ('ruleid2', 'myid2', 'myid3'), + ('ruleid3', 'myid2', 'myid2')]} + _expected = {'cols': ['id', 'security_group', 'remote_group'], + 'data': [('ruleid1', 'group1', 'group1'), + ('ruleid2', 'group2', 'group3'), + ('ruleid3', 'group2', 'group2')]} + if data is None: + data = _data + list_data = [dict(zip(data['cols'], d)) for d in data['data']] + if expected is None: + expected = {} + expected['cols'] = expected.get('cols', _expected['cols']) + expected['data'] = expected.get('data', _expected['data']) + + cmd = securitygroup.ListSecurityGroupRule( + test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(cmd, 'get_client') + self.mox.StubOutWithMock(self.client.httpclient, 'request') + cmd.get_client().AndReturn(self.client) + query = '' + if query_field: + query = '&'.join(['fields=' + f for f in data['cols']]) + setup_list_stub('security_group_rules', list_data, query) + if conv: + cmd.get_client().AndReturn(self.client) + sec_ids = set() + for n in data['data']: + sec_ids.add(n[1]) + sec_ids.add(n[2]) + filters = '' + for id in sec_ids: + filters = filters + "&id=%s" % id + setup_list_stub('security_groups', + [{'id': 'myid1', 'name': 'group1'}, + {'id': 'myid2', 'name': 'group2'}, + {'id': 'myid3', 'name': 'group3'}], + query='fields=id&fields=name' + filters) + self.mox.ReplayAll() + + cmd_parser = cmd.get_parser('list_security_group_rules') + parsed_args = cmd_parser.parse_args(args) + result = cmd.get_data(parsed_args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + # Check columns + self.assertEqual(result[0], expected['cols']) + # Check data + _result = [x for x in result[1]] + self.assertEqual(len(_result), len(expected['data'])) + for res, exp in zip(_result, expected['data']): + self.assertEqual(len(res), len(exp)) + self.assertEqual(res, exp) + + def test_list_security_group_rules_extend_source_id(self): + self._test_list_security_group_rules_extend() + + def test_list_security_group_rules_extend_no_nameconv(self): + expected = {'cols': ['id', 'security_group_id', 'remote_group_id'], + 'data': [('ruleid1', 'myid1', 'myid1'), + ('ruleid2', 'myid2', 'myid3'), + ('ruleid3', 'myid2', 'myid2')]} + args = ['--no-nameconv'] + self._test_list_security_group_rules_extend(expected=expected, + args=args, conv=False) + + def test_list_security_group_rules_extend_with_columns(self): + args = '-c id -c security_group_id -c remote_group_id'.split() + self._test_list_security_group_rules_extend(args=args) + + def test_list_security_group_rules_extend_with_columns_no_id(self): + args = '-c id -c security_group -c remote_group'.split() + self._test_list_security_group_rules_extend(args=args) + + def test_list_security_group_rules_extend_with_fields(self): + args = '-F id -F security_group_id -F remote_group_id'.split() + self._test_list_security_group_rules_extend(args=args, + query_field=True) + + def test_list_security_group_rules_extend_with_fields_no_id(self): + args = '-F id -F security_group -F remote_group'.split() + self._test_list_security_group_rules_extend(args=args, + query_field=True) + + +class CLITestV20SecurityGroupsXML(CLITestV20SecurityGroupsJSON): + format = 'xml' diff --git a/tests/unit/test_cli20_subnet.py b/tests/unit/test_cli20_subnet.py new file mode 100644 index 000000000..c7fc9fd54 --- /dev/null +++ b/tests/unit/test_cli20_subnet.py @@ -0,0 +1,402 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import sys + +from neutronclient.neutron.v2_0 import subnet +from tests.unit import test_cli20 + + +class CLITestV20SubnetJSON(test_cli20.CLITestV20Base): + def setUp(self): + super(CLITestV20SubnetJSON, self).setUp(plurals={'tags': 'tag'}) + + def test_create_subnet(self): + """Create subnet: --gateway gateway netid cidr.""" + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'cidrvalue' + gateway = 'gatewayvalue' + args = ['--gateway', gateway, netid, cidr] + position_names = ['ip_version', 'network_id', 'cidr', 'gateway_ip'] + position_values = [4, netid, cidr, gateway] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_subnet_with_no_gateway(self): + """Create subnet: --no-gateway netid cidr.""" + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'cidrvalue' + args = ['--no-gateway', netid, cidr] + position_names = ['ip_version', 'network_id', 'cidr', 'gateway_ip'] + position_values = [4, netid, cidr, None] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + + def test_create_subnet_with_bad_gateway_option(self): + """Create sbunet: --no-gateway netid cidr.""" + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'cidrvalue' + gateway = 'gatewayvalue' + args = ['--gateway', gateway, '--no-gateway', netid, cidr] + position_names = ['ip_version', 'network_id', 'cidr', 'gateway_ip'] + position_values = [4, netid, cidr, None] + try: + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) + except Exception: + return + self.fail('No exception for bad gateway option') + + def test_create_subnet_tenant(self): + """Create subnet: --tenant_id tenantid netid cidr.""" + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', netid, cidr] + position_names = ['ip_version', 'network_id', 'cidr'] + position_values = [4, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_tags(self): + """Create subnet: netid cidr --tags a b.""" + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = [netid, cidr, '--tags', 'a', 'b'] + position_names = ['ip_version', 'network_id', 'cidr'] + position_values = [4, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tags=['a', 'b']) + + def test_create_subnet_allocation_pool(self): + """Create subnet: --tenant_id tenantid netid cidr. + The is --allocation_pool start=1.1.1.10,end=1.1.1.20 + """ + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--allocation_pool', 'start=1.1.1.10,end=1.1.1.20', + netid, cidr] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pool = [{'start': '1.1.1.10', 'end': '1.1.1.20'}] + position_values = [4, pool, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_allocation_pools(self): + """Create subnet: --tenant-id tenantid netid cidr. + The are --allocation_pool start=1.1.1.10,end=1.1.1.20 and + --allocation_pool start=1.1.1.30,end=1.1.1.40 + """ + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--allocation_pool', 'start=1.1.1.10,end=1.1.1.20', + '--allocation_pool', 'start=1.1.1.30,end=1.1.1.40', + netid, cidr] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pools = [{'start': '1.1.1.10', 'end': '1.1.1.20'}, + {'start': '1.1.1.30', 'end': '1.1.1.40'}] + position_values = [4, pools, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_host_route(self): + """Create subnet: --tenant_id tenantid netid cidr. + The is + --host-route destination=172.16.1.0/24,nexthop=1.1.1.20 + """ + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--host-route', 'destination=172.16.1.0/24,nexthop=1.1.1.20', + netid, cidr] + position_names = ['ip_version', 'host_routes', 'network_id', + 'cidr'] + route = [{'destination': '172.16.1.0/24', 'nexthop': '1.1.1.20'}] + position_values = [4, route, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_host_routes(self): + """Create subnet: --tenant-id tenantid netid cidr. + The are + --host-route destination=172.16.1.0/24,nexthop=1.1.1.20 and + --host-route destination=172.17.7.0/24,nexthop=1.1.1.40 + """ + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--host-route', 'destination=172.16.1.0/24,nexthop=1.1.1.20', + '--host-route', 'destination=172.17.7.0/24,nexthop=1.1.1.40', + netid, cidr] + position_names = ['ip_version', 'host_routes', 'network_id', + 'cidr'] + routes = [{'destination': '172.16.1.0/24', 'nexthop': '1.1.1.20'}, + {'destination': '172.17.7.0/24', 'nexthop': '1.1.1.40'}] + position_values = [4, routes, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_dns_nameservers(self): + """Create subnet: --tenant-id tenantid netid cidr. + The are + --dns-nameserver 1.1.1.20 and --dns-nameserver 1.1.1.40 + """ + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--dns-nameserver', '1.1.1.20', + '--dns-nameserver', '1.1.1.40', + netid, cidr] + position_names = ['ip_version', 'dns_nameservers', 'network_id', + 'cidr'] + nameservers = ['1.1.1.20', '1.1.1.40'] + position_values = [4, nameservers, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_with_disable_dhcp(self): + """Create subnet: --tenant-id tenantid --disable-dhcp netid cidr.""" + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--disable-dhcp', + netid, cidr] + position_names = ['ip_version', 'enable_dhcp', 'network_id', + 'cidr'] + position_values = [4, False, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_merge_single_plurar(self): + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--allocation-pool', 'start=1.1.1.10,end=1.1.1.20', + netid, cidr, + '--allocation-pools', 'list=true', 'type=dict', + 'start=1.1.1.30,end=1.1.1.40'] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pools = [{'start': '1.1.1.10', 'end': '1.1.1.20'}, + {'start': '1.1.1.30', 'end': '1.1.1.40'}] + position_values = [4, pools, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_merge_plurar(self): + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + netid, cidr, + '--allocation-pools', 'list=true', 'type=dict', + 'start=1.1.1.30,end=1.1.1.40'] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pools = [{'start': '1.1.1.30', 'end': '1.1.1.40'}] + position_values = [4, pools, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_merge_single_single(self): + resource = 'subnet' + cmd = subnet.CreateSubnet(test_cli20.MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--allocation-pool', 'start=1.1.1.10,end=1.1.1.20', + netid, cidr, + '--allocation-pool', + 'start=1.1.1.30,end=1.1.1.40'] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pools = [{'start': '1.1.1.10', 'end': '1.1.1.20'}, + {'start': '1.1.1.30', 'end': '1.1.1.40'}] + position_values = [4, pools, netid, cidr] + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_list_subnets_detail(self): + """List subnets: -D.""" + resources = "subnets" + cmd = subnet.ListSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, True) + + def test_list_subnets_tags(self): + """List subnets: -- --tags a b.""" + resources = "subnets" + cmd = subnet.ListSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, tags=['a', 'b']) + + def test_list_subnets_known_option_after_unknown(self): + """List subnets: -- --tags a b --request-format xml.""" + resources = "subnets" + cmd = subnet.ListSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, tags=['a', 'b']) + + def test_list_subnets_detail_tags(self): + """List subnets: -D -- --tags a b.""" + resources = "subnets" + cmd = subnet.ListSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, detail=True, tags=['a', 'b']) + + def test_list_subnets_fields(self): + """List subnets: --fields a --fields b -- --fields c d.""" + resources = "subnets" + cmd = subnet.ListSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + fields_1=['a', 'b'], fields_2=['c', 'd']) + + def test_list_subnets_pagination(self): + resources = "subnets" + cmd = subnet.ListSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources_with_pagination(resources, cmd) + + def test_list_subnets_sort(self): + """List subnets: --sort-key name --sort-key id --sort-key asc + --sort-key desc + """ + resources = "subnets" + cmd = subnet.ListSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, + sort_key=["name", "id"], + sort_dir=["asc", "desc"]) + + def test_list_subnets_limit(self): + """List subnets: -P.""" + resources = "subnets" + cmd = subnet.ListSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_list_resources(resources, cmd, page_size=1000) + + def test_update_subnet(self): + """Update subnet: myid --name myname --tags a b.""" + resource = 'subnet' + cmd = subnet.UpdateSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'myname', + '--tags', 'a', 'b'], + {'name': 'myname', 'tags': ['a', 'b'], } + ) + + def test_update_subnet_known_option_before_id(self): + """Update subnet: --request-format json myid --name myname.""" + # --request-format xml is known option + resource = 'subnet' + cmd = subnet.UpdateSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['--request-format', 'json', + 'myid', '--name', 'myname'], + {'name': 'myname', } + ) + + def test_update_subnet_known_option_after_id(self): + """Update subnet: myid --name myname --request-format json.""" + # --request-format xml is known option + resource = 'subnet' + cmd = subnet.UpdateSubnet(test_cli20.MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['myid', '--name', 'myname', + '--request-format', 'json'], + {'name': 'myname', } + ) + + def test_show_subnet(self): + """Show subnet: --fields id --fields name myid.""" + resource = 'subnet' + cmd = subnet.ShowSubnet(test_cli20.MyApp(sys.stdout), None) + args = ['--fields', 'id', '--fields', 'name', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, + args, ['id', 'name']) + + def test_delete_subnet(self): + """Delete subnet: subnetid.""" + resource = 'subnet' + cmd = subnet.DeleteSubnet(test_cli20.MyApp(sys.stdout), None) + myid = 'myid' + args = [myid] + self._test_delete_resource(resource, cmd, myid, args) + + +class CLITestV20SubnetXML(CLITestV20SubnetJSON): + format = 'xml' diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py new file mode 100644 index 000000000..09182324f --- /dev/null +++ b/tests/unit/test_http.py @@ -0,0 +1,64 @@ +# Copyright (C) 2013 OpenStack Foundation. +# 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. +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import httplib2 +import mox +import testtools + +from neutronclient.client import HTTPClient +from neutronclient.common import exceptions +from tests.unit.test_cli20 import MyResp + + +AUTH_TOKEN = 'test_token' +END_URL = 'test_url' +METHOD = 'GET' +URL = 'http://test.test:1234/v2.0/test' + + +class TestHTTPClient(testtools.TestCase): + def setUp(self): + super(TestHTTPClient, self).setUp() + + self.mox = mox.Mox() + self.mox.StubOutWithMock(httplib2.Http, 'request') + self.addCleanup(self.mox.UnsetStubs) + + self.http = HTTPClient(token=AUTH_TOKEN, endpoint_url=END_URL) + + def test_request_error(self): + httplib2.Http.request( + URL, METHOD, headers=mox.IgnoreArg() + ).AndRaise(Exception('error msg')) + self.mox.ReplayAll() + + self.assertRaises( + exceptions.ConnectionFailed, + self.http._cs_request, + URL, METHOD + ) + self.mox.VerifyAll() + + def test_request_success(self): + rv_should_be = MyResp(200), 'test content' + + httplib2.Http.request( + URL, METHOD, headers=mox.IgnoreArg() + ).AndReturn(rv_should_be) + self.mox.ReplayAll() + + self.assertEqual(rv_should_be, self.http._cs_request(URL, METHOD)) + self.mox.VerifyAll() diff --git a/tests/unit/test_name_or_id.py b/tests/unit/test_name_or_id.py new file mode 100644 index 000000000..09de0add3 --- /dev/null +++ b/tests/unit/test_name_or_id.py @@ -0,0 +1,132 @@ +# Copyright 2012 OpenStack LLC. +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import uuid + +import mox +import testtools + +from neutronclient.common import exceptions +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.v2_0 import client +from tests.unit import test_cli20 + + +class CLITestNameorID(testtools.TestCase): + + def setUp(self): + """Prepare the test environment.""" + super(CLITestNameorID, self).setUp() + self.mox = mox.Mox() + self.endurl = test_cli20.ENDURL + self.client = client.Client(token=test_cli20.TOKEN, + endpoint_url=self.endurl) + self.addCleanup(self.mox.VerifyAll) + self.addCleanup(self.mox.UnsetStubs) + + def test_get_id_from_id(self): + _id = str(uuid.uuid4()) + reses = {'networks': [{'id': _id, }, ], } + resstr = self.client.serialize(reses) + self.mox.StubOutWithMock(self.client.httpclient, "request") + path = getattr(self.client, "networks_path") + self.client.httpclient.request( + test_cli20.end_url(path, "fields=id&id=" + _id), 'GET', + body=None, + headers=mox.ContainsKeyValue('X-Auth-Token', test_cli20.TOKEN) + ).AndReturn((test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + returned_id = neutronV20.find_resourceid_by_name_or_id( + self.client, 'network', _id) + self.assertEqual(_id, returned_id) + + def test_get_id_from_id_then_name_empty(self): + _id = str(uuid.uuid4()) + reses = {'networks': [{'id': _id, }, ], } + resstr = self.client.serialize(reses) + resstr1 = self.client.serialize({'networks': []}) + self.mox.StubOutWithMock(self.client.httpclient, "request") + path = getattr(self.client, "networks_path") + self.client.httpclient.request( + test_cli20.end_url(path, "fields=id&id=" + _id), 'GET', + body=None, + headers=mox.ContainsKeyValue('X-Auth-Token', test_cli20.TOKEN) + ).AndReturn((test_cli20.MyResp(200), resstr1)) + self.client.httpclient.request( + test_cli20.end_url(path, "fields=id&name=" + _id), 'GET', + body=None, + headers=mox.ContainsKeyValue('X-Auth-Token', test_cli20.TOKEN) + ).AndReturn((test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + returned_id = neutronV20.find_resourceid_by_name_or_id( + self.client, 'network', _id) + self.assertEqual(_id, returned_id) + + def test_get_id_from_name(self): + name = 'myname' + _id = str(uuid.uuid4()) + reses = {'networks': [{'id': _id, }, ], } + resstr = self.client.serialize(reses) + self.mox.StubOutWithMock(self.client.httpclient, "request") + path = getattr(self.client, "networks_path") + self.client.httpclient.request( + test_cli20.end_url(path, "fields=id&name=" + name), 'GET', + body=None, + headers=mox.ContainsKeyValue('X-Auth-Token', test_cli20.TOKEN) + ).AndReturn((test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + returned_id = neutronV20.find_resourceid_by_name_or_id( + self.client, 'network', name) + self.assertEqual(_id, returned_id) + + def test_get_id_from_name_multiple(self): + name = 'myname' + reses = {'networks': [{'id': str(uuid.uuid4())}, + {'id': str(uuid.uuid4())}]} + resstr = self.client.serialize(reses) + self.mox.StubOutWithMock(self.client.httpclient, "request") + path = getattr(self.client, "networks_path") + self.client.httpclient.request( + test_cli20.end_url(path, "fields=id&name=" + name), 'GET', + body=None, + headers=mox.ContainsKeyValue('X-Auth-Token', test_cli20.TOKEN) + ).AndReturn((test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + try: + neutronV20.find_resourceid_by_name_or_id( + self.client, 'network', name) + except exceptions.NeutronClientException as ex: + self.assertTrue('Multiple' in ex.message) + + def test_get_id_from_name_notfound(self): + name = 'myname' + reses = {'networks': []} + resstr = self.client.serialize(reses) + self.mox.StubOutWithMock(self.client.httpclient, "request") + path = getattr(self.client, "networks_path") + self.client.httpclient.request( + test_cli20.end_url(path, "fields=id&name=" + name), 'GET', + body=None, + headers=mox.ContainsKeyValue('X-Auth-Token', test_cli20.TOKEN) + ).AndReturn((test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + try: + neutronV20.find_resourceid_by_name_or_id( + self.client, 'network', name) + except exceptions.NeutronClientException as ex: + self.assertTrue('Unable to find' in ex.message) + self.assertEqual(404, ex.status_code) diff --git a/tests/unit/test_quota.py b/tests/unit/test_quota.py new file mode 100644 index 000000000..e971f46cc --- /dev/null +++ b/tests/unit/test_quota.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# Copyright (C) 2013 Yahoo! 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. +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import sys + +from neutronclient.common import exceptions +from neutronclient.neutron.v2_0 import quota as test_quota +from tests.unit import test_cli20 + + +class CLITestV20Quota(test_cli20.CLITestV20Base): + def test_show_quota(self): + resource = 'quota' + cmd = test_quota.ShowQuota( + test_cli20.MyApp(sys.stdout), None) + args = ['--tenant-id', self.test_id] + self._test_show_resource(resource, cmd, self.test_id, args) + + def test_update_quota(self): + resource = 'quota' + cmd = test_quota.UpdateQuota( + test_cli20.MyApp(sys.stdout), None) + args = ['--tenant-id', self.test_id, '--network', 'test'] + self.assertRaises( + exceptions.NeutronClientException, self._test_update_resource, + resource, cmd, self.test_id, args=args, + extrafields={'network': 'new'}) + + def test_delete_quota_get_parser(self): + cmd = test_cli20.MyApp(sys.stdout) + test_quota.DeleteQuota(cmd, None).get_parser(cmd) diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py new file mode 100644 index 000000000..a3ac68bbc --- /dev/null +++ b/tests/unit/test_shell.py @@ -0,0 +1,173 @@ +# Copyright (C) 2013 Yahoo! 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. +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import argparse +import cStringIO +import os +import re +import sys + +import fixtures +import mox +import testtools +from testtools import matchers + +from neutronclient.common import exceptions +from neutronclient import shell as openstack_shell + + +DEFAULT_USERNAME = 'username' +DEFAULT_PASSWORD = 'password' +DEFAULT_TENANT_ID = 'tenant_id' +DEFAULT_TENANT_NAME = 'tenant_name' +DEFAULT_AUTH_URL = 'http://127.0.0.1:5000/v2.0/' +DEFAULT_TOKEN = '3bcc3d3a03f44e3d8377f9247b0ad155' +DEFAULT_URL = 'http://quantum.example.org:9696/' + + +class NoExitArgumentParser(argparse.ArgumentParser): + def error(self, message): + raise exceptions.CommandError(message) + + +class ShellTest(testtools.TestCase): + + FAKE_ENV = { + 'OS_USERNAME': DEFAULT_USERNAME, + 'OS_PASSWORD': DEFAULT_PASSWORD, + 'OS_TENANT_ID': DEFAULT_TENANT_ID, + 'OS_TENANT_NAME': DEFAULT_TENANT_NAME, + 'OS_AUTH_URL': DEFAULT_AUTH_URL} + + def _tolerant_shell(self, cmd): + t_shell = openstack_shell.NeutronShell('2.0') + t_shell.run(cmd.split()) + + # Patch os.environ to avoid required auth info. + def setUp(self): + super(ShellTest, self).setUp() + self.mox = mox.Mox() + for var in self.FAKE_ENV: + self.useFixture( + fixtures.EnvironmentVariable( + var, self.FAKE_ENV[var])) + + # Make a fake shell object, a helping wrapper to call it, and a quick + # way of asserting that certain API calls were made. + global shell, _shell, assert_called, assert_called_anytime + _shell = openstack_shell.NeutronShell('2.0') + shell = lambda cmd: _shell.run(cmd.split()) + + def shell(self, argstr): + orig = sys.stdout + clean_env = {} + _old_env, os.environ = os.environ, clean_env.copy() + try: + sys.stdout = cStringIO.StringIO() + _shell = openstack_shell.NeutronShell('2.0') + _shell.run(argstr.split()) + except SystemExit: + exc_type, exc_value, exc_traceback = sys.exc_info() + self.assertEqual(exc_value.code, 0) + finally: + out = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = orig + os.environ = _old_env + return out + + def test_run_unknown_command(self): + openstack_shell.NeutronShell('2.0').run('fake') + + def test_help(self): + required = 'usage:' + help_text = self.shell('help') + self.assertThat( + help_text, + matchers.MatchesRegex(required)) + + def test_help_on_subcommand(self): + required = [ + '.*?^usage: .* quota-list'] + stdout = self.shell('help quota-list') + for r in required: + self.assertThat( + stdout, + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + + def test_help_command(self): + required = 'usage:' + help_text = self.shell('help network-create') + self.assertThat( + help_text, + matchers.MatchesRegex(required)) + + def test_unknown_auth_strategy(self): + self.shell('--os-auth-strategy fake quota-list') + + def test_auth(self): + self.shell(' --os-username test' + ' --os-password test' + ' --os-tenant-name test' + ' --os-auth-url http://127.0.0.1:5000/' + ' --os-auth-strategy keystone quota-list') + + def test_build_option_parser(self): + neutron_shell = openstack_shell.NeutronShell('2.0') + result = neutron_shell.build_option_parser('descr', '2.0') + self.assertEqual(True, isinstance(result, argparse.ArgumentParser)) + + def test_main_with_unicode(self): + self.mox.StubOutClassWithMocks(openstack_shell, 'NeutronShell') + qshell_mock = openstack_shell.NeutronShell('2.0') + #self.mox.StubOutWithMock(qshell_mock, 'run') + unicode_text = u'\u7f51\u7edc' + argv = ['net-list', unicode_text, unicode_text.encode('utf-8')] + qshell_mock.run([u'net-list', unicode_text, + unicode_text]).AndReturn(0) + self.mox.ReplayAll() + ret = openstack_shell.main(argv=argv) + self.mox.VerifyAll() + self.mox.UnsetStubs() + self.assertEqual(ret, 0) + + def test_endpoint_option(self): + shell = openstack_shell.NeutronShell('2.0') + parser = shell.build_option_parser('descr', '2.0') + + # Neither $OS_ENDPOINT_TYPE nor --endpoint-type + namespace = parser.parse_args([]) + self.assertEqual('publicURL', namespace.endpoint_type) + + # --endpoint-type but not $OS_ENDPOINT_TYPE + namespace = parser.parse_args(['--endpoint-type=admin']) + self.assertEqual('admin', namespace.endpoint_type) + + def test_endpoint_environment_variable(self): + fixture = fixtures.EnvironmentVariable("OS_ENDPOINT_TYPE", + "public") + self.useFixture(fixture) + + shell = openstack_shell.NeutronShell('2.0') + parser = shell.build_option_parser('descr', '2.0') + + # $OS_ENDPOINT_TYPE but not --endpoint-type + namespace = parser.parse_args([]) + self.assertEqual("public", namespace.endpoint_type) + + # --endpoint-type and $OS_ENDPOINT_TYPE + namespace = parser.parse_args(['--endpoint-type=admin']) + self.assertEqual('admin', namespace.endpoint_type) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 000000000..e9b4d42ad --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,190 @@ +# Copyright (C) 2013 Yahoo! 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. +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import datetime +import sys + +import testtools + +from neutronclient.common import exceptions +from neutronclient.common import utils + + +class TestUtils(testtools.TestCase): + def test_string_to_bool_true(self): + self.assertTrue(utils.str2bool('true')) + + def test_string_to_bool_false(self): + self.assertFalse(utils.str2bool('false')) + + def test_string_to_bool_None(self): + self.assertIsNone(utils.str2bool(None)) + + def test_string_to_dictionary(self): + input_str = 'key1=value1,key2=value2' + expected = {'key1': 'value1', 'key2': 'value2'} + self.assertEqual(expected, utils.str2dict(input_str)) + + def test_get_dict_item_properties(self): + item = {'name': 'test_name', 'id': 'test_id'} + fields = ('name', 'id') + actual = utils.get_item_properties(item=item, fields=fields) + self.assertEqual(('test_name', 'test_id'), actual) + + def test_get_object_item_properties_mixed_case_fields(self): + class Fake(object): + def __init__(self): + self.id = 'test_id' + self.name = 'test_name' + self.test_user = 'test' + + fields = ('name', 'id', 'test user') + mixed_fields = ('test user', 'ID') + item = Fake() + actual = utils.get_item_properties(item, fields, mixed_fields) + self.assertEqual(('test_name', 'test_id', 'test'), actual) + + def test_get_object_item_desired_fields_differ_from_item(self): + class Fake(object): + def __init__(self): + self.id = 'test_id_1' + self.name = 'test_name' + self.test_user = 'test' + + fields = ('name', 'id', 'test user') + item = Fake() + actual = utils.get_item_properties(item, fields) + self.assertNotEqual(('test_name', 'test_id', 'test'), actual) + + def test_get_object_item_desired_fields_is_empty(self): + class Fake(object): + def __init__(self): + self.id = 'test_id_1' + self.name = 'test_name' + self.test_user = 'test' + + fields = [] + item = Fake() + actual = utils.get_item_properties(item, fields) + self.assertEqual((), actual) + + def test_get_object_item_with_formatters(self): + class Fake(object): + def __init__(self): + self.id = 'test_id' + self.name = 'test_name' + self.test_user = 'test' + + class FakeCallable(object): + def __call__(self, *args, **kwargs): + return 'pass' + + fields = ('name', 'id', 'test user', 'is_public') + formatters = {'is_public': FakeCallable()} + item = Fake() + act = utils.get_item_properties(item, fields, formatters=formatters) + self.assertEqual(('test_name', 'test_id', 'test', 'pass'), act) + + +class JSONUtilsTestCase(testtools.TestCase): + def test_dumps(self): + self.assertEqual(utils.dumps({'a': 'b'}), '{"a": "b"}') + + def test_dumps_dict_with_date_value(self): + x = datetime.datetime(1920, 2, 3, 4, 5, 6, 7) + res = utils.dumps({1: 'a', 2: x}) + expected = '{"1": "a", "2": "1920-02-03 04:05:06.000007"}' + self.assertEqual(expected, res) + + def test_dumps_dict_with_spaces(self): + x = datetime.datetime(1920, 2, 3, 4, 5, 6, 7) + res = utils.dumps({1: 'a ', 2: x}) + expected = '{"1": "a ", "2": "1920-02-03 04:05:06.000007"}' + self.assertEqual(expected, res) + + def test_loads(self): + self.assertEqual(utils.loads('{"a": "b"}'), {'a': 'b'}) + + +class ToPrimitiveTestCase(testtools.TestCase): + def test_list(self): + self.assertEqual(utils.to_primitive([1, 2, 3]), [1, 2, 3]) + + def test_empty_list(self): + self.assertEqual(utils.to_primitive([]), []) + + def test_tuple(self): + self.assertEqual(utils.to_primitive((1, 2, 3)), [1, 2, 3]) + + def test_empty_tuple(self): + self.assertEqual(utils.to_primitive(()), []) + + def test_dict(self): + self.assertEqual( + utils.to_primitive(dict(a=1, b=2, c=3)), + dict(a=1, b=2, c=3)) + + def test_empty_dict(self): + self.assertEqual(utils.to_primitive({}), {}) + + def test_datetime(self): + x = datetime.datetime(1920, 2, 3, 4, 5, 6, 7) + self.assertEqual( + utils.to_primitive(x), + '1920-02-03 04:05:06.000007') + + def test_iter(self): + x = xrange(1, 6) + self.assertEqual(utils.to_primitive(x), [1, 2, 3, 4, 5]) + + def test_iteritems(self): + d = {'a': 1, 'b': 2, 'c': 3} + + class IterItemsClass(object): + def iteritems(self): + return d.iteritems() + + x = IterItemsClass() + p = utils.to_primitive(x) + self.assertEqual(p, {'a': 1, 'b': 2, 'c': 3}) + + def test_nasties(self): + def foo(): + pass + x = [datetime, foo, dir] + ret = utils.to_primitive(x) + self.assertEqual(len(ret), 3) + + def test_to_primitive_dict_with_date_value(self): + x = datetime.datetime(1920, 2, 3, 4, 5, 6, 7) + res = utils.to_primitive({'a': x}) + self.assertEqual({'a': '1920-02-03 04:05:06.000007'}, res) + + +class ImportClassTestCase(testtools.TestCase): + def test_import_class(self): + dt = utils.import_class('datetime.datetime') + self.assertTrue(sys.modules['datetime'].datetime is dt) + + def test_import_bad_class(self): + self.assertRaises( + ImportError, utils.import_class, + 'lol.u_mad.brah') + + def test_get_client_class_invalid_version(self): + self.assertRaises( + exceptions.UnsupportedVersion, + utils.get_client_class, 'image', '2', {'image': '2'}) diff --git a/tools/neutron.bash_completion b/tools/neutron.bash_completion new file mode 100644 index 000000000..3e542e0a9 --- /dev/null +++ b/tools/neutron.bash_completion @@ -0,0 +1,27 @@ +_neutron_opts="" # lazy init +_neutron_flags="" # lazy init +_neutron_opts_exp="" # lazy init +_neutron() +{ + local cur prev nbc cflags + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + if [ "x$_neutron_opts" == "x" ] ; then + nbc="`neutron bash-completion`" + _neutron_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" + _neutron_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" + _neutron_opts_exp="`echo "$_neutron_opts" | sed -e "s/\s/|/g"`" + fi + + if [[ " ${COMP_WORDS[@]} " =~ " "($_neutron_opts_exp)" " && "$prev" != "help" ]] ; then + COMPLETION_CACHE=~/.neutronclient/*/*-cache + cflags="$_neutron_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') + COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) + else + COMPREPLY=($(compgen -W "${_neutron_opts}" -- ${cur})) + fi + return 0 +} +complete -F _neutron neutron diff --git a/tools/pip-requires b/tools/pip-requires deleted file mode 100644 index 4b447507a..000000000 --- a/tools/pip-requires +++ /dev/null @@ -1,16 +0,0 @@ -# NOTE(danwent): these paste dependencies are only here because of the weird -# split of "common" quantum code between quantum-server and quantum-client. -# Should be removed from here and setup.py once we fix that. -Paste -PasteDeploy==1.5.0 -python-gflags==1.3 - -distribute>=0.6.24 - -coverage -nose -nosexcover -pep8==0.6.1 - --e git+https://review.openstack.org/p/openstack-dev/openstack-nose.git#egg=openstack.nose_plugin --e git+https://review.openstack.org/p/openstack/quantum#egg=quantum-dev diff --git a/tox.ini b/tox.ini index 23565b9dd..71ade0244 100644 --- a/tox.ini +++ b/tox.ini @@ -1,31 +1,31 @@ [tox] -envlist = py26,py27,pep8 +envlist = py26,py27,py33,pep8 [testenv] -deps = -r{toxinidir}/tools/pip-requires -commands = nosetests +setenv = VIRTUAL_ENV={envdir} + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] -commands = pep8 --exclude=vcsversion.py,*.pyc --repeat --show-source quantum setup.py version.py +commands = flake8 +distribute = false + +[testenv:venv] +commands = {posargs} [testenv:cover] -commands = nosetests --with-coverage --cover-html --cover-erase --cover-package=quantum +commands = python setup.py testr --coverage --testr-args='{posargs}' -[testenv:hudson] +[tox:jenkins] downloadcache = ~/cache/pip -[testenv:jenkins26] -basepython = python2.6 -deps = file://{toxinidir}/.cache.bundle - -[testenv:jenkins27] -basepython = python2.7 -deps = file://{toxinidir}/.cache.bundle - -[testenv:jenkinspep8] -deps = file://{toxinidir}/.cache.bundle -commands = pep8 --exclude=vcsversion.py,*.pyc --repeat --show-source quantum setup.py version.py - -[testenv:jenkinscover] -deps = file://{toxinidir}/.cache.bundle -commands = nosetests --cover-erase --cover-package=quantum --with-xcoverage +[flake8] +# E125 continuation line does not distinguish itself from next logical line +# H302 import only modules +ignore = E125,H302 +show-source = true +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools diff --git a/version.py b/version.py deleted file mode 100644 index d41abf372..000000000 --- a/version.py +++ /dev/null @@ -1,46 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 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. - -try: - from quantum.vcsversion import version_info -except ImportError: - version_info = {'branch_nick': u'LOCALBRANCH', - 'revision_id': 'LOCALREVISION', - 'revno': 0} - -QUANTUM_VERSION = ['2012', '2', None] -YEAR, COUNT, REVSISION = QUANTUM_VERSION - -FINAL = True # This becomes true at Release Candidate time - - -def canonical_version_string(): - return '.'.join(filter(None, QUANTUM_VERSION)) - - -def version_string(): - if FINAL: - return canonical_version_string() - else: - return '%s-dev' % (canonical_version_string(),) - - -def vcs_version_string(): - return "%s:%s" % (version_info['branch_nick'], version_info['revision_id']) - - -def version_string_with_vcs(): - return "%s-%s" % (canonical_version_string(), vcs_version_string())