From 50c46f61d1a76b5429c20ad61203afedc7c508bb Mon Sep 17 00:00:00 2001
From: Yong Sheng Gong <gongysh@cn.ibm.com>
Date: Sun, 24 Jun 2012 16:11:11 +0800
Subject: [PATCH] add --fixed-ip argument to create port

add --fixed-ip argument to create port and add list and dict type for unknow option
now we can use known option feature:
 quantumv2 create_port --fixed-ip subnet_id=<id>,ip_address=<ip>
--fixed-ip subnet_id=<id>, ip_address=<ip2> network_id
or unknown option feature:
one ip:
 quantumv2 create_port network_id --fixed_ips type=dict list=true subnet_id=<id>,ip_address=<ip>
two ips:
quantumv2 create_port network_id --fixed_ips type=dict subnet_id=<id>,ip_address=<ip> subnet_id=<id>,ip_address=<ip2>
to create port
Please download: https://review.openstack.org/#/c/8794/4 and
set core_plugin = quantum.db.db_base_plugin_v2.QuantumDbPluginV2 on quantum server side

Patch 2: support cliff 1.0
Patch 3: support specify auth strategy, for now, any other auth strategy than keystone will disable auth,
format port output
Patch 4: format None as '' when outputing, deal with list of dict, add QUANTUMCLIENT_DEBUG env to enable http req/resp print,
which is helpful for testing nova integration
Patch 5: fix interactive mode, and initialize_app problem

Change-Id: I693848c75055d1947862d55f4b538c1dfb1e86db
---
 quantumclient/client.py                      | 20 ++++--
 quantumclient/common/clientmanager.py        |  2 +
 quantumclient/common/command.py              |  6 ++
 quantumclient/common/utils.py                | 17 ++++-
 quantumclient/quantum/client.py              |  3 +-
 quantumclient/quantum/v2_0/__init__.py       | 40 +++++++++--
 quantumclient/quantum/v2_0/port.py           | 15 ++++-
 quantumclient/shell.py                       | 71 ++++++++++++--------
 quantumclient/tests/unit/test_casual_args.py | 13 ++++
 9 files changed, 145 insertions(+), 42 deletions(-)

diff --git a/quantumclient/client.py b/quantumclient/client.py
index b892d303e..03c590a21 100644
--- a/quantumclient/client.py
+++ b/quantumclient/client.py
@@ -20,6 +20,7 @@ try:
 except ImportError:
     import simplejson as json
 import logging
+import os
 import urlparse
 # Python 2.5 compat fix
 if not hasattr(urlparse, 'parse_qsl'):
@@ -33,6 +34,11 @@ from quantumclient.common import utils
 
 _logger = logging.getLogger(__name__)
 
+if 'QUANTUMCLIENT_DEBUG' in os.environ and os.environ['QUANTUMCLIENT_DEBUG']:
+    ch = logging.StreamHandler()
+    _logger.setLevel(logging.DEBUG)
+    _logger.addHandler(ch)
+
 
 class ServiceCatalog(object):
     """Helper methods for dealing with a Keystone Service Catalog."""
@@ -86,7 +92,8 @@ class HTTPClient(httplib2.Http):
     def __init__(self, username=None, tenant_name=None,
                  password=None, auth_url=None,
                  token=None, region_name=None, timeout=None,
-                 endpoint_url=None, insecure=False, **kwargs):
+                 endpoint_url=None, insecure=False,
+                 auth_strategy='keystone', **kwargs):
         super(HTTPClient, self).__init__(timeout=timeout)
         self.username = username
         self.tenant_name = tenant_name
@@ -96,6 +103,7 @@ class HTTPClient(httplib2.Http):
         self.auth_token = token
         self.content_type = 'application/json'
         self.endpoint_url = endpoint_url
+        self.auth_strategy = auth_strategy
         # httplib2 overrides
         self.force_exception_to_status_code = True
         self.disable_ssl_certificate_validation = insecure
@@ -126,19 +134,21 @@ class HTTPClient(httplib2.Http):
         return resp, body
 
     def do_request(self, url, method, **kwargs):
-        if not self.endpoint_url or not self.auth_token:
+        if not self.endpoint_url:
             self.authenticate()
 
         # Perform the request once. If we get a 401 back then it
         # might be because the auth token expired, so try to
         # re-authenticate and try again. If it still fails, bail.
         try:
