diff --git a/.gitignore b/.gitignore index 15eae7e22..1a5761972 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ run_tests.log doc/build/ doc/source/api/ quantumclient/versioninfo +ChangeLog diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 000000000..d356fcf1e --- /dev/null +++ b/.testr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./quantumclient/tests $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/HACKING.rst b/HACKING.rst index d99a07f9a..c7643238e 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -185,3 +185,22 @@ For every new feature, unit tests should be created that both test and bug that had no unit test, a new passing unit test should be added. If a submitted bug fix does have a unit test, be sure to add a new one that fails without the patch and passes with the patch. + +Running Tests +------------- +The testing system is based on a combination of tox and testr. The canonical +approach to running tests is to simply run the command `tox`. This will +create virtual environments, populate them with depenedencies and run all of +the tests that OpenStack CI systems run. Behind the scenes, tox is running +`testr run --parallel`, but is set up such that you can supply any additional +testr arguments that are needed to tox. For example, you can run: +`tox -- --analyze-isolation` to cause tox to tell testr to add +--analyze-isolation to its argument list. + +It is also possible to run the tests inside of a virtual environment +you have created, or it is possible that you have all of the dependencies +installed locally already. In this case, you can interact with the testr +command directly. Running `testr run` will run the entire test suite. `testr +run --parallel` will run it in parallel (this is the default incantation tox +uses.) More information about testr can be found at: +http://wiki.openstack.org/testr diff --git a/MANIFEST.in b/MANIFEST.in index 62ce0fa62..41d72419d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include tox.ini include LICENSE README HACKING.rst +include ChangeLog include tools/* include quantumclient/tests/* include quantumclient/tests/unit/* diff --git a/doc/source/index.rst b/doc/source/index.rst index 85ca4009f..1dd7caefc 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,13 +3,17 @@ Python bindings to the OpenStack Network API In order to use the python quantum client directly, you must first obtain an auth token and identify which endpoint you wish to speak to. Once you have done so, you can use the API like so:: + >>> import logging >>> from quantumclient.quantum import client + >>> logging.basicConfig(level=logging.DEBUG) >>> quantum = client.Client('2.0', endpoint_url=OS_URL, token=OS_TOKEN) + >>> quantum.format = 'json' >>> network = {'name': 'mynetwork', 'admin_state_up': True} >>> quantum.create_network({'network':network}) >>> networks = quantum.list_networks(name='mynetwork') >>> print networks - >>> quantum.delete_network(name='mynetwork') + >>> network_id = networks['networks'][0]['id'] + >>> quantum.delete_network(network_id) Command-line Tool diff --git a/openstack-common.conf b/openstack-common.conf index 35d48d06e..d52611484 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=setup +modules=jsonutils,setup,timeutils # The base module to hold the copy of openstack.common base=quantumclient diff --git a/quantum_test.sh b/quantum_test.sh index e19600bec..b42299beb 100755 --- a/quantum_test.sh +++ b/quantum_test.sh @@ -14,11 +14,12 @@ else NOAUTH= fi +FORMAT=" --request-format xml" # test the CRUD of network network=mynet1 -quantum net-create $NOAUTH $network || die "fail to create network $network" -temp=`quantum net-list -- --name $network --fields id | wc -l` +quantum net-create $FORMAT $NOAUTH $network || die "fail to create network $network" +temp=`quantum net-list $FORMAT -- --name $network --fields id | wc -l` echo $temp if [ $temp -ne 5 ]; then die "networks with name $network is not unique or found" @@ -26,102 +27,102 @@ fi network_id=`quantum net-list -- --name $network --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` echo "ID of network with name $network is $network_id" -quantum net-show $network || die "fail to show network $network" -quantum net-show $network_id || die "fail to show network $network_id" +quantum net-show $FORMAT $network || die "fail to show network $network" +quantum net-show $FORMAT $network_id || die "fail to show network $network_id" -quantum net-update $network --admin_state_up False || die "fail to update network $network" -quantum net-update $network_id --admin_state_up True || die "fail to update network $network_id" +quantum net-update $FORMAT $network --admin_state_up False || die "fail to update network $network" +quantum net-update $FORMAT $network_id --admin_state_up True || die "fail to update network $network_id" -quantum net-list -c id -- --id fakeid || die "fail to list networks with column selection on empty list" +quantum net-list $FORMAT -c id -- --id fakeid || die "fail to list networks with column selection on empty list" # test the CRUD of subnet subnet=mysubnet1 cidr=10.0.1.3/24 -quantum subnet-create $NOAUTH $network $cidr --name $subnet || die "fail to create subnet $subnet" -tempsubnet=`quantum subnet-list -- --name $subnet --fields id | wc -l` +quantum subnet-create $FORMAT $NOAUTH $network $cidr --name $subnet || die "fail to create subnet $subnet" +tempsubnet=`quantum subnet-list $FORMAT -- --name $subnet --fields id | wc -l` echo $tempsubnet if [ $tempsubnet -ne 5 ]; then die "subnets with name $subnet is not unique or found" fi -subnet_id=`quantum subnet-list -- --name $subnet --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` +subnet_id=`quantum subnet-list $FORMAT -- --name $subnet --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` echo "ID of subnet with name $subnet is $subnet_id" -quantum subnet-show $subnet || die "fail to show subnet $subnet" -quantum subnet-show $subnet_id || die "fail to show subnet $subnet_id" +quantum subnet-show $FORMAT $subnet || die "fail to show subnet $subnet" +quantum subnet-show $FORMAT $subnet_id || die "fail to show subnet $subnet_id" -quantum subnet-update $subnet --dns_namesevers host1 || die "fail to update subnet $subnet" -quantum subnet-update $subnet_id --dns_namesevers host2 || die "fail to update subnet $subnet_id" +quantum subnet-update $FORMAT $subnet --dns_namesevers host1 || die "fail to update subnet $subnet" +quantum subnet-update $FORMAT $subnet_id --dns_namesevers host2 || die "fail to update subnet $subnet_id" # test the crud of ports port=myport1 -quantum port-create $NOAUTH $network --name $port || die "fail to create port $port" -tempport=`quantum port-list -- --name $port --fields id | wc -l` +quantum port-create $FORMAT $NOAUTH $network --name $port || die "fail to create port $port" +tempport=`quantum port-list $FORMAT -- --name $port --fields id | wc -l` echo $tempport if [ $tempport -ne 5 ]; then die "ports with name $port is not unique or found" fi -port_id=`quantum port-list -- --name $port --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` +port_id=`quantum port-list $FORMAT -- --name $port --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` echo "ID of port with name $port is $port_id" -quantum port-show $port || die "fail to show port $port" -quantum port-show $port_id || die "fail to show port $port_id" +quantum port-show $FORMAT $port || die "fail to show port $port" +quantum port-show $FORMAT $port_id || die "fail to show port $port_id" -quantum port-update $port --device_id deviceid1 || die "fail to update port $port" -quantum port-update $port_id --device_id deviceid2 || die "fail to update port $port_id" +quantum port-update $FORMAT $port --device_id deviceid1 || die "fail to update port $port" +quantum port-update $FORMAT $port_id --device_id deviceid2 || die "fail to update port $port_id" # test quota commands RUD DEFAULT_NETWORKS=10 DEFAULT_PORTS=50 tenant_id=tenant_a tenant_id_b=tenant_b -quantum quota-update --tenant_id $tenant_id --network 30 || die "fail to update quota for tenant $tenant_id" -quantum quota-update --tenant_id $tenant_id_b --network 20 || die "fail to update quota for tenant $tenant_id" -networks=`quantum quota-list -c network -c tenant_id | grep $tenant_id | awk '{print $2}'` +quantum quota-update $FORMAT --tenant_id $tenant_id --network 30 || die "fail to update quota for tenant $tenant_id" +quantum quota-update $FORMAT --tenant_id $tenant_id_b --network 20 || die "fail to update quota for tenant $tenant_id" +networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id | awk '{print $2}'` if [ $networks -ne 30 ]; then die "networks quota should be 30" fi -networks=`quantum quota-list -c network -c tenant_id | grep $tenant_id_b | awk '{print $2}'` +networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id_b | awk '{print $2}'` if [ $networks -ne 20 ]; then die "networks quota should be 20" fi -networks=`quantum quota-show --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` +networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` if [ $networks -ne 30 ]; then die "networks quota should be 30" fi -quantum quota-delete --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id" -networks=`quantum quota-show --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` +quantum quota-delete $FORMAT --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id" +networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` if [ $networks -ne $DEFAULT_NETWORKS ]; then die "networks quota should be $DEFAULT_NETWORKS" fi # update self if [ "t$NOAUTH" = "t" ]; then # with auth - quantum quota-update --port 99 || die "fail to update quota for self" - ports=`quantum quota-show | grep port | awk -F'|' '{print $3}'` + quantum quota-update $FORMAT --port 99 || die "fail to update quota for self" + ports=`quantum quota-show $FORMAT | grep port | awk -F'|' '{print $3}'` if [ $ports -ne 99 ]; then die "ports quota should be 99" fi - ports=`quantum quota-list -c port | grep 99 | awk '{print $2}'` + ports=`quantum quota-list $FORMAT -c port | grep 99 | awk '{print $2}'` if [ $ports -ne 99 ]; then die "ports quota should be 99" fi - quantum quota-delete || die "fail to delete quota for tenant self" - ports=`quantum quota-show | grep port | awk -F'|' '{print $3}'` + quantum quota-delete $FORMAT || die "fail to delete quota for tenant self" + ports=`quantum quota-show $FORMAT | grep port | awk -F'|' '{print $3}'` if [ $ports -ne $DEFAULT_PORTS ]; then die "ports quota should be $DEFAULT_PORTS" fi else # without auth - quantum quota-update --port 100 + quantum quota-update $FORMAT --port 100 if [ $? -eq 0 ]; then die "without valid context on server, quota update command should fail." fi - quantum quota-show + quantum quota-show $FORMAT if [ $? -eq 0 ]; then die "without valid context on server, quota show command should fail." fi - quantum quota-delete + quantum quota-delete $FORMAT if [ $? -eq 0 ]; then die "without valid context on server, quota delete command should fail." fi - quantum quota-list || die "fail to update quota for self" + quantum quota-list $FORMAT || die "fail to update quota for self" fi diff --git a/quantumclient/__init__.py b/quantumclient/__init__.py index 5558fdb97..034d66eaf 100644 --- a/quantumclient/__init__.py +++ b/quantumclient/__init__.py @@ -15,9 +15,3 @@ # License for the specific language governing permissions and limitations # under the License. # @author: Tyler Smith, Cisco Systems - -import gettext - - -# gettext must be initialized before any quantumclient imports -gettext.install('quantumclient', unicode=1) diff --git a/quantumclient/client.py b/quantumclient/client.py index 18bb3de2a..39e770e79 100644 --- a/quantumclient/client.py +++ b/quantumclient/client.py @@ -123,10 +123,9 @@ def _cs_request(self, *args, **kwargs): if 'body' in kwargs: kargs['body'] = kwargs['body'] - + utils.http_log_req(_logger, args, kargs) resp, body = self.request(*args, **kargs) - - utils.http_log(_logger, args, kargs, resp, body) + utils.http_log_resp(_logger, resp, body) status_code = self.get_status_code(resp) if status_code == 401: raise exceptions.Unauthorized(message=body) diff --git a/quantumclient/common/__init__.py b/quantumclient/common/__init__.py index 7e695ff08..1415c50a3 100644 --- a/quantumclient/common/__init__.py +++ b/quantumclient/common/__init__.py @@ -14,3 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. # @author: Somik Behera, Nicira Networks, Inc. + +import gettext + +t = gettext.translation('quantumclient', fallback=True) + + +def _(msg): + return t.ugettext(msg) diff --git a/quantumclient/common/clientmanager.py b/quantumclient/common/clientmanager.py index 1c1f1acd0..7346aa541 100644 --- a/quantumclient/common/clientmanager.py +++ b/quantumclient/common/clientmanager.py @@ -20,10 +20,11 @@ import logging +from quantumclient.client import HTTPClient from quantumclient.common import exceptions as exc - from quantumclient.quantum import client as quantum_client -from quantumclient.client import HTTPClient + + LOG = logging.getLogger(__name__) diff --git a/quantumclient/common/constants.py b/quantumclient/common/constants.py new file mode 100644 index 000000000..572baa941 --- /dev/null +++ b/quantumclient/common/constants.py @@ -0,0 +1,40 @@ +# 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" + +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/quantumclient/common/exceptions.py b/quantumclient/common/exceptions.py index 734a49894..1df133d7b 100644 --- a/quantumclient/common/exceptions.py +++ b/quantumclient/common/exceptions.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +from quantumclient.common import _ + """ Quantum base exception handling. """ @@ -92,7 +94,7 @@ class Unauthorized(QuantumClientException): """ HTTP 401 - Unauthorized: bad credentials. """ - pass + message = _("Unauthorized: bad credentials.") class Forbidden(QuantumClientException): @@ -100,12 +102,13 @@ class Forbidden(QuantumClientException): HTTP 403 - Forbidden: your credentials don't give you access to this resource. """ - pass + message = _("Forbidden: your credentials don't give you access to this " + "resource.") class EndpointNotFound(QuantumClientException): """Could not find Service or Region in Service Catalog.""" - pass + message = _("Could not find Service or Region in Service Catalog.") class AmbiguousEndpoints(QuantumClientException): diff --git a/quantumclient/common/serializer.py b/quantumclient/common/serializer.py index 6e914673a..7eba93069 100644 --- a/quantumclient/common/serializer.py +++ b/quantumclient/common/serializer.py @@ -1,7 +1,353 @@ -from xml.dom import minidom +# 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 quantum wsgi +### + +import logging + +from xml.etree import ElementTree as etree +from xml.parsers import expat + +from quantumclient.common import constants from quantumclient.common import exceptions as exception -from quantumclient.common import utils +from quantumclient.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): + return jsonutils.dumps(data) + + +class XMLDictSerializer(DictSerializer): + + def __init__(self, metadata=None, xmlns=None): + """ + :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): + # We expect data to contain a single key which is the XML root or + # non root + try: + key_len = data and len(data.keys()) or 0 + if (key_len == 1): + root_key = data.keys()[0] + root_value = data[root_key] + else: + root_key = constants.VIRTUAL_ROOT_KEY + root_value = data + doc = etree.Element("_temp_root") + used_prefixes = [] + self._to_xml_node(doc, self.metadata, root_key, + root_value, used_prefixes) + return self.to_xml_string(list(doc)[0], used_prefixes) + 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('xmlns:atom', "http://www.w3.org/2005/Atom") + 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)}) + result.text = str(data) + return result + + def _create_link_nodes(self, xml_doc, links): + link_nodes = [] + for link in links: + link_node = xml_doc.createElement('atom:link') + link_node.set('rel', link['rel']) + link_node.set('href', link['href']) + if 'type' in link: + link_node.set('type', link['type']) + link_nodes.append(link_node) + return link_nodes + + +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): + """ + :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 _from_xml(self, datastring): + if datastring is None: + return None + plurals = set(self.metadata.get('plurals', {})) + try: + node = etree.fromstring(datastring) + result = self._from_xml_node(node, plurals) + root_tag = self._get_key(node.tag) + if root_tag == constants.VIRTUAL_ROOT_KEY: + return result + else: + return {root_tag: result} + 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 find_first_child_named(self, parent, name): + """Search a nodes children for the first child with a given name""" + for node in parent.childNodes: + if node.nodeName == name: + return node + return None + + def find_children_named(self, parent, name): + """Return all of a nodes children who have the given name""" + for node in parent.childNodes: + if node.nodeName == name: + yield node + + def extract_text(self, node): + """Get the text field contained by the given node""" + if len(node.childNodes) == 1: + child = node.childNodes[0] + if child.nodeType == child.TEXT_NODE: + return child.nodeValue + return "" + + 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 quantum.wsgi @@ -20,8 +366,8 @@ def __init__(self, metadata=None, default_xmlns=None): def _get_serialize_handler(self, content_type): handlers = { - 'application/json': self._to_json, - 'application/xml': self._to_xml, + 'application/json': JSONDictSerializer(), + 'application/xml': XMLDictSerializer(self.metadata), } try: @@ -31,7 +377,7 @@ def _get_serialize_handler(self, content_type): def serialize(self, data, content_type): """Serialize a dictionary into the specified content type.""" - return self._get_serialize_handler(content_type)(data) + return self._get_serialize_handler(content_type).serialize(data) def deserialize(self, datastring, content_type): """Deserialize a string to a dictionary. @@ -39,117 +385,16 @@ def deserialize(self, datastring, content_type): The string must be in the format of a supported MIME type. """ - try: - return self.get_deserialize_handler(content_type)(datastring) - except Exception: - raise exception.MalformedResponseBody( - reason="Unable to deserialize response body") + return self.get_deserialize_handler(content_type).deserialize( + datastring) def get_deserialize_handler(self, content_type): handlers = { - 'application/json': self._from_json, - 'application/xml': self._from_xml, + 'application/json': JSONDeserializer(), + 'application/xml': XMLDeserializer(self.metadata), } try: return handlers[content_type] except Exception: raise exception.InvalidContentType(content_type=content_type) - - def _from_json(self, datastring): - return utils.loads(datastring) - - def _from_xml(self, datastring): - xmldata = self.metadata.get('application/xml', {}) - plurals = set(xmldata.get('plurals', {})) - node = minidom.parseString(datastring).childNodes[0] - return {node.nodeName: self._from_xml_node(node, plurals)} - - def _from_xml_node(self, node, listnames): - """Convert a minidom node to a simple Python type. - - listnames is a collection of names of XML nodes whose subnodes should - be considered list items. - - """ - if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3: - return node.childNodes[0].nodeValue - elif node.nodeName in listnames: - return [self._from_xml_node(n, listnames) - for n in node.childNodes if n.nodeType != node.TEXT_NODE] - else: - result = dict() - for attr in node.attributes.keys(): - result[attr] = node.attributes[attr].nodeValue - for child in node.childNodes: - if child.nodeType != node.TEXT_NODE: - result[child.nodeName] = self._from_xml_node(child, - listnames) - return result - - def _to_json(self, data): - return utils.dumps(data) - - def _to_xml(self, data): - metadata = self.metadata.get('application/xml', {}) - # We expect data to contain a single key which is the XML root. - root_key = data.keys()[0] - doc = minidom.Document() - node = self._to_xml_node(doc, metadata, root_key, data[root_key]) - - xmlns = node.getAttribute('xmlns') - if not xmlns and self.default_xmlns: - node.setAttribute('xmlns', self.default_xmlns) - - return node.toprettyxml(indent='', newl='') - - def _to_xml_node(self, doc, metadata, nodename, data): - """Recursive method to convert data members to XML nodes.""" - result = doc.createElement(nodename) - - # Set the xml namespace if one is specified - # TODO(justinsb): We could also use prefixes on the keys - xmlns = metadata.get('xmlns', None) - if xmlns: - result.setAttribute('xmlns', xmlns) - if type(data) is list: - collections = metadata.get('list_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for item in data: - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(item)) - result.appendChild(node) - return result - singular = metadata.get('plurals', {}).get(nodename, None) - if singular is None: - if nodename.endswith('s'): - singular = nodename[:-1] - else: - singular = 'item' - for item in data: - node = self._to_xml_node(doc, metadata, singular, item) - result.appendChild(node) - elif type(data) is dict: - collections = metadata.get('dict_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for k, v in data.items(): - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(k)) - text = doc.createTextNode(str(v)) - node.appendChild(text) - result.appendChild(node) - return result - attrs = metadata.get('attributes', {}).get(nodename, {}) - for k, v in data.items(): - if k in attrs: - result.setAttribute(k, str(v)) - else: - node = self._to_xml_node(doc, metadata, k, v) - result.appendChild(node) - else: - # Type is atom. - node = doc.createTextNode(str(data)) - result.appendChild(node) - return result diff --git a/quantumclient/common/utils.py b/quantumclient/common/utils.py index bed02e010..45c64a998 100644 --- a/quantumclient/common/utils.py +++ b/quantumclient/common/utils.py @@ -148,23 +148,27 @@ def str2dict(strdict): return _info -def http_log(_logger, args, kwargs, resp, body): - if not _logger.isEnabledFor(logging.DEBUG): - return - - string_parts = ['curl -i'] - for element in args: - if element in ('GET', 'POST'): - string_parts.append(' -X %s' % element) - else: - string_parts.append(' %s' % element) +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'])) + _logger.debug("\nREQ: %s\n" % "".join(string_parts)) - for element in kwargs['headers']: - header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) - string_parts.append(header) - _logger.debug("REQ: %s\n" % "".join(string_parts)) - if 'body' in kwargs and kwargs['body']: - _logger.debug("REQ BODY: %s\n" % (kwargs['body'])) - _logger.debug("RESP:%s\n", resp) - _logger.debug("RESP BODY:%s\n", body) +def http_log_resp(_logger, resp, body): + if not _logger.isEnabledFor(logging.DEBUG): + return + _logger.debug("RESP:%s %s\n", resp, body) diff --git a/quantumclient/openstack/common/jsonutils.py b/quantumclient/openstack/common/jsonutils.py new file mode 100644 index 000000000..ad76e0655 --- /dev/null +++ b/quantumclient/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 quantumclient.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/quantumclient/openstack/common/setup.py b/quantumclient/openstack/common/setup.py index caf06fa5b..e6f72f034 100644 --- a/quantumclient/openstack/common/setup.py +++ b/quantumclient/openstack/common/setup.py @@ -31,12 +31,13 @@ def parse_mailmap(mailmap='.mailmap'): mapping = {} if os.path.exists(mailmap): - fp = open(mailmap, 'r') - for l in fp: - l = l.strip() - if not l.startswith('#') and ' ' in l: - canonical_email, alias = l.split(' ') - mapping[alias] = canonical_email + with open(mailmap, 'r') as fp: + for l in fp: + l = l.strip() + if not l.startswith('#') and ' ' in l: + canonical_email, alias = [x for x in l.split(' ') + if x.startswith('<')] + mapping[alias] = canonical_email return mapping @@ -51,10 +52,10 @@ def canonicalize_emails(changelog, mapping): # Get requirements from the first file that exists def get_reqs_from_files(requirements_files): - reqs_in = [] for requirements_file in requirements_files: if os.path.exists(requirements_file): - return open(requirements_file, 'r').read().split('\n') + with open(requirements_file, 'r') as fil: + return fil.read().split('\n') return [] @@ -116,8 +117,12 @@ def write_requirements(): def _run_shell_command(cmd): - output = subprocess.Popen(["/bin/sh", "-c", cmd], - stdout=subprocess.PIPE) + if os.name == 'nt': + output = subprocess.Popen(["cmd.exe", "/C", cmd], + stdout=subprocess.PIPE) + else: + output = subprocess.Popen(["/bin/sh", "-c", cmd], + stdout=subprocess.PIPE) out = output.communicate() if len(out) == 0: return None @@ -135,11 +140,19 @@ def _get_git_next_version_suffix(branch_name): _run_shell_command("git fetch origin +refs/meta/*:refs/remotes/meta/*") milestone_cmd = "git show meta/openstack/release:%s" % branch_name milestonever = _run_shell_command(milestone_cmd) - if not milestonever: - milestonever = "" + if milestonever: + first_half = "%s~%s" % (milestonever, datestamp) + else: + first_half = datestamp + post_version = _get_git_post_version() - revno = post_version.split(".")[-1] - return "%s~%s.%s%s" % (milestonever, datestamp, revno_prefix, revno) + # post version should look like: + # 0.1.1.4.gcc9e28a + # where the bit after the last . is the short sha, and the bit between + # the last and second to last is the revno count + (revno, sha) = post_version.split(".")[-2:] + second_half = "%s%s.%s" % (revno_prefix, revno, sha) + return ".".join((first_half, second_half)) def _get_git_current_tag(): @@ -161,39 +174,48 @@ def _get_git_post_version(): cmd = "git --no-pager log --oneline" out = _run_shell_command(cmd) revno = len(out.split("\n")) + sha = _run_shell_command("git describe --always") else: tag_infos = tag_info.split("-") base_version = "-".join(tag_infos[:-2]) - revno = tag_infos[-2] - return "%s.%s" % (base_version, revno) + (revno, sha) = tag_infos[-2:] + return "%s.%s.%s" % (base_version, revno, sha) def write_git_changelog(): """Write a changelog based on the git changelog.""" - if os.path.isdir('.git'): - git_log_cmd = 'git log --stat' - changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() - with open("ChangeLog", "w") as changelog_file: - changelog_file.write(canonicalize_emails(changelog, mailmap)) + new_changelog = 'ChangeLog' + if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'): + if os.path.isdir('.git'): + git_log_cmd = 'git log --stat' + changelog = _run_shell_command(git_log_cmd) + mailmap = parse_mailmap() + with open(new_changelog, "w") as changelog_file: + changelog_file.write(canonicalize_emails(changelog, mailmap)) + else: + open(new_changelog, 'w').close() def generate_authors(): """Create AUTHORS file using git commits.""" - jenkins_email = 'jenkins@review.openstack.org' + jenkins_email = 'jenkins@review.(openstack|stackforge).org' old_authors = 'AUTHORS.in' new_authors = 'AUTHORS' - if os.path.isdir('.git'): - # don't include jenkins email address in AUTHORS file - git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | " - "grep -v " + jenkins_email) - changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() - with open(new_authors, 'w') as new_authors_fh: - new_authors_fh.write(canonicalize_emails(changelog, mailmap)) - if os.path.exists(old_authors): - with open(old_authors, "r") as old_authors_fh: - new_authors_fh.write('\n' + old_authors_fh.read()) + if not os.getenv('SKIP_GENERATE_AUTHORS'): + if os.path.isdir('.git'): + # don't include jenkins email address in AUTHORS file + git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | " + "egrep -v '" + jenkins_email + "'") + changelog = _run_shell_command(git_log_cmd) + mailmap = parse_mailmap() + with open(new_authors, 'w') as new_authors_fh: + new_authors_fh.write(canonicalize_emails(changelog, mailmap)) + if os.path.exists(old_authors): + with open(old_authors, "r") as old_authors_fh: + new_authors_fh.write('\n' + old_authors_fh.read()) + else: + open(new_authors, 'w').close() + _rst_template = """%(heading)s %(underline)s @@ -207,7 +229,7 @@ def generate_authors(): def read_versioninfo(project): """Read the versioninfo file. If it doesn't exist, we're in a github - zipball, and there's really know way to know what version we really + zipball, and there's really no way to know what version we really are, but that should be ok, because the utility of that should be just about nil if this code path is in use in the first place.""" versioninfo_path = os.path.join(project, 'versioninfo') @@ -221,7 +243,8 @@ def read_versioninfo(project): def write_versioninfo(project, version): """Write a simple file containing the version of the package.""" - open(os.path.join(project, 'versioninfo'), 'w').write("%s\n" % version) + with open(os.path.join(project, 'versioninfo'), 'w') as fil: + fil.write("%s\n" % version) def get_cmdclass(): @@ -312,7 +335,8 @@ def get_git_branchname(): def get_pre_version(projectname, base_version): - """Return a version which is based""" + """Return a version which is leading up to a version that will + be released in the future.""" if os.path.isdir('.git'): current_tag = _get_git_current_tag() if current_tag is not None: @@ -324,10 +348,10 @@ def get_pre_version(projectname, base_version): version_suffix = _get_git_next_version_suffix(branch_name) version = "%s~%s" % (base_version, version_suffix) write_versioninfo(projectname, version) - return version.split('~')[0] + return version else: version = read_versioninfo(projectname) - return version.split('~')[0] + return version def get_post_version(projectname): diff --git a/quantumclient/openstack/common/timeutils.py b/quantumclient/openstack/common/timeutils.py new file mode 100644 index 000000000..0f346087f --- /dev/null +++ b/quantumclient/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/quantumclient/quantum/v2_0/__init__.py b/quantumclient/quantum/v2_0/__init__.py index c719d15ba..e1b07b09e 100644 --- a/quantumclient/quantum/v2_0/__init__.py +++ b/quantumclient/quantum/v2_0/__init__.py @@ -77,11 +77,17 @@ def add_show_list_common_argument(parser): action='store_true', help=argparse.SUPPRESS) parser.add_argument( - '-F', '--fields', + '--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=[], ) + default=[]) def add_extra_argument(parser, name, _help): @@ -108,15 +114,16 @@ def parse_args_to_dict(values_specs): ''' # -- is a pseudo argument - if values_specs and values_specs[0] == '--': - del values_specs[0] + 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: + for _item in values_specs_copy: if _item.startswith('--'): if current_arg is not None: if _value_number > 1 or _list_flag: @@ -166,22 +173,49 @@ def parse_args_to_dict(values_specs): current_arg.update({'nargs': '+'}) elif _value_number == 0: current_arg.update({'action': 'store_true'}) - _parser = argparse.ArgumentParser(add_help=False) - for opt, optspec in _options.iteritems(): - _parser.add_argument(opt, **optspec) - _args = _parser.parse_args(_values_specs) + _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 = {} - for opt in _options.iterkeys(): - _opt = opt.split('--', 2)[1] - _value = getattr(_args, _opt.replace('-', '_')) - if _value is not None: - result_dict.update({_opt: _value}) + 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) + + class QuantumCommand(command.OpenStackCommand): api = 'network' log = logging.getLogger(__name__ + '.QuantumCommand') + values_specs = [] def get_client(self): return self.app.client_manager.quantum @@ -200,6 +234,26 @@ def get_parser(self, prog_name): 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) if isinstance(i, dict) + else str(i) for i in v) + data[self.resource][k] = value + elif isinstance(v, dict): + value = utils.dumps(v) + 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(QuantumCommand, show.ShowOne): """Create a resource for a given tenant @@ -213,51 +267,33 @@ class CreateCommand(QuantumCommand, show.ShowOne): def get_parser(self, prog_name): parser = super(CreateCommand, self).get_parser(prog_name) parser.add_argument( - '--tenant-id', metavar='tenant-id', + '--tenant-id', metavar='TENANT_ID', help=_('the owner tenant ID'), ) parser.add_argument( '--tenant_id', help=argparse.SUPPRESS) self.add_known_arguments(parser) - add_extra_argument(parser, 'value_specs', - 'new values for the %s' % self.resource) return parser - def add_known_arguments(self, parser): - pass - - def args2body(self, parsed_args): - return {} - def get_data(self, parsed_args): self.log.debug('get_data(%s)' % parsed_args) quantum_client = self.get_client() quantum_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) - _extra_values = parse_args_to_dict(parsed_args.value_specs) body[self.resource].update(_extra_values) obj_creator = getattr(quantum_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 = {'': ''} - for k, v in info.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) - info[k] = value - elif v is None: - info[k] = '' return zip(*sorted(info.iteritems())) @@ -276,6 +312,7 @@ def get_parser(self, prog_name): help='ID or name of %s to update' % self.resource) add_extra_argument(parser, 'value_specs', 'new values for the %s' % self.resource) + self.add_known_arguments(parser) return parser def run(self, parsed_args): @@ -283,16 +320,19 @@ def run(self, parsed_args): quantum_client = self.get_client() quantum_client.format = parsed_args.request_format value_specs = parsed_args.value_specs - if not value_specs: + dict_args = self.args2body(parsed_args).get(self.resource, {}) + dict_specs = parse_args_to_dict(value_specs) + body = {self.resource: dict(dict_args.items() + + dict_specs.items())} + if not body[self.resource]: raise exceptions.CommandError( "Must specify new values to update %s" % self.resource) - data = {self.resource: parse_args_to_dict(value_specs)} _id = find_resourceid_by_name_or_id(quantum_client, self.resource, parsed_args.id) obj_updator = getattr(quantum_client, "update_%s" % self.resource) - obj_updator(_id, data) + obj_updator(_id, body) print >>self.app.stdout, ( _('Updated %(resource)s: %(id)s') % {'id': parsed_args.id, 'resource': self.resource}) @@ -339,7 +379,7 @@ def run(self, parsed_args): class ListCommand(QuantumCommand, lister.Lister): - """List resourcs that belong to a given tenant + """List resources that belong to a given tenant """ @@ -355,11 +395,10 @@ def get_parser(self, prog_name): add_extra_argument(parser, 'filter_specs', 'filters options') return parser - def get_data(self, parsed_args): - self.log.debug('get_data(%s)' % parsed_args) + def retrieve_list(self, parsed_args): + """Retrieve a list of resources from Quantum server""" quantum_client = self.get_client() search_opts = parse_args_to_dict(parsed_args.filter_specs) - self.log.debug('search options: %s', search_opts) quantum_client.format = parsed_args.request_format fields = parsed_args.fields @@ -375,17 +414,28 @@ def get_data(self, parsed_args): search_opts.update({'verbose': 'True'}) obj_lister = getattr(quantum_client, "list_%ss" % self.resource) - data = obj_lister(**search_opts) - info = [] + collection = self.resource + "s" - if collection in data: - info = data[collection] + 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 quantum 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 not parsed_args.columns and self.list_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 @@ -394,6 +444,12 @@ def get_data(self, parsed_args): 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(QuantumCommand, show.ShowOne): """Show information of a given resource @@ -435,23 +491,9 @@ def get_data(self, parsed_args): obj_shower = getattr(quantum_client, "show_%s" % self.resource) data = obj_shower(_id, **params) + self.format_output_data(data) + resource = data[self.resource] 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 isinstance(v, dict): - value = utils.dumps(v) - data[self.resource][k] = value - elif v is None: - data[self.resource][k] = '' - return zip(*sorted(data[self.resource].iteritems())) + return zip(*sorted(resource.iteritems())) else: return None diff --git a/quantumclient/quantum/v2_0/floatingip.py b/quantumclient/quantum/v2_0/floatingip.py index 5613af866..3198dfeb7 100644 --- a/quantumclient/quantum/v2_0/floatingip.py +++ b/quantumclient/quantum/v2_0/floatingip.py @@ -52,8 +52,8 @@ class CreateFloatingIP(CreateCommand): def add_known_arguments(self, parser): parser.add_argument( - 'floating_network_id', - help='Network to allocate floating IP from') + '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') diff --git a/quantumclient/quantum/v2_0/network.py b/quantumclient/quantum/v2_0/network.py index 21efd2d84..b714ae59e 100644 --- a/quantumclient/quantum/v2_0/network.py +++ b/quantumclient/quantum/v2_0/network.py @@ -18,16 +18,18 @@ import argparse import logging +from quantumclient.common import utils from quantumclient.quantum.v2_0 import CreateCommand from quantumclient.quantum.v2_0 import DeleteCommand from quantumclient.quantum.v2_0 import ListCommand -from quantumclient.quantum.v2_0 import UpdateCommand from quantumclient.quantum.v2_0 import ShowCommand +from quantumclient.quantum.v2_0 import UpdateCommand def _format_subnets(network): try: - return '\n'.join(network['subnets']) + return '\n'.join([' '.join([s['id'], s.get('cidr', '')]) + for s in network['subnets']]) except Exception: return '' @@ -40,6 +42,29 @@ class ListNetwork(ListCommand): _formatters = {'subnets': _format_subnets, } list_columns = ['id', 'name', 'subnets'] + def extend_list(self, data, parsed_args): + """Add subnet information to a network list""" + quantum_client = self.get_client() + search_opts = {'fields': ['id', 'cidr']} + subnets = quantum_client.list_subnets(**search_opts).get('subnets', []) + 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') + + def retrieve_list(self, parsed_args): + if '--' not in parsed_args.filter_specs: + parsed_args.filter_specs.append('--') + parsed_args.filter_specs.append('--router:external=True') + return super(ListExternalNetwork, self).retrieve_list(parsed_args) + class ShowNetwork(ShowCommand): """Show information of a given network.""" @@ -69,7 +94,7 @@ def add_known_arguments(self, parser): default=argparse.SUPPRESS, help='Set the network as shared') parser.add_argument( - 'name', metavar='name', + 'name', metavar='NAME', help='Name of network to create') def args2body(self, parsed_args): diff --git a/quantumclient/quantum/v2_0/port.py b/quantumclient/quantum/v2_0/port.py index 6984f5de5..448738848 100644 --- a/quantumclient/quantum/v2_0/port.py +++ b/quantumclient/quantum/v2_0/port.py @@ -35,7 +35,7 @@ def _format_fixed_ips(port): class ListPort(ListCommand): - """List networks that belong to a given tenant.""" + """List ports that belong to a given tenant.""" resource = 'port' log = logging.getLogger(__name__ + '.ListPort') @@ -43,6 +43,33 @@ class ListPort(ListCommand): list_columns = ['id', 'name', 'mac_address', 'fixed_ips'] +class ListRouterPort(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'] + + def get_parser(self, prog_name): + parser = super(ListCommand, self).get_parser(prog_name) + quantumv20.add_show_list_common_argument(parser) + parser.add_argument( + 'id', metavar='router', + help='ID or name of router to look up') + quantumv20.add_extra_argument(parser, 'filter_specs', + 'filters options') + return parser + + def get_data(self, parsed_args): + quantum_client = self.get_client() + quantum_client.format = parsed_args.request_format + _id = quantumv20.find_resourceid_by_name_or_id( + quantum_client, 'router', parsed_args.id) + parsed_args.filter_specs.append('--device_id=%s' % _id) + return super(ListRouterPort, self).get_data(parsed_args) + + class ShowPort(ShowCommand): """Show information of a given port.""" @@ -85,7 +112,7 @@ def add_known_arguments(self, parser): action='append', help='desired IP for this port: ' 'subnet_id=,ip_address=, ' - 'can be repeated') + '(This option can be repeated.)') parser.add_argument( '--fixed_ip', action='append', @@ -134,3 +161,15 @@ class UpdatePort(UpdateCommand): resource = 'port' log = logging.getLogger(__name__ + '.UpdatePort') + + def add_known_arguments(self, parser): + parser.add_argument( + '--no-security-groups', + default=False, 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/quantumclient/quantum/v2_0/quota.py b/quantumclient/quantum/v2_0/quota.py index 4028bc720..0e749491e 100644 --- a/quantumclient/quantum/v2_0/quota.py +++ b/quantumclient/quantum/v2_0/quota.py @@ -33,7 +33,7 @@ def get_tenant_id(tenant_id, client): class DeleteQuota(QuantumCommand): - """Delete a given tenant's quotas.""" + """Delete defined quotas of a given tenant.""" api = 'network' resource = 'quota' @@ -65,7 +65,7 @@ def run(self, parsed_args): class ListQuota(QuantumCommand, lister.Lister): - """List all tenants' quotas.""" + """List defined quotas of all tenants.""" api = 'network' resource = 'quota' @@ -95,7 +95,7 @@ def get_data(self, parsed_args): class ShowQuota(QuantumCommand, show.ShowOne): - """Show information of a given resource + """Show quotas of a given tenant """ api = 'network' @@ -142,7 +142,7 @@ def get_data(self, parsed_args): class UpdateQuota(QuantumCommand, show.ShowOne): - """Update port's information.""" + """Define tenant's quotas not to use defaults.""" resource = 'quota' log = logging.getLogger(__name__ + '.UpdateQuota') @@ -164,6 +164,12 @@ def get_parser(self, prog_name): parser.add_argument( '--port', metavar='ports', help='the limit of port quota') + parser.add_argument( + '--router', metavar='routers', + help='the limit of router quota') + parser.add_argument( + '--floatingip', metavar='floatingips', + help='the limit of floating IP quota') quantumv20.add_extra_argument( parser, 'value_specs', 'new values for the %s' % self.resource) @@ -183,7 +189,7 @@ def get_data(self, parsed_args): quantum_client = self.get_client() quantum_client.format = parsed_args.request_format quota = {} - for resource in ('network', 'subnet', 'port'): + for resource in ('network', 'subnet', 'port', 'router', 'floatingip'): if getattr(parsed_args, resource): quota[resource] = self._validate_int( resource, diff --git a/quantumclient/quantum/v2_0/router.py b/quantumclient/quantum/v2_0/router.py index 9d6787d6a..a0de0c835 100644 --- a/quantumclient/quantum/v2_0/router.py +++ b/quantumclient/quantum/v2_0/router.py @@ -56,6 +56,7 @@ class CreateRouter(CreateCommand): resource = 'router' log = logging.getLogger(__name__ + '.CreateRouter') + _formatters = {'external_gateway_info': _format_external_gateway_info, } def add_known_arguments(self, parser): parser.add_argument( @@ -67,7 +68,7 @@ def add_known_arguments(self, parser): action='store_false', help=argparse.SUPPRESS) parser.add_argument( - 'name', metavar='name', + 'name', metavar='NAME', help='Name of router to create') def args2body(self, parsed_args): diff --git a/quantumclient/quantum/v2_0/subnet.py b/quantumclient/quantum/v2_0/subnet.py index 0e67cb948..9339ff58b 100644 --- a/quantumclient/quantum/v2_0/subnet.py +++ b/quantumclient/quantum/v2_0/subnet.py @@ -91,27 +91,39 @@ def add_known_arguments(self, parser): choices=[4, 6], help=argparse.SUPPRESS) parser.add_argument( - '--gateway', metavar='gateway', + '--gateway', metavar='GATEWAY_IP', help='gateway ip of this subnet') parser.add_argument( '--no-gateway', default=False, action='store_true', help='No distribution of gateway') parser.add_argument( - '--allocation-pool', - action='append', - help='Allocation pool IP addresses for this subnet: ' - 'start=,end= ' - 'can be repeated') + '--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', + action='append', dest='allocation_pools', type=utils.str2dict, help=argparse.SUPPRESS) parser.add_argument( - 'network_id', metavar='network', - help='Network id or name this subnet belongs to') + '--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( - 'cidr', metavar='cidr', + '--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): @@ -133,12 +145,14 @@ def args2body(self, parsed_args): body['subnet'].update({'tenant_id': parsed_args.tenant_id}) if parsed_args.name: body['subnet'].update({'name': parsed_args.name}) - ips = [] - if parsed_args.allocation_pool: - for ip_spec in parsed_args.allocation_pool: - ips.append(utils.str2dict(ip_spec)) - if ips: - body['subnet'].update({'allocation_pools': ips}) + 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 diff --git a/quantumclient/shell.py b/quantumclient/shell.py index 5f5eccdc8..f95507ef3 100644 --- a/quantumclient/shell.py +++ b/quantumclient/shell.py @@ -18,6 +18,7 @@ """ Command-line interface to the Quantum APIs """ + import argparse import gettext import logging @@ -32,7 +33,6 @@ from quantumclient.common import utils -gettext.install('quantum', unicode=1) VERSION = '2.0' QUANTUM_API_VERSION = '2.0' @@ -54,6 +54,8 @@ def env(*_vars, **kwargs): COMMAND_V2 = { 'net-list': utils.import_class( 'quantumclient.quantum.v2_0.network.ListNetwork'), + 'net-external-list': utils.import_class( + 'quantumclient.quantum.v2_0.network.ListExternalNetwork'), 'net-show': utils.import_class( 'quantumclient.quantum.v2_0.network.ShowNetwork'), 'net-create': utils.import_class( @@ -96,6 +98,8 @@ def env(*_vars, **kwargs): 'quantumclient.quantum.v2_0.extension.ShowExt'), 'router-list': utils.import_class( 'quantumclient.quantum.v2_0.router.ListRouter'), + 'router-port-list': utils.import_class( + 'quantumclient.quantum.v2_0.port.ListRouterPort'), 'router-show': utils.import_class( 'quantumclient.quantum.v2_0.router.ShowRouter'), 'router-create': utils.import_class( @@ -137,6 +141,8 @@ class HelpAction(argparse.Action): 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) @@ -145,7 +151,10 @@ def __call__(self, parser, namespace, values, option_string=None): factory = ep.load() cmd = factory(self, None) one_liner = cmd.get_description().split('\n')[0] - app.stdout.write(' %-25s %s\n' % (name, one_liner)) + 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) @@ -160,7 +169,8 @@ def __init__(self, apiversion): description=__doc__.strip(), version=VERSION, command_manager=CommandManager('quantum.cli'), ) - for k, v in COMMANDS[apiversion].items(): + 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 @@ -278,6 +288,24 @@ def build_option_parser(self, description, version): return parser + def _bash_completion(self): + """ + Prints all of the commands and options to stdout so that the + quantum's bash-completion script doesn't have to hard code them. + """ + 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. @@ -288,16 +316,25 @@ def run(self, argv): index = 0 command_pos = -1 help_pos = -1 + help_command_pos = -1 for arg in argv: - if arg in COMMANDS[self.api_version]: + 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 @@ -318,6 +355,46 @@ def run(self, argv): 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) + known_args, values_specs = cmd_parser.parse_known_args(sub_argv) + cmd.values_specs = values_specs + result = cmd.run(known_args) + except Exception as err: + if self.options.debug: + self.log.exception(err) + else: + self.log.error(err) + try: + self.clean_up(cmd, result, err) + except Exception as err2: + if self.options.debug: + self.log.exception(err2) + else: + self.log.error('Could not clean up: %s', 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(err3) + else: + self.log.error('Could not clean up: %s', err3) + return result + def authenticate_user(self): """Make sure the user has provided all of the authentication info we need. @@ -424,6 +501,7 @@ def configure_logging(self): def main(argv=sys.argv[1:]): + gettext.install('quantumclient', unicode=1) try: return QuantumShell(QUANTUM_API_VERSION).run(argv) except exc.QuantumClientException: diff --git a/quantumclient/tests/unit/__init__.py b/quantumclient/tests/unit/__init__.py index e69de29bb..353303fb4 100644 --- a/quantumclient/tests/unit/__init__.py +++ b/quantumclient/tests/unit/__init__.py @@ -0,0 +1,22 @@ +# 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 gettext + +# Because we installed '_' for quantum cli in shell.py, this help unittest +# have definition of '_' +gettext.install('quantumclient', unicode=1) diff --git a/quantumclient/tests/unit/test_auth.py b/quantumclient/tests/unit/test_auth.py index 9f270b5b6..477ba904f 100644 --- a/quantumclient/tests/unit/test_auth.py +++ b/quantumclient/tests/unit/test_auth.py @@ -15,16 +15,16 @@ # # vim: tabstop=4 shiftwidth=4 softtabstop=4 -import mox -from mox import ContainsKeyValue, IsA, StrContains -import unittest - import httplib2 import json +import unittest import uuid -from quantumclient.common import exceptions +import mox +from mox import ContainsKeyValue, IsA, StrContains + from quantumclient.client import HTTPClient +from quantumclient.common import exceptions USERNAME = 'testuser' diff --git a/quantumclient/tests/unit/test_casual_args.py b/quantumclient/tests/unit/test_casual_args.py index 4192ff81b..0e34168c3 100644 --- a/quantumclient/tests/unit/test_casual_args.py +++ b/quantumclient/tests/unit/test_casual_args.py @@ -33,12 +33,12 @@ def test_default_bool(self): self.assertTrue(_mydict['my_bool']) def test_bool_true(self): - _specs = ['--my-bool', 'type=bool', 'true', '--arg1', 'value1'] + _specs = ['--my-bool', 'type=bool', 'true', '--arg1', 'value1'] _mydict = quantumV20.parse_args_to_dict(_specs) - self.assertTrue(_mydict['my-bool']) + self.assertTrue(_mydict['my_bool']) def test_bool_false(self): - _specs = ['--my_bool', 'type=bool', 'false', '--arg1', 'value1'] + _specs = ['--my_bool', 'type=bool', 'false', '--arg1', 'value1'] _mydict = quantumV20.parse_args_to_dict(_specs) self.assertFalse(_mydict['my_bool']) diff --git a/quantumclient/tests/unit/test_cli20.py b/quantumclient/tests/unit/test_cli20.py index 15ca12270..4c8f61cd1 100644 --- a/quantumclient/tests/unit/test_cli20.py +++ b/quantumclient/tests/unit/test_cli20.py @@ -19,8 +19,8 @@ import unittest import mox -from mox import ContainsKeyValue from mox import Comparator +from mox import ContainsKeyValue from quantumclient.quantum import v2_0 as quantumv20 from quantumclient.v2_0.client import Client @@ -168,8 +168,9 @@ def _test_create_resource(self, resource, cmd, resstr)) self.mox.ReplayAll() cmd_parser = cmd.get_parser('create_' + resource) - parsed_args = cmd_parser.parse_args(args) - cmd.run(parsed_args) + known_args, values_specs = cmd_parser.parse_known_args(args) + cmd.values_specs = values_specs + cmd.run(known_args) self.mox.VerifyAll() self.mox.UnsetStubs() _str = self.fake_stdout.make_string() diff --git a/quantumclient/tests/unit/test_cli20_floatingips.py b/quantumclient/tests/unit/test_cli20_floatingips.py index e48d29e86..7b5912c00 100644 --- a/quantumclient/tests/unit/test_cli20_floatingips.py +++ b/quantumclient/tests/unit/test_cli20_floatingips.py @@ -57,7 +57,7 @@ def test_create_floatingip_and_port(self): # Test dashed options args = [name, '--port-id', pid] - position_names = ['floating_network_id', 'port-id'] + position_names = ['floating_network_id', 'port_id'] _str = self._test_create_resource(resource, cmd, name, myid, args, position_names, position_values) @@ -76,7 +76,7 @@ def test_create_floatingip_and_port_and_address(self): position_names, position_values) # Test dashed options args = [name, '--port-id', pid, '--fixed-ip-address', addr] - position_names = ['floating_network_id', 'port-id', 'fixed-ip-address'] + position_names = ['floating_network_id', 'port_id', 'fixed_ip_address'] _str = self._test_create_resource(resource, cmd, name, myid, args, position_names, position_values) @@ -108,7 +108,7 @@ def test_disassociate_ip(self): cmd = DisassociateFloatingIP(MyApp(sys.stdout), None) args = ['myid'] self._test_update_resource(resource, cmd, 'myid', - args, {"port_id": None} + args, {"port_id": None} ) def test_associate_ip(self): @@ -117,5 +117,5 @@ def test_associate_ip(self): cmd = AssociateFloatingIP(MyApp(sys.stdout), None) args = ['myid', 'portid'] self._test_update_resource(resource, cmd, 'myid', - args, {"port_id": "portid"} + args, {"port_id": "portid"} ) diff --git a/quantumclient/tests/unit/test_cli20_network.py b/quantumclient/tests/unit/test_cli20_network.py index 729e98274..8eebcfe6c 100644 --- a/quantumclient/tests/unit/test_cli20_network.py +++ b/quantumclient/tests/unit/test_cli20_network.py @@ -17,16 +17,20 @@ import sys +import mox +from mox import (ContainsKeyValue, IgnoreArg, IsA) + from quantumclient.common import exceptions from quantumclient.common import utils -from quantumclient.tests.unit import test_cli20 -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp from quantumclient.quantum.v2_0.network import CreateNetwork +from quantumclient.quantum.v2_0.network import DeleteNetwork +from quantumclient.quantum.v2_0.network import ListExternalNetwork from quantumclient.quantum.v2_0.network import ListNetwork -from quantumclient.quantum.v2_0.network import UpdateNetwork from quantumclient.quantum.v2_0.network import ShowNetwork -from quantumclient.quantum.v2_0.network import DeleteNetwork +from quantumclient.quantum.v2_0.network import UpdateNetwork +from quantumclient.tests.unit import test_cli20 +from quantumclient.tests.unit.test_cli20 import CLITestV20Base +from quantumclient.tests.unit.test_cli20 import MyApp class CLITestV20Network(CLITestV20Base): @@ -39,8 +43,8 @@ def test_create_network(self): args = [name, ] position_names = ['name', ] position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) def test_create_network_tenant(self): """Create net: --tenant_id tenantid myname.""" @@ -51,15 +55,15 @@ def test_create_network_tenant(self): args = ['--tenant_id', 'tenantid', name] position_names = ['name', ] position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') # Test dashed options args = ['--tenant-id', 'tenantid', name] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') def test_create_network_tags(self): """Create net: myname --tags a b.""" @@ -70,9 +74,9 @@ def test_create_network_tags(self): args = [name, '--tags', 'a', 'b'] position_names = ['name', ] position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tags=['a', 'b']) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tags=['a', 'b']) def test_create_network_state(self): """Create net: --admin_state_down myname.""" @@ -83,21 +87,23 @@ def test_create_network_state(self): args = ['--admin_state_down', name, ] position_names = ['name', ] position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - admin_state_up=False) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + admin_state_up=False) # Test dashed options args = ['--admin-state-down', name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - admin_state_up=False) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + admin_state_up=False) def test_list_nets_empty_with_column(self): resources = "networks" cmd = ListNetwork(MyApp(sys.stdout), None) self.mox.StubOutWithMock(cmd, "get_client") self.mox.StubOutWithMock(self.client.httpclient, "request") + self.mox.StubOutWithMock(ListNetwork, "extend_list") + ListNetwork.extend_list(IsA(list), IgnoreArg()) cmd.get_client().MultipleTimes().AndReturn(self.client) reses = {resources: []} resstr = self.client.serialize(reses) @@ -122,40 +128,109 @@ def test_list_nets_empty_with_column(self): _str = self.fake_stdout.make_string() self.assertEquals('\n', _str) + def _test_list_networks(self, cmd, detail=False, tags=[], + fields_1=[], fields_2=[]): + resources = "networks" + self.mox.StubOutWithMock(ListNetwork, "extend_list") + ListNetwork.extend_list(IsA(list), IgnoreArg()) + self._test_list_resources(resources, cmd, detail, tags, + fields_1, fields_2) + def test_list_nets_detail(self): """list nets: -D.""" - resources = "networks" cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) + self._test_list_networks(cmd, True) def test_list_nets_tags(self): """List nets: -- --tags a b.""" - resources = "networks" cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, tags=['a', 'b']) + self._test_list_networks(cmd, tags=['a', 'b']) def test_list_nets_detail_tags(self): """List nets: -D -- --tags a b.""" resources = "networks" cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, detail=True, tags=['a', 'b']) + self._test_list_networks(cmd, detail=True, tags=['a', 'b']) + + def _test_list_nets_extend_subnets(self, data, expected): + def setup_list_stub(resources, data, query): + reses = {resources: data} + resstr = self.client.serialize(reses) + resp = (test_cli20.MyResp(200), resstr) + path = getattr(self.client, resources + '_path') + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(resp) + + resources = "networks" + cmd = ListNetwork(test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(cmd, 'get_client') + self.mox.StubOutWithMock(self.client.httpclient, 'request') + cmd.get_client().AndReturn(self.client) + setup_list_stub('networks', data, '') + cmd.get_client().AndReturn(self.client) + setup_list_stub('subnets', + [{'id': 'mysubid1', 'cidr': '192.168.1.0/24'}, + {'id': 'mysubid2', 'cidr': '172.16.0.0/24'}, + {'id': 'mysubid3', 'cidr': '10.1.1.0/24'}], + query='fields=id&fields=cidr') + self.mox.ReplayAll() + + args = [] + cmd_parser = cmd.get_parser('list_networks') + parsed_args = cmd_parser.parse_args(args) + result = cmd.get_data(parsed_args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _result = [x for x in result[1]] + self.assertEqual(len(_result), len(expected)) + for res, exp in zip(_result, expected): + self.assertEqual(len(res), len(exp)) + for a, b in zip(res, exp): + self.assertEqual(a, b) + + def test_list_nets_extend_subnets(self): + data = [{'id': 'netid1', 'name': 'net1', 'subnets': ['mysubid1']}, + {'id': 'netid2', 'name': 'net2', 'subnets': ['mysubid2', + 'mysubid3']}] + # id, name, subnets + expected = [('netid1', 'net1', 'mysubid1 192.168.1.0/24'), + ('netid2', 'net2', + 'mysubid2 172.16.0.0/24\nmysubid3 10.1.1.0/24')] + self._test_list_nets_extend_subnets(data, expected) + + def test_list_nets_extend_subnets_no_subnet(self): + data = [{'id': 'netid1', 'name': 'net1', 'subnets': ['mysubid1']}, + {'id': 'netid2', 'name': 'net2', 'subnets': ['mysubid4']}] + # id, name, subnets + expected = [('netid1', 'net1', 'mysubid1 192.168.1.0/24'), + ('netid2', 'net2', 'mysubid4 ')] + self._test_list_nets_extend_subnets(data, expected) def test_list_nets_fields(self): """List nets: --fields a --fields b -- --fields c d.""" resources = "networks" cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, - fields_1=['a', 'b'], fields_2=['c', 'd']) + self._test_list_networks(cmd, + fields_1=['a', 'b'], fields_2=['c', 'd']) - def test_list_nets_defined_column(self): + def _test_list_nets_columns(self, cmd, returned_body, + args=['-f', 'json']): resources = 'networks' + self.mox.StubOutWithMock(ListNetwork, "extend_list") + ListNetwork.extend_list(IsA(list), IgnoreArg()) + self._test_list_columns(cmd, resources, returned_body, args=args) + + def test_list_nets_defined_column(self): cmd = ListNetwork(MyApp(sys.stdout), None) returned_body = {"networks": [{"name": "buildname3", "id": "id3", "tenant_id": "tenant_3", "subnets": []}]} - self._test_list_columns(cmd, resources, returned_body, - args=['-f', 'json', '-c', 'id']) + self._test_list_nets_columns(cmd, returned_body, + args=['-f', 'json', '-c', 'id']) _str = self.fake_stdout.make_string() returned_networks = utils.loads(_str) self.assertEquals(1, len(returned_networks)) @@ -164,13 +239,12 @@ def test_list_nets_defined_column(self): self.assertEquals("id", network.keys()[0]) def test_list_nets_with_default_column(self): - resources = 'networks' cmd = ListNetwork(MyApp(sys.stdout), None) returned_body = {"networks": [{"name": "buildname3", "id": "id3", "tenant_id": "tenant_3", "subnets": []}]} - self._test_list_columns(cmd, resources, returned_body) + self._test_list_nets_columns(cmd, returned_body) _str = self.fake_stdout.make_string() returned_networks = utils.loads(_str) self.assertEquals(1, len(returned_networks)) @@ -179,6 +253,134 @@ def test_list_nets_with_default_column(self): self.assertEquals(0, len(set(network) ^ set(cmd.list_columns))) + def test_list_external_nets_empty_with_column(self): + resources = "networks" + cmd = ListExternalNetwork(MyApp(sys.stdout), None) + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + self.mox.StubOutWithMock(ListNetwork, "extend_list") + ListNetwork.extend_list(IsA(list), IgnoreArg()) + cmd.get_client().MultipleTimes().AndReturn(self.client) + reses = {resources: []} + resstr = self.client.serialize(reses) + # url method body + query = "router%3Aexternal=True&id=myfakeid" + args = ['-c', 'id', '--', '--id', 'myfakeid'] + path = getattr(self.client, resources + "_path") + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=test_cli20.ContainsKeyValue( + 'X-Auth-Token', + test_cli20.TOKEN)).AndReturn( + (test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + + parsed_args = cmd_parser.parse_args(args) + + cmd.run(parsed_args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertEquals('\n', _str) + + def _test_list_external_nets(self, resources, cmd, + detail=False, tags=[], + fields_1=[], fields_2=[]): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + self.mox.StubOutWithMock(ListNetwork, "extend_list") + ListNetwork.extend_list(IsA(list), IgnoreArg()) + cmd.get_client().MultipleTimes().AndReturn(self.client) + reses = {resources: [{'id': 'myid1', }, + {'id': 'myid2', }, ], } + + resstr = self.client.serialize(reses) + + # url method body + query = "" + args = detail and ['-D', ] or [] + if fields_1: + for field in fields_1: + args.append('--fields') + args.append(field) + if tags: + args.append('--') + args.append("--tag") + for tag in tags: + args.append(tag) + if (not tags) and fields_2: + args.append('--') + if fields_2: + args.append("--fields") + for field in fields_2: + args.append(field) + fields_1.extend(fields_2) + for field in fields_1: + if query: + query += "&fields=" + field + else: + query = "fields=" + field + if query: + query += '&router%3Aexternal=True' + else: + query += 'router%3Aexternal=True' + for tag in tags: + if query: + query += "&tag=" + tag + else: + query = "tag=" + tag + if detail: + query = query and query + '&verbose=True' or 'verbose=True' + path = getattr(self.client, resources + "_path") + + self.client.httpclient.request( + test_cli20.end_url(path, query), 'GET', + body=None, + headers=ContainsKeyValue('X-Auth-Token', + test_cli20.TOKEN)).AndReturn( + (test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + + parsed_args = cmd_parser.parse_args(args) + cmd.run(parsed_args) + + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + + self.assertTrue('myid1' in _str) + + def test_list_external_nets_detail(self): + """list external nets: -D.""" + resources = "networks" + cmd = ListExternalNetwork(MyApp(sys.stdout), None) + self._test_list_external_nets(resources, cmd, True) + + def test_list_external_nets_tags(self): + """List external nets: -- --tags a b.""" + resources = "networks" + cmd = ListExternalNetwork(MyApp(sys.stdout), None) + self._test_list_external_nets(resources, + cmd, tags=['a', 'b']) + + def test_list_external_nets_detail_tags(self): + """List external nets: -D -- --tags a b.""" + resources = "networks" + cmd = ListExternalNetwork(MyApp(sys.stdout), None) + self._test_list_external_nets(resources, cmd, + detail=True, tags=['a', 'b']) + + def test_list_externel_nets_fields(self): + """List external nets: --fields a --fields b -- --fields c d.""" + resources = "networks" + cmd = ListExternalNetwork(MyApp(sys.stdout), None) + self._test_list_external_nets(resources, cmd, + fields_1=['a', 'b'], + fields_2=['c', 'd']) + def test_update_network_exception(self): """Update net: myid.""" resource = 'network' diff --git a/quantumclient/tests/unit/test_cli20_port.py b/quantumclient/tests/unit/test_cli20_port.py index c57aa899d..7324d2324 100644 --- a/quantumclient/tests/unit/test_cli20_port.py +++ b/quantumclient/tests/unit/test_cli20_port.py @@ -17,13 +17,17 @@ import sys -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp +from mox import ContainsKeyValue + from quantumclient.quantum.v2_0.port import CreatePort +from quantumclient.quantum.v2_0.port import DeletePort from quantumclient.quantum.v2_0.port import ListPort -from quantumclient.quantum.v2_0.port import UpdatePort +from quantumclient.quantum.v2_0.port import ListRouterPort from quantumclient.quantum.v2_0.port import ShowPort -from quantumclient.quantum.v2_0.port import DeletePort +from quantumclient.quantum.v2_0.port import UpdatePort +from quantumclient.tests.unit import test_cli20 +from quantumclient.tests.unit.test_cli20 import CLITestV20Base +from quantumclient.tests.unit.test_cli20 import MyApp class CLITestV20Port(CLITestV20Base): @@ -39,8 +43,8 @@ def test_create_port(self): position_names = ['network_id'] position_values = [] position_values.extend([netid]) - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) def test_create_port_full(self): """Create port: --mac_address mac --device_id deviceid netid.""" @@ -52,14 +56,14 @@ def test_create_port_full(self): args = ['--mac_address', 'mac', '--device_id', 'deviceid', netid] position_names = ['network_id', 'mac_address', 'device_id'] position_values = [netid, 'mac', 'deviceid'] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) # Test dashed options args = ['--mac-address', 'mac', '--device-id', 'deviceid', netid] position_names = ['network_id', 'mac_address', 'device_id'] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values) def test_create_port_tenant(self): """Create port: --tenant_id tenantid netid.""" @@ -72,15 +76,15 @@ def test_create_port_tenant(self): position_names = ['network_id'] position_values = [] position_values.extend([netid]) - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') # Test dashed options args = ['--tenant-id', 'tenantid', netid, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') def test_create_port_tags(self): """Create port: netid mac_address device_id --tags a b.""" @@ -93,9 +97,9 @@ def test_create_port_tags(self): position_names = ['network_id'] position_values = [] position_values.extend([netid]) - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tags=['a', 'b']) + self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tags=['a', 'b']) def test_list_ports(self): """List ports: -D.""" @@ -122,6 +126,100 @@ def test_list_ports_fields(self): self._test_list_resources(resources, cmd, fields_1=['a', 'b'], fields_2=['c', 'd']) + def _test_list_router_port(self, resources, cmd, + myid, detail=False, tags=[], + fields_1=[], fields_2=[]): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + reses = {resources: [{'id': 'myid1', }, + {'id': 'myid2', }, ], } + + resstr = self.client.serialize(reses) + + # url method body + query = "" + args = detail and ['-D', ] or [] + + if fields_1: + for field in fields_1: + args.append('--fields') + args.append(field) + args.append(myid) + if tags: + args.append('--') + args.append("--tag") + for tag in tags: + args.append(tag) + if (not tags) and fields_2: + args.append('--') + if fields_2: + args.append("--fields") + for field in fields_2: + args.append(field) + fields_1.extend(fields_2) + for field in fields_1: + if query: + query += "&fields=" + field + else: + query = "fields=" + field + + for tag in tags: + if query: + query += "&tag=" + tag + else: + query = "tag=" + tag + if detail: + query = query and query + '&verbose=True' or 'verbose=True' + query = query and query + '&device_id=%s' or 'device_id=%s' + path = getattr(self.client, resources + "_path") + self.client.httpclient.request( + test_cli20.end_url(path, query % myid), 'GET', + body=None, + headers=ContainsKeyValue('X-Auth-Token', + test_cli20.TOKEN)).AndReturn( + (test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + + parsed_args = cmd_parser.parse_args(args) + cmd.run(parsed_args) + + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + + self.assertTrue('myid1' in _str) + + def test_list_router_ports(self): + """List router ports: -D.""" + resources = "ports" + cmd = ListRouterPort(MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, + self.test_id, True) + + def test_list_router_ports_tags(self): + """List router ports: -- --tags a b.""" + resources = "ports" + cmd = ListRouterPort(MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, + self.test_id, tags=['a', 'b']) + + def test_list_router_ports_detail_tags(self): + """List router ports: -D -- --tags a b.""" + resources = "ports" + cmd = ListRouterPort(MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, self.test_id, + detail=True, tags=['a', 'b']) + + def test_list_router_ports_fields(self): + """List ports: --fields a --fields b -- --fields c d.""" + resources = "ports" + cmd = ListRouterPort(MyApp(sys.stdout), None) + self._test_list_router_port(resources, cmd, self.test_id, + fields_1=['a', 'b'], + fields_2=['c', 'd']) + def test_update_port(self): """Update port: myid --name myname --tags a b.""" resource = 'port' @@ -132,6 +230,14 @@ def test_update_port(self): {'name': 'myname', 'tags': ['a', 'b'], } ) + def test_update_port_security_group_off(self): + """Update port: --no-security-groups myid.""" + resource = 'port' + cmd = UpdatePort(MyApp(sys.stdout), None) + self._test_update_resource(resource, cmd, 'myid', + ['--no-security-groups', 'myid'], + {'security_groups': None}) + def test_show_port(self): """Show port: --fields id --fields name myid.""" resource = 'port' diff --git a/quantumclient/tests/unit/test_cli20_subnet.py b/quantumclient/tests/unit/test_cli20_subnet.py index 77bf0674e..997718bb2 100644 --- a/quantumclient/tests/unit/test_cli20_subnet.py +++ b/quantumclient/tests/unit/test_cli20_subnet.py @@ -17,13 +17,13 @@ import sys -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp from quantumclient.quantum.v2_0.subnet import CreateSubnet +from quantumclient.quantum.v2_0.subnet import DeleteSubnet from quantumclient.quantum.v2_0.subnet import ListSubnet -from quantumclient.quantum.v2_0.subnet import UpdateSubnet from quantumclient.quantum.v2_0.subnet import ShowSubnet -from quantumclient.quantum.v2_0.subnet import DeleteSubnet +from quantumclient.quantum.v2_0.subnet import UpdateSubnet +from quantumclient.tests.unit.test_cli20 import CLITestV20Base +from quantumclient.tests.unit.test_cli20 import MyApp class CLITestV20Subnet(CLITestV20Base): @@ -51,7 +51,7 @@ def test_create_subnet_with_no_gateway(self): myid = 'myid' netid = 'netid' cidr = 'cidrvalue' - args = ['--no-gateway', netid, cidr] + args = ['--no-gateway', netid, cidr] position_names = ['ip_version', 'network_id', 'cidr', 'gateway_ip'] position_values = [4, netid, cidr, None] _str = self._test_create_resource(resource, cmd, name, myid, args, @@ -66,7 +66,7 @@ def test_create_subnet_with_bad_gateway_option(self): netid = 'netid' cidr = 'cidrvalue' gateway = 'gatewayvalue' - args = ['--gateway', gateway, '--no-gateway', netid, cidr] + args = ['--gateway', gateway, '--no-gateway', netid, cidr] position_names = ['ip_version', 'network_id', 'cidr', 'gateway_ip'] position_values = [4, netid, cidr, None] try: @@ -151,6 +151,155 @@ def test_create_subnet_allocation_pools(self): position_names, position_values, tenant_id='tenantid') + def test_create_subnet_host_route(self): + """Create subnet: --tenant_id tenantid netid cidr. + The is + --host-route destination=172.16.1.0/24,nexthop=1.1.1.20 + """ + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--host-route', 'destination=172.16.1.0/24,nexthop=1.1.1.20', + netid, cidr] + position_names = ['ip_version', 'host_routes', 'network_id', + 'cidr'] + route = [{'destination': '172.16.1.0/24', 'nexthop': '1.1.1.20'}] + position_values = [4, route, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_host_routes(self): + """Create subnet: --tenant-id tenantid netid cidr. + The are + --host-route destination=172.16.1.0/24,nexthop=1.1.1.20 and + --host-route destination=172.17.7.0/24,nexthop=1.1.1.40 + """ + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--host-route', 'destination=172.16.1.0/24,nexthop=1.1.1.20', + '--host-route', 'destination=172.17.7.0/24,nexthop=1.1.1.40', + netid, cidr] + position_names = ['ip_version', 'host_routes', 'network_id', + 'cidr'] + routes = [{'destination': '172.16.1.0/24', 'nexthop': '1.1.1.20'}, + {'destination': '172.17.7.0/24', 'nexthop': '1.1.1.40'}] + position_values = [4, routes, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_dns_nameservers(self): + """Create subnet: --tenant-id tenantid netid cidr. + The are + --dns-nameserver 1.1.1.20 and --dns-nameserver 1.1.1.40 + """ + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--dns-nameserver', '1.1.1.20', + '--dns-nameserver', '1.1.1.40', + netid, cidr] + position_names = ['ip_version', 'dns_nameservers', 'network_id', + 'cidr'] + nameservers = ['1.1.1.20', '1.1.1.40'] + position_values = [4, nameservers, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_with_disable_dhcp(self): + """Create subnet: --tenant-id tenantid --disable-dhcp netid cidr.""" + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--disable-dhcp', + netid, cidr] + position_names = ['ip_version', 'enable_dhcp', 'network_id', + 'cidr'] + position_values = [4, False, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_merge_single_plurar(self): + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--allocation-pool', 'start=1.1.1.10,end=1.1.1.20', + netid, cidr, + '--allocation-pools', 'list=true', 'type=dict', + 'start=1.1.1.30,end=1.1.1.40'] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pools = [{'start': '1.1.1.10', 'end': '1.1.1.20'}, + {'start': '1.1.1.30', 'end': '1.1.1.40'}] + position_values = [4, pools, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_merge_plurar(self): + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + netid, cidr, + '--allocation-pools', 'list=true', 'type=dict', + 'start=1.1.1.30,end=1.1.1.40'] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pools = [{'start': '1.1.1.30', 'end': '1.1.1.40'}] + position_values = [4, pools, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + + def test_create_subnet_merge_single_single(self): + resource = 'subnet' + cmd = CreateSubnet(MyApp(sys.stdout), None) + name = 'myname' + myid = 'myid' + netid = 'netid' + cidr = 'prefixvalue' + args = ['--tenant_id', 'tenantid', + '--allocation-pool', 'start=1.1.1.10,end=1.1.1.20', + netid, cidr, + '--allocation-pool', + 'start=1.1.1.30,end=1.1.1.40'] + position_names = ['ip_version', 'allocation_pools', 'network_id', + 'cidr'] + pools = [{'start': '1.1.1.10', 'end': '1.1.1.20'}, + {'start': '1.1.1.30', 'end': '1.1.1.40'}] + position_values = [4, pools, netid, cidr] + _str = self._test_create_resource(resource, cmd, name, myid, args, + position_names, position_values, + tenant_id='tenantid') + def test_list_subnets_detail(self): """List subnets: -D.""" resources = "subnets" diff --git a/quantumclient/v2_0/client.py b/quantumclient/v2_0/client.py index 6ec752def..53b597da8 100644 --- a/quantumclient/v2_0/client.py +++ b/quantumclient/v2_0/client.py @@ -21,8 +21,11 @@ import urllib from quantumclient.client import HTTPClient +from quantumclient.common import _ +from quantumclient.common import constants from quantumclient.common import exceptions -from quantumclient.common.serializer import Serializer +from quantumclient.common import serializer + _logger = logging.getLogger(__name__) @@ -72,7 +75,8 @@ def exception_handler_v20(status_code, error_content): if ex: raise ex else: - raise exceptions.QuantumClientException(message=error_dict) + raise exceptions.QuantumClientException(status_code=status_code, + message=error_dict) else: message = None if isinstance(error_content, dict): @@ -131,23 +135,11 @@ class Client(object): tenant_name=TENANT_NAME, auth_url=KEYSTONE_URL) - >>> nets = quantum.list_nets() + >>> nets = quantum.list_networks() ... """ - #Metadata for deserializing xml - _serialization_metadata = { - "application/xml": { - "attributes": { - "network": ["id", "name"], - "port": ["id", "mac_address"], - "subnet": ["id", "prefix"]}, - "plurals": { - "networks": "network", - "ports": "port", - "subnets": "subnet", }, }, } - networks_path = "/networks" network_path = "/networks/%s" ports_path = "/ports" @@ -163,6 +155,33 @@ class Client(object): floatingips_path = "/floatingips" floatingip_path = "/floatingips/%s" + # 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', + } + + 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 @@ -422,16 +441,14 @@ def __init__(self, **kwargs): def _handle_fault_response(self, status_code, response_body): # Create exception with HTTP status code and message - error_message = response_body - _logger.debug("Error message: %s", error_message) + _logger.debug("Error message: %s", response_body) # Add deserialized error message to exception arguments try: - des_error_body = Serializer().deserialize(error_message, - self.content_type()) + des_error_body = self.deserialize(response_body, status_code) except: # If unable to deserialized body it is probably not a # Quantum error - des_error_body = {'message': error_message} + des_error_body = {'message': response_body} # Raise the appropriate exception exception_handler_v20(status_code, des_error_body) @@ -472,7 +489,8 @@ def serialize(self, data): if data is None: return None elif type(data) is dict: - return Serializer().serialize(data, self.content_type()) + return serializer.Serializer( + self.get_attr_metadata()).serialize(data, self.content_type()) else: raise Exception("unable to serialize object of type = '%s'" % type(data)) @@ -483,17 +501,16 @@ def deserialize(self, data, status_code): """ if status_code == 204: return data - return Serializer(self._serialization_metadata).deserialize( - data, self.content_type()) + return serializer.Serializer(self.get_attr_metadata()).deserialize( + data, self.content_type())['body'] - def content_type(self, format=None): + def content_type(self, _format=None): """ Returns the mime-type for either 'xml' or 'json'. Defaults to the currently set format """ - if not format: - format = self.format - return "application/%s" % (format) + _format = _format or self.format + return "application/%s" % (_format) def retry_request(self, method, action, body=None, headers=None, params=None): @@ -532,17 +549,3 @@ def post(self, action, body=None, headers=None, params=None): def put(self, action, body=None, headers=None, params=None): return self.retry_request("PUT", action, body=body, headers=headers, params=params) - -#if __name__ == '__main__': -# -# client20 = Client(username='admin', -# password='password', -# auth_url='http://localhost:5000/v2.0', -# tenant_name='admin') -# client20 = Client(token='ec796583fcad4aa690b723bc0b25270e', -# endpoint_url='http://localhost:9696') -# -# client20.tenant = 'default' -# client20.format = 'json' -# nets = client20.list_networks() -# print nets diff --git a/setup.cfg b/setup.cfg index 394b128fe..14dcb5c8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,10 +3,8 @@ all_files = 1 build-dir = doc/build source-dir = doc/source -[nosetests] -# NOTE(jkoelker) To run the test suite under nose install the following -# coverage http://pypi.python.org/pypi/coverage -# tissue http://pypi.python.org/pypi/tissue (pep8 checker) -# openstack-nose https://github.com/jkoelker/openstack-nose -verbosity=2 -detailed-errors=1 +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/setup.py b/setup.py index 7681059b0..c3d851cbd 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,16 @@ description=ShortDescription, long_description=Description, license=License, + classifiers=[ + 'Environment :: OpenStack', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + ], scripts=ProjectScripts, dependency_links=dependency_links, install_requires=setup.parse_requirements(), diff --git a/tools/pip-requires b/tools/pip-requires index 6183e4e1b..59ee8fa34 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,6 +1,7 @@ -cliff>=1.2.1 argparse +cliff>=1.2.1 httplib2 +iso8601 prettytable>=0.6.0 +pyparsing>=1.5.6,<2.0 simplejson -pyparsing diff --git a/tools/quantum.bash_completion b/tools/quantum.bash_completion new file mode 100644 index 000000000..311f53309 --- /dev/null +++ b/tools/quantum.bash_completion @@ -0,0 +1,27 @@ +_quantum_opts="" # lazy init +_quantum_flags="" # lazy init +_quantum_opts_exp="" # lazy init +_quantum() +{ + local cur prev nbc cflags + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + if [ "x$_quantum_opts" == "x" ] ; then + nbc="`quantum bash-completion`" + _quantum_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" + _quantum_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" + _quantum_opts_exp="`echo "$_quantum_opts" | sed -e "s/\s/|/g"`" + fi + + if [[ " ${COMP_WORDS[@]} " =~ " "($_quantum_opts_exp)" " && "$prev" != "help" ]] ; then + COMPLETION_CACHE=~/.quantumclient/*/*-cache + cflags="$_quantum_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') + COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) + else + COMPREPLY=($(compgen -W "${_quantum_opts}" -- ${cur})) + fi + return 0 +} +complete -F _quantum quantum diff --git a/tools/test-requires b/tools/test-requires index 8b02c3b54..b82e805ed 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -1,11 +1,11 @@ -distribute>=0.6.24 cliff-tablib>=1.0 +coverage +discover +distribute>=0.6.24 +fixtures>=0.3.12 mox -nose -nose-exclude -nosexcover -openstack.nose_plugin -nosehtmloutput pep8 +python-subunit sphinx>=1.1.2 - +testrepository>=0.0.13 +testtools>=0.9.22 diff --git a/tox.ini b/tox.ini index fce1185cf..dbb367d4d 100644 --- a/tox.ini +++ b/tox.ini @@ -3,23 +3,21 @@ envlist = py26,py27,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} - NOSE_WITH_OPENSTACK=1 - NOSE_OPENSTACK_COLOR=1 - NOSE_OPENSTACK_RED=0.05 - NOSE_OPENSTACK_YELLOW=0.025 - NOSE_OPENSTACK_SHOW_ELAPSED=1 - NOSE_OPENSTACK_STDOUT=1 -deps = -r{toxinidir}/tools/test-requires -commands = nosetests {posargs} + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C -[tox:jenkins] -downloadcache = ~/cache/pip +deps = -r{toxinidir}/tools/test-requires +commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] -commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,doc . - -[testenv:cover] -setenv = NOSE_WITH_COVERAGE=1 +commands = pep8 --repeat --show-source --ignore=E125 --exclude=.venv,.tox,dist,doc . [testenv:venv] commands = {posargs} + +[testenv:cover] +commands = python setup.py testr --coverage --testr-args='{posargs}' + +[tox:jenkins] +downloadcache = ~/cache/pip