diff options
| author | Mark McClain <mark.mcclain@dreamhost.com> | 2013-07-02 18:44:42 -0400 |
|---|---|---|
| committer | Mark McClain <mark.mcclain@dreamhost.com> | 2013-07-03 11:56:44 -0400 |
| commit | 93ac15bfeb51ee6a097d878e4eeef69cd2b3a6bc (patch) | |
| tree | 39acb2ba93a28e1f03a3e6d739845758cd58715b /neutronclient | |
| parent | 8ed38707b12ae6e77480ae8d8542712d63b7fc70 (diff) | |
| download | python-neutronclient-2.2.4.tar.gz | |
Rename quantumclient to neutronclient2.2.4
Implements Blueprint: remove-use-of-quantum
Change-Id: Idebe92d56d277435ffd23f292984f9b8b8fdb2df
Diffstat (limited to 'neutronclient')
41 files changed, 6558 insertions, 0 deletions
diff --git a/neutronclient/__init__.py b/neutronclient/__init__.py new file mode 100644 index 0000000..034d66e --- /dev/null +++ b/neutronclient/__init__.py @@ -0,0 +1,17 @@ +# 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 diff --git a/neutronclient/client.py b/neutronclient/client.py new file mode 100644 index 0000000..9dc80a3 --- /dev/null +++ b/neutronclient/client.py @@ -0,0 +1,250 @@ +# 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, + 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.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.force_exception_to_status_code = True + 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) + resp, body = self.request(*args, **kargs) + 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') + 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 0000000..47ca708 --- /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 0000000..8e0614d --- /dev/null +++ b/neutronclient/common/clientmanager.py @@ -0,0 +1,87 @@ +# 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, + 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 0000000..7191436 --- /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 0000000..a8e8276 --- /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 0000000..e02be30 --- /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 0000000..b8f1c19 --- /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 0000000..ca2e6ec --- /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 0000000..63c3905 --- /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 0000000..daf0481 --- /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 0000000..9cb8cd1 --- /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 0000000..8c85f54 --- /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 0000000..d761125 --- /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 0000000..b2a6411 --- /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 0000000..7931f31 --- /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/neutronclient/neutron/v2_0/lb/__init__.py b/neutronclient/neutron/v2_0/lb/__init__.py new file mode 100644 index 0000000..1668497 --- /dev/null +++ b/neutronclient/neutron/v2_0/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/neutronclient/neutron/v2_0/lb/healthmonitor.py b/neutronclient/neutron/v2_0/lb/healthmonitor.py new file mode 100644 index 0000000..a6e847a --- /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 0000000..b13fdcd --- /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 0000000..4fb1e79 --- /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 0000000..f836862 --- /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 0000000..0f384ab --- /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=<uuid>& (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 0000000..79a1801 --- /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 0000000..1853514 --- /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=<device identifier>,' + 'interface_name=<name_or_identifier> ' + '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 0000000..f8ac9a9 --- /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=<name_or_id>,ip_address=<ip>, ' + '(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 0000000..bc568ef --- /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 0000000..54b8117 --- /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 0000000..92570fa --- /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 0000000..3674db8 --- /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/neutronclient/openstack/__init__.py b/neutronclient/openstack/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/neutronclient/openstack/__init__.py diff --git a/neutronclient/openstack/common/__init__.py b/neutronclient/openstack/common/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/neutronclient/openstack/common/__init__.py diff --git a/neutronclient/openstack/common/exception.py b/neutronclient/openstack/common/exception.py new file mode 100644 index 0000000..bb6e6dc --- /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 0000000..5c597c7 --- /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 0000000..9ff7415 --- /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 0000000..ecf3cfd --- /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 0000000..0f34608 --- /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 0000000..8cc6441 --- /dev/null +++ b/neutronclient/shell.py @@ -0,0 +1,575 @@ +# 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='<auth-strategy>', + 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='<auth-url>', + 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='<auth-tenant-name>', + 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-username', metavar='<auth-username>', + 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='<auth-password>', + 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='<auth-region-name>', + 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='<token>', + default=env('OS_TOKEN'), + help='Defaults to env[OS_TOKEN]') + parser.add_argument( + '--os_token', + help=argparse.SUPPRESS) + + parser.add_argument( + '--endpoint-type', metavar='<endpoint-type>', + default=env('OS_ENDPOINT_TYPE', default='publicURL'), + help='Defaults to env[OS_ENDPOINT_TYPE] or publicURL.') + + parser.add_argument( + '--os-url', metavar='<url>', + 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): + raise exc.CommandError( + "You must provide a tenant_name via" + " either --os-tenant-name or via env[OS_TENANT_NAME]") + + 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, + 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 0000000..22c4ba4 --- /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 0000000..63c3905 --- /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 0000000..9463614 --- /dev/null +++ b/neutronclient/v2_0/client.py @@ -0,0 +1,880 @@ +# 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 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 0000000..9315671 --- /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() |