-            kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
+            if self.auth_token:
+                kwargs.setdefault('headers', {})
+                kwargs['headers']['X-Auth-Token'] = self.auth_token
             resp, body = self._cs_request(self.endpoint_url + url, method,
                                           **kwargs)
             return resp, body
         except exceptions.Unauthorized as ex:
-            if not self.endpoint_url or not self.auth_token:
+            if not self.endpoint_url:
                 self.authenticate()
                 resp, body = self._cs_request(
                     self.management_url + url, method, **kwargs)
@@ -161,6 +171,8 @@ class HTTPClient(httplib2.Http):
             endpoint_type='adminURL')
 
     def authenticate(self):
+        if self.auth_strategy != 'keystone':
+            raise exceptions.Unauthorized(message='unknown auth strategy')
         body = {'auth': {'passwordCredentials':
                          {'username': self.username,
                           'password': self.password, },
diff --git a/quantumclient/common/clientmanager.py b/quantumclient/common/clientmanager.py
index ee2fcf3d2..1c1f1acd0 100644
--- a/quantumclient/common/clientmanager.py
+++ b/quantumclient/common/clientmanager.py
@@ -53,6 +53,7 @@ class ClientManager(object):
                  username=None, password=None,
                  region_name=None,
                  api_version=None,
+                 auth_strategy=None
                  ):
         self._token = token
         self._url = url
@@ -64,6 +65,7 @@ class ClientManager(object):
         self._region_name = region_name
         self._api_version = api_version
         self._service_catalog = None
+        self._auth_strategy = auth_strategy
         return
 
     def initialize(self):
diff --git a/quantumclient/common/command.py b/quantumclient/common/command.py
index 405f65556..f2706d019 100644
--- a/quantumclient/common/command.py
+++ b/quantumclient/common/command.py
@@ -33,3 +33,9 @@ class OpenStackCommand(Command):
             return
         else:
             return super(OpenStackCommand, self).run(parsed_args)
+
+    def get_data(self, parsed_args):
+        pass
+
+    def take_action(self, parsed_args):
+        return self.get_data(parsed_args)
diff --git a/quantumclient/common/utils.py b/quantumclient/common/utils.py
index 4044ae8af..bed02e010 100644
--- a/quantumclient/common/utils.py
+++ b/quantumclient/common/utils.py
@@ -62,9 +62,9 @@ def to_primitive(value):
         return value
 
 
-def dumps(value):
+def dumps(value, indent=None):
     try:
-        return json.dumps(value)
+        return json.dumps(value, indent=indent)
     except TypeError:
         pass
     return json.dumps(to_primitive(value))
@@ -126,17 +126,28 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
                 data = item[field_name]
             else:
                 data = getattr(item, field_name, '')
+            if data is None:
+                data = ''
             row.append(data)
     return tuple(row)
 
 
-def __str2bool(strbool):
+def str2bool(strbool):
     if strbool is None:
         return None
     else:
         return strbool.lower() == 'true'
 
 
+def str2dict(strdict):
+        '''@param strdict: key1=value1,key2=value2'''
+        _info = {}
+        for kv_str in strdict.split(","):
+            k, v = kv_str.split("=", 1)
+            _info.update({k: v})
+        return _info
+
+
 def http_log(_logger, args, kwargs, resp, body):
         if not _logger.isEnabledFor(logging.DEBUG):
             return
diff --git a/quantumclient/quantum/client.py b/quantumclient/quantum/client.py
index e2c059fc0..7aa1f1589 100644
--- a/quantumclient/quantum/client.py
+++ b/quantumclient/quantum/client.py
@@ -64,5 +64,6 @@ def make_client(instance):
                                 region_name=instance._region_name,
                                 auth_url=instance._auth_url,
                                 endpoint_url=url,
-                                token=instance._token)
+                                token=instance._token,
+                                auth_strategy=instance._auth_strategy)
         return client
diff --git a/quantumclient/quantum/v2_0/__init__.py b/quantumclient/quantum/v2_0/__init__.py
index 89c1c7a63..e1402af85 100644
--- a/quantumclient/quantum/v2_0/__init__.py
+++ b/quantumclient/quantum/v2_0/__init__.py
@@ -70,11 +70,12 @@ def parse_args_to_dict(values_specs):
     current_arg = None
     _values_specs = []
     _value_number = 0
+    _list_flag = False
     current_item = None
     for _item in values_specs:
         if _item.startswith('--'):
             if current_arg is not None:
-                if _value_number > 1:
+                if _value_number > 1 or _list_flag:
                     current_arg.update({'nargs': '+'})
                 elif _value_number == 0:
                     current_arg.update({'action': 'store_true'})
@@ -93,12 +94,16 @@ def parse_args_to_dict(values_specs):
                 _type_str = _item.split('=', 2)[1]
                 current_arg.update({'type': eval(_type_str)})
                 if _type_str == 'bool':
-                    current_arg.update({'type': utils.__str2bool})
+                    current_arg.update({'type': utils.str2bool})
+                elif _type_str == 'dict':
+                    current_arg.update({'type': utils.str2dict})
                 continue
             else:
                 raise exceptions.CommandError(
                     "invalid values_specs %s" % ' '.join(values_specs))
-
+        elif _item == 'list=true':
+            _list_flag = True
+            continue
         if not _item.startswith('--'):
             if not current_item or '=' in current_item:
                 raise exceptions.CommandError(
@@ -110,9 +115,10 @@ def parse_args_to_dict(values_specs):
                 _value_number = 1
             else:
                 _value_number = 0
+            _list_flag = False
         _values_specs.append(_item)
     if current_arg is not None:
-        if _value_number > 1:
+        if _value_number > 1 or _list_flag:
             current_arg.update({'nargs': '+'})
         elif _value_number == 0:
             current_arg.update({'action': 'store_true'})
@@ -188,6 +194,19 @@ class CreateCommand(QuantumCommand, show.ShowOne):
             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()))
 
 
@@ -334,6 +353,19 @@ class ShowCommand(QuantumCommand, show.ShowOne):
                              "show_%s" % self.resource)
         data = obj_showor(parsed_args.id, **params)
         if self.resource in data:
+            for k, v in data[self.resource].iteritems():
+                if isinstance(v, list):
+                    value = ""
+                    for _item in v:
+                        if value:
+                            value += "\n"
+                        if isinstance(_item, dict):
+                            value += utils.dumps(_item)
+                        else:
+                            value += str(_item)
+                    data[self.resource][k] = value
+                elif v is None:
+                    data[self.resource][k] = ''
             return zip(*sorted(data[self.resource].iteritems()))
         else:
             return None
diff --git a/quantumclient/quantum/v2_0/port.py b/quantumclient/quantum/v2_0/port.py
index 9b8af129e..bd213f8ae 100644
--- a/quantumclient/quantum/v2_0/port.py
+++ b/quantumclient/quantum/v2_0/port.py
@@ -17,6 +17,7 @@
 
 import logging
 
+from quantumclient import utils
 from quantumclient.quantum.v2_0 import CreateCommand
 from quantumclient.quantum.v2_0 import DeleteCommand
 from quantumclient.quantum.v2_0 import ListCommand
@@ -26,7 +27,7 @@ from quantumclient.quantum.v2_0 import UpdateCommand
 
 def _format_fixed_ips(port):
     try:
-        return '\n'.join(port['fixed_ips'])
+        return '\n'.join([utils.dumps(ip) for ip in port['fixed_ips']])
     except Exception:
         return ''
 
@@ -74,6 +75,12 @@ class CreatePort(CreateCommand):
         parser.add_argument(
             '--device-id',
             help='device id of this port')
+        parser.add_argument(
+            '--fixed-ip',
+            action='append',
+            help='desired Ip for this port: '
+            'subnet_id=<id>,ip_address=<ip>, '
+            'can be repeated')
         parser.add_argument(
             'network_id',
             help='Network id of this port belongs to')
@@ -87,6 +94,12 @@ class CreatePort(CreateCommand):
             body['port'].update({'device_id': parsed_args.device_id})
         if parsed_args.tenant_id:
             body['port'].update({'tenant_id': parsed_args.tenant_id})
+        ips = []
+        if parsed_args.fixed_ip:
+            for ip_spec in parsed_args.fixed_ip:
+                ips.append(utils.str2dict(ip_spec))
+        if ips:
+            body['port'].update({'fixed_ips': ips})
         return body
 
 
diff --git a/quantumclient/shell.py b/quantumclient/shell.py
index 975a1459f..a737b4b66 100644
--- a/quantumclient/shell.py
+++ b/quantumclient/shell.py
@@ -197,6 +197,13 @@ class QuantumShell(App):
             action='store_true',
             help='show tracebacks on errors', )
         # Global arguments
+        parser.add_argument(
+            '--os-auth-strategy', metavar='<auth-strategy>',
+            default=env('OS_AUTH_STRATEGY', default='keystone'),
+            help='Authentication strategy (Env: OS_AUTH_STRATEGY'
+            ', default keystone). For now, any other value will'
+            ' disable the authentication')
+
         parser.add_argument(
             '--os-auth-url', metavar='<auth-url>',
             default=env('OS_AUTH_URL'),
@@ -272,41 +279,46 @@ class QuantumShell(App):
         """Make sure the user has provided all of the authentication
         info we need.
         """
+        if self.options.os_auth_strategy == 'keystone':
+            if self.options.os_token or self.options.os_url:
+                # Token flow auth takes priority
+                if not self.options.os_token:
+                    raise exc.CommandError(
+                        "You must provide a token via"
+                        " either --os-token or env[OS_TOKEN]")
 
-        if self.options.os_token or self.options.os_url:
-            # Token flow auth takes priority
-            if not self.options.os_token:
-                raise exc.CommandError(
-                    "You must provide a token via"
-                    " either --os-token or env[OS_TOKEN]")
+                if not self.options.os_url:
+                    raise exc.CommandError(
+                        "You must provide a service URL via"
+                        " either --os-url or env[OS_URL]")
 
+            else:
+                # Validate password flow auth
+                if not self.options.os_username:
+                    raise exc.CommandError(
+                        "You must provide a username via"
+                        " either --os-username or env[OS_USERNAME]")
+
+                if not self.options.os_password:
+                    raise exc.CommandError(
+                        "You must provide a password via"
+                        " either --os-password or env[OS_PASSWORD]")
+
+                if not (self.options.os_tenant_name):
+                    raise exc.CommandError(
+                        "You must provide a tenant_name via"
+                        " either --os-tenant-name or via env[OS_TENANT_NAME]")
+
+                if not self.options.os_auth_url:
+                    raise exc.CommandError(
+                        "You must provide an auth url via"
+                        " either --os-auth-url or via env[OS_AUTH_URL]")
+        else:   # not keystone
             if not self.options.os_url:
                 raise exc.CommandError(
                     "You must provide a service URL via"
                     " either --os-url or env[OS_URL]")
 
-        else:
-            # Validate password flow auth
-            if not self.options.os_username:
-                raise exc.CommandError(
-                    "You must provide a username via"
-                    " either --os-username or env[OS_USERNAME]")
-
-            if not self.options.os_password:
-                raise exc.CommandError(
-                    "You must provide a password via"
-                    " either --os-password or env[OS_PASSWORD]")
-
-            if not (self.options.os_tenant_name):
-                raise exc.CommandError(
-                    "You must provide a tenant_name via"
-                    " either --os-tenant-name or via env[OS_TENANT_NAME]")
-
-            if not self.options.os_auth_url:
-                raise exc.CommandError(
-                    "You must provide an auth url via"
-                    " either --os-auth-url or via env[OS_AUTH_URL]")
-
         self.client_manager = clientmanager.ClientManager(
             token=self.options.os_token,
             url=self.options.os_url,
@@ -315,7 +327,8 @@ class QuantumShell(App):
             username=self.options.os_username,
             password=self.options.os_password,
             region_name=self.options.os_region_name,
-            api_version=self.api_version, )
+            api_version=self.api_version,
+            auth_strategy=self.options.os_auth_strategy, )
         return
 
     def initialize_app(self, argv):
diff --git a/quantumclient/tests/unit/test_casual_args.py b/quantumclient/tests/unit/test_casual_args.py
index 695aa0aff..4192ff81b 100644
--- a/quantumclient/tests/unit/test_casual_args.py
+++ b/quantumclient/tests/unit/test_casual_args.py
@@ -57,3 +57,16 @@ class CLITestArgs(unittest.TestCase):
         _specs = ['--tag=t', '--arg1', 'value1']
         self.assertEqual('value1',
                          quantumV20.parse_args_to_dict(_specs)['arg1'])
+
+    def test_dict_arg(self):
+        _specs = ['--tag=t', '--arg1', 'type=dict', 'key1=value1,key2=value2']
+        arg1 = quantumV20.parse_args_to_dict(_specs)['arg1']
+        self.assertEqual('value1', arg1['key1'])
+        self.assertEqual('value2', arg1['key2'])
+
+    def test_list_of_dict_arg(self):
+        _specs = ['--tag=t', '--arg1', 'type=dict',
+                  'list=true', 'key1=value1,key2=value2']
+        arg1 = quantumV20.parse_args_to_dict(_specs)['arg1']
+        self.assertEqual('value1', arg1[0]['key1'])
+        self.assertEqual('value2', arg1[0]['key2'])