2013-12-06 10:47:41 +10:30
|
|
|
#
|
|
|
|
# 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.
|
|
|
|
|
2015-03-18 17:37:54 +08:00
|
|
|
import contextlib
|
2015-12-17 16:24:33 +02:00
|
|
|
import os
|
1999-11-30 22:24:27 -05:00
|
|
|
import re
|
2013-03-01 23:07:18 +00:00
|
|
|
import textwrap
|
2015-03-18 17:37:54 +08:00
|
|
|
import time
|
2011-08-03 17:41:33 -04:00
|
|
|
|
2015-01-26 16:37:53 +02:00
|
|
|
from oslo_serialization import jsonutils
|
|
|
|
from oslo_utils import encodeutils
|
2018-08-08 15:00:34 +09:00
|
|
|
from oslo_utils import uuidutils
|
2012-03-06 00:33:37 +00:00
|
|
|
import prettytable
|
2013-06-24 08:32:05 -05:00
|
|
|
import six
|
2016-08-01 17:38:14 +03:00
|
|
|
from six.moves.urllib import parse
|
2012-03-06 00:33:37 +00:00
|
|
|
|
2011-08-22 15:13:26 -04:00
|
|
|
from novaclient import exceptions
|
2014-10-16 01:19:48 +03:00
|
|
|
from novaclient.i18n import _
|
2011-08-22 15:13:26 -04:00
|
|
|
|
2011-08-03 17:41:33 -04:00
|
|
|
|
1999-11-30 22:24:27 -05:00
|
|
|
VALID_KEY_REGEX = re.compile(r"[\w\.\- :]+$", re.UNICODE)
|
|
|
|
|
2011-12-21 19:25:19 +00:00
|
|
|
|
2015-12-17 16:24:33 +02:00
|
|
|
def env(*args, **kwargs):
|
|
|
|
"""Returns the first environment variable set.
|
|
|
|
|
|
|
|
If all are empty, defaults to '' or keyword arg `default`.
|
|
|
|
"""
|
|
|
|
for arg in args:
|
|
|
|
value = os.environ.get(arg)
|
|
|
|
if value:
|
|
|
|
return value
|
|
|
|
return kwargs.get('default', '')
|
|
|
|
|
|
|
|
|
|
|
|
def get_service_type(f):
|
|
|
|
"""Retrieves service type from function."""
|
|
|
|
return getattr(f, 'service_type', None)
|
|
|
|
|
|
|
|
|
|
|
|
def unauthenticated(func):
|
|
|
|
"""Adds 'unauthenticated' attribute to decorated function.
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
|
|
|
>>> @unauthenticated
|
|
|
|
... def mymethod(f):
|
|
|
|
... pass
|
|
|
|
"""
|
|
|
|
func.unauthenticated = True
|
|
|
|
return func
|
|
|
|
|
|
|
|
|
|
|
|
def isunauthenticated(func):
|
|
|
|
"""Checks if the function does not require authentication.
|
|
|
|
|
|
|
|
Mark such functions with the `@unauthenticated` decorator.
|
|
|
|
|
|
|
|
:returns: bool
|
|
|
|
"""
|
|
|
|
return getattr(func, 'unauthenticated', False)
|
|
|
|
|
|
|
|
|
|
|
|
def arg(*args, **kwargs):
|
|
|
|
"""Decorator for CLI args.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
>>> @arg("name", help="Name of the new entity")
|
|
|
|
... def entity_create(args):
|
|
|
|
... pass
|
|
|
|
"""
|
|
|
|
def _decorator(func):
|
|
|
|
add_arg(func, *args, **kwargs)
|
|
|
|
return func
|
|
|
|
return _decorator
|
|
|
|
|
|
|
|
|
|
|
|
def add_arg(func, *args, **kwargs):
|
|
|
|
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
|
|
|
|
|
|
|
if not hasattr(func, 'arguments'):
|
|
|
|
func.arguments = []
|
|
|
|
|
|
|
|
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
|
|
|
# tests.
|
|
|
|
if (args, kwargs) not in func.arguments:
|
|
|
|
# Because of the semantics of decorator composition if we just append
|
|
|
|
# to the options list positional options will appear to be backwards.
|
|
|
|
func.arguments.insert(0, (args, kwargs))
|
|
|
|
|
|
|
|
|
|
|
|
def service_type(stype):
|
|
|
|
"""Adds 'service_type' attribute to decorated function.
|
2017-07-04 17:35:31 +01:00
|
|
|
|
2015-12-17 16:24:33 +02:00
|
|
|
Usage:
|
2017-07-04 17:35:31 +01:00
|
|
|
|
2015-12-17 16:24:33 +02:00
|
|
|
.. code-block:: python
|
2017-07-04 17:35:31 +01:00
|
|
|
|
2015-12-17 16:24:33 +02:00
|
|
|
@service_type('volume')
|
|
|
|
def mymethod(f):
|
|
|
|
...
|
|
|
|
"""
|
|
|
|
def inner(f):
|
|
|
|
f.service_type = stype
|
|
|
|
return f
|
|
|
|
return inner
|
|
|
|
|
|
|
|
|
|
|
|
def pretty_choice_list(l):
|
|
|
|
return ', '.join("'%s'" % i for i in l)
|
|
|
|
|
|
|
|
|
2013-11-13 12:59:18 +00:00
|
|
|
def pretty_choice_dict(d):
|
|
|
|
"""Returns a formatted dict as 'key=value'."""
|
2015-12-17 16:24:33 +02:00
|
|
|
return pretty_choice_list(['%s=%s' % (k, d[k]) for k in sorted(d.keys())])
|
2013-11-13 12:59:18 +00:00
|
|
|
|
|
|
|
|
2013-05-14 11:21:08 +08:00
|
|
|
def print_list(objs, fields, formatters={}, sortby_index=None):
|
2013-03-04 18:11:54 +01:00
|
|
|
if sortby_index is None:
|
2012-06-15 15:12:23 -03:00
|
|
|
sortby = None
|
|
|
|
else:
|
|
|
|
sortby = fields[sortby_index]
|
2011-10-30 17:20:47 -05:00
|
|
|
mixed_case_fields = ['serverId']
|
2011-08-03 17:41:33 -04:00
|
|
|
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
2012-06-11 09:57:00 -07:00
|
|
|
pt.align = 'l'
|
2011-08-03 17:41:33 -04:00
|
|
|
|
|
|
|
for o in objs:
|
|
|
|
row = []
|
|
|
|
for field in fields:
|
|
|
|
if field in formatters:
|
|
|
|
row.append(formatters[field](o))
|
|
|
|
else:
|
2011-10-30 17:20:47 -05:00
|
|
|
if field in mixed_case_fields:
|
|
|
|
field_name = field.replace(' ', '_')
|
2011-11-03 22:44:22 -05:00
|
|
|
else:
|
2011-10-30 17:20:47 -05:00
|
|
|
field_name = field.lower().replace(' ', '_')
|
2011-08-03 17:41:33 -04:00
|
|
|
data = getattr(o, field_name, '')
|
2014-01-07 22:08:28 +05:30
|
|
|
if data is None:
|
|
|
|
data = '-'
|
2015-07-22 10:01:44 +08:00
|
|
|
# '\r' would break the table, so remove it.
|
2015-11-21 01:33:10 +08:00
|
|
|
data = six.text_type(data).replace("\r", "")
|
2011-08-03 17:41:33 -04:00
|
|
|
row.append(data)
|
|
|
|
pt.add_row(row)
|
|
|
|
|
2013-01-17 11:51:46 +08:00
|
|
|
if sortby is not None:
|
2014-08-27 18:08:14 +03:00
|
|
|
result = encodeutils.safe_encode(pt.get_string(sortby=sortby))
|
2013-01-17 11:51:46 +08:00
|
|
|
else:
|
2014-08-27 18:08:14 +03:00
|
|
|
result = encodeutils.safe_encode(pt.get_string())
|
|
|
|
|
|
|
|
if six.PY3:
|
|
|
|
result = result.decode()
|
2013-09-25 15:48:42 +08:00
|
|
|
|
|
|
|
print(result)
|
2011-08-03 17:41:33 -04:00
|
|
|
|
|
|
|
|
2013-10-23 14:43:17 +11:00
|
|
|
def _flatten(data, prefix=None):
|
|
|
|
"""Flatten a dict, using name as a prefix for the keys of dict.
|
|
|
|
|
|
|
|
>>> _flatten('cpu_info', {'arch':'x86_64'})
|
|
|
|
[('cpu_info_arch': 'x86_64')]
|
|
|
|
|
|
|
|
"""
|
|
|
|
if isinstance(data, dict):
|
2016-12-09 11:04:16 +08:00
|
|
|
for key, value in data.items():
|
2013-10-23 14:43:17 +11:00
|
|
|
new_key = '%s_%s' % (prefix, key) if prefix else key
|
2016-08-05 02:08:36 +09:00
|
|
|
if isinstance(value, (dict, list)) and value:
|
2013-10-23 14:43:17 +11:00
|
|
|
for item in _flatten(value, new_key):
|
|
|
|
yield item
|
|
|
|
else:
|
|
|
|
yield new_key, value
|
|
|
|
else:
|
|
|
|
yield prefix, data
|
|
|
|
|
|
|
|
|
|
|
|
def flatten_dict(data):
|
|
|
|
"""Return a new dict whose sub-dicts have been merged into the
|
|
|
|
original. Each of the parents keys are prepended to the child's
|
|
|
|
to prevent collisions. Any string elements will be JSON parsed
|
|
|
|
before flattening.
|
|
|
|
|
|
|
|
>>> flatten_dict({'service': {'host':'cloud9@compute-068', 'id': 143}})
|
|
|
|
{'service_host': colud9@compute-068', 'service_id': 143}
|
|
|
|
|
|
|
|
"""
|
|
|
|
data = data.copy()
|
|
|
|
# Try and decode any nested JSON structures.
|
2016-12-09 11:04:16 +08:00
|
|
|
for key, value in data.items():
|
2013-10-23 14:43:17 +11:00
|
|
|
if isinstance(value, six.string_types):
|
|
|
|
try:
|
2018-07-31 11:59:54 +09:00
|
|
|
data[key] = jsonutils.loads(value)
|
2013-10-23 14:43:17 +11:00
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
return dict(_flatten(data))
|
|
|
|
|
|
|
|
|
2013-06-10 14:15:06 +05:30
|
|
|
def print_dict(d, dict_property="Property", dict_value="Value", wrap=0):
|
|
|
|
pt = prettytable.PrettyTable([dict_property, dict_value], caching=False)
|
2012-06-11 09:57:00 -07:00
|
|
|
pt.align = 'l'
|
2013-10-14 15:27:01 +11:00
|
|
|
for k, v in sorted(d.items()):
|
2013-01-10 01:27:33 +00:00
|
|
|
# convert dict to str to check length
|
2013-12-30 07:37:35 -08:00
|
|
|
if isinstance(v, (dict, list)):
|
2017-03-03 14:54:11 +08:00
|
|
|
v = jsonutils.dumps(v, ensure_ascii=False)
|
2013-03-01 23:07:18 +00:00
|
|
|
if wrap > 0:
|
2015-11-21 01:33:10 +08:00
|
|
|
v = textwrap.fill(six.text_type(v), wrap)
|
2013-01-10 01:27:33 +00:00
|
|
|
# if value has a newline, add in multiple rows
|
|
|
|
# e.g. fault with stacktrace
|
2015-07-22 10:01:44 +08:00
|
|
|
if v and isinstance(v, six.string_types) and (r'\n' in v or '\r' in v):
|
|
|
|
# '\r' would break the table, so remove it.
|
|
|
|
if '\r' in v:
|
|
|
|
v = v.replace('\r', '')
|
2013-01-10 01:27:33 +00:00
|
|
|
lines = v.strip().split(r'\n')
|
|
|
|
col1 = k
|
|
|
|
for line in lines:
|
|
|
|
pt.add_row([col1, line])
|
|
|
|
col1 = ''
|
|
|
|
else:
|
2014-01-07 22:08:28 +05:30
|
|
|
if v is None:
|
|
|
|
v = '-'
|
2013-01-10 01:27:33 +00:00
|
|
|
pt.add_row([k, v])
|
2013-12-30 07:37:35 -08:00
|
|
|
|
2014-08-27 18:08:14 +03:00
|
|
|
result = encodeutils.safe_encode(pt.get_string())
|
|
|
|
|
|
|
|
if six.PY3:
|
|
|
|
result = result.decode()
|
2014-01-13 12:24:55 +01:00
|
|
|
|
2013-12-30 07:37:35 -08:00
|
|
|
print(result)
|
2011-08-22 15:13:26 -04:00
|
|
|
|
|
|
|
|
2016-02-10 17:37:25 +02:00
|
|
|
def find_resource(manager, name_or_id, wrap_exception=True, **find_args):
|
2011-08-22 15:13:26 -04:00
|
|
|
"""Helper for the _find_* methods."""
|
2015-07-19 18:31:45 +08:00
|
|
|
# for str id which is not uuid (for Flavor, Keypair and hypervsior in cells
|
|
|
|
# environments search currently)
|
2014-01-10 11:43:32 +09:00
|
|
|
if getattr(manager, 'is_alphanum_id_allowed', False):
|
|
|
|
try:
|
|
|
|
return manager.get(name_or_id)
|
|
|
|
except exceptions.NotFound:
|
|
|
|
pass
|
|
|
|
|
2015-11-18 11:21:33 +00:00
|
|
|
# first try to get entity as uuid
|
2011-08-22 15:13:26 -04:00
|
|
|
try:
|
2014-08-27 18:08:14 +03:00
|
|
|
tmp_id = encodeutils.safe_encode(name_or_id)
|
|
|
|
|
|
|
|
if six.PY3:
|
|
|
|
tmp_id = tmp_id.decode()
|
|
|
|
|
2018-08-08 15:00:34 +09:00
|
|
|
if uuidutils.is_uuid_like(tmp_id):
|
|
|
|
return manager.get(tmp_id)
|
|
|
|
except (TypeError, exceptions.NotFound):
|
2011-08-22 15:13:26 -04:00
|
|
|
pass
|
|
|
|
|
2015-11-18 11:21:33 +00:00
|
|
|
# then try to get entity as name
|
2012-03-06 00:33:37 +00:00
|
|
|
try:
|
2011-12-13 16:59:15 +01:00
|
|
|
try:
|
2012-08-08 11:18:13 -07:00
|
|
|
resource = getattr(manager, 'resource_class', None)
|
|
|
|
name_attr = resource.NAME_ATTR if resource else 'name'
|
|
|
|
kwargs = {name_attr: name_or_id}
|
2013-09-02 11:03:50 +09:00
|
|
|
kwargs.update(find_args)
|
2012-08-08 11:18:13 -07:00
|
|
|
return manager.find(**kwargs)
|
2015-02-05 02:32:58 +00:00
|
|
|
except exceptions.NotFound:
|
|
|
|
pass
|
|
|
|
|
2015-11-18 11:21:33 +00:00
|
|
|
# then try to find entity by human_id
|
2015-02-05 02:32:58 +00:00
|
|
|
try:
|
|
|
|
return manager.find(human_id=name_or_id, **find_args)
|
2011-12-13 16:59:15 +01:00
|
|
|
except exceptions.NotFound:
|
2015-11-18 11:21:33 +00:00
|
|
|
pass
|
2012-04-09 20:32:37 +00:00
|
|
|
except exceptions.NoUniqueMatch:
|
2013-12-12 23:17:28 -05:00
|
|
|
msg = (_("Multiple %(class)s matches found for '%(name)s', use an ID "
|
|
|
|
"to be more specific.") %
|
|
|
|
{'class': manager.resource_class.__name__.lower(),
|
|
|
|
'name': name_or_id})
|
2016-02-10 17:37:25 +02:00
|
|
|
if wrap_exception:
|
|
|
|
raise exceptions.CommandError(msg)
|
|
|
|
raise exceptions.NoUniqueMatch(msg)
|
2011-12-15 19:39:33 +00:00
|
|
|
|
2015-11-18 11:21:33 +00:00
|
|
|
# finally try to get entity as integer id
|
|
|
|
try:
|
|
|
|
return manager.get(int(name_or_id))
|
|
|
|
except (TypeError, ValueError, exceptions.NotFound):
|
|
|
|
msg = (_("No %(class)s with a name or ID of '%(name)s' exists.") %
|
|
|
|
{'class': manager.resource_class.__name__.lower(),
|
|
|
|
'name': name_or_id})
|
2016-02-10 17:37:25 +02:00
|
|
|
if wrap_exception:
|
|
|
|
raise exceptions.CommandError(msg)
|
|
|
|
raise exceptions.NotFound(404, msg)
|
2015-11-18 11:21:33 +00:00
|
|
|
|
2011-12-15 19:39:33 +00:00
|
|
|
|
2016-04-27 20:13:45 +08:00
|
|
|
def format_servers_list_networks(server):
|
2011-12-15 19:39:33 +00:00
|
|
|
output = []
|
|
|
|
for (network, addresses) in server.networks.items():
|
|
|
|
if len(addresses) == 0:
|
|
|
|
continue
|
|
|
|
addresses_csv = ', '.join(addresses)
|
|
|
|
group = "%s=%s" % (network, addresses_csv)
|
|
|
|
output.append(group)
|
|
|
|
|
|
|
|
return '; '.join(output)
|
2011-12-21 19:25:19 +00:00
|
|
|
|
|
|
|
|
2016-04-27 20:13:45 +08:00
|
|
|
def format_security_groups(groups):
|
2013-02-07 19:24:11 +11:00
|
|
|
return ', '.join(group['name'] for group in groups)
|
|
|
|
|
|
|
|
|
|
|
|
def _format_field_name(attr):
|
|
|
|
"""Format an object attribute in a human-friendly way."""
|
|
|
|
# Split at ':' and leave the extension name as-is.
|
|
|
|
parts = attr.rsplit(':', 1)
|
|
|
|
name = parts[-1].replace('_', ' ')
|
|
|
|
# Don't title() on mixed case
|
|
|
|
if name.isupper() or name.islower():
|
|
|
|
name = name.title()
|
|
|
|
parts[-1] = name
|
|
|
|
return ': '.join(parts)
|
|
|
|
|
|
|
|
|
2016-04-27 20:13:45 +08:00
|
|
|
def make_field_formatter(attr, filters=None):
|
2013-02-07 19:24:11 +11:00
|
|
|
"""
|
|
|
|
Given an object attribute, return a formatted field name and a
|
|
|
|
formatter suitable for passing to print_list.
|
|
|
|
|
|
|
|
Optionally pass a dict mapping attribute names to a function. The function
|
|
|
|
will be passed the value of the attribute and should return the string to
|
|
|
|
display.
|
|
|
|
"""
|
|
|
|
filter_ = None
|
|
|
|
if filters:
|
|
|
|
filter_ = filters.get(attr)
|
|
|
|
|
|
|
|
def get_field(obj):
|
|
|
|
field = getattr(obj, attr, '')
|
|
|
|
if field and filter_:
|
|
|
|
field = filter_(field)
|
|
|
|
return field
|
|
|
|
|
|
|
|
name = _format_field_name(attr)
|
|
|
|
formatter = get_field
|
|
|
|
return name, formatter
|
|
|
|
|
|
|
|
|
2011-12-21 19:25:19 +00:00
|
|
|
def safe_issubclass(*args):
|
|
|
|
"""Like issubclass, but will just return False if not a class."""
|
|
|
|
|
|
|
|
try:
|
|
|
|
if issubclass(*args):
|
|
|
|
return True
|
|
|
|
except TypeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
return False
|
2011-12-29 15:37:05 -05:00
|
|
|
|
|
|
|
|
2018-08-06 00:16:29 +09:00
|
|
|
def _get_resource_string(resource):
|
|
|
|
if hasattr(resource, 'human_id') and resource.human_id:
|
|
|
|
if hasattr(resource, 'id') and resource.id:
|
|
|
|
return "%s (%s)" % (resource.human_id, resource.id)
|
|
|
|
else:
|
|
|
|
return resource.human_id
|
|
|
|
elif hasattr(resource, 'id') and resource.id:
|
|
|
|
return resource.id
|
|
|
|
else:
|
|
|
|
return resource
|
|
|
|
|
|
|
|
|
2014-10-22 16:04:03 +02:00
|
|
|
def do_action_on_many(action, resources, success_msg, error_msg):
|
|
|
|
"""Helper to run an action on many resources."""
|
|
|
|
failure_flag = False
|
|
|
|
|
|
|
|
for resource in resources:
|
|
|
|
try:
|
|
|
|
action(resource)
|
2018-08-06 00:16:29 +09:00
|
|
|
print(success_msg % _get_resource_string(resource))
|
2014-10-22 16:04:03 +02:00
|
|
|
except Exception as e:
|
|
|
|
failure_flag = True
|
2016-07-05 19:24:21 +05:30
|
|
|
print(encodeutils.safe_encode(six.text_type(e)))
|
2014-10-22 16:04:03 +02:00
|
|
|
|
|
|
|
if failure_flag:
|
|
|
|
raise exceptions.CommandError(error_msg)
|
|
|
|
|
|
|
|
|
2013-07-31 05:47:53 -04:00
|
|
|
def is_integer_like(val):
|
|
|
|
"""Returns validation of a value as an integer."""
|
|
|
|
try:
|
2014-06-17 15:07:04 +08:00
|
|
|
int(val)
|
2013-07-31 05:47:53 -04:00
|
|
|
return True
|
|
|
|
except (TypeError, ValueError, AttributeError):
|
|
|
|
return False
|
1999-11-30 22:24:27 -05:00
|
|
|
|
|
|
|
|
|
|
|
def validate_flavor_metadata_keys(keys):
|
|
|
|
for key in keys:
|
|
|
|
valid_name = VALID_KEY_REGEX.match(key)
|
|
|
|
if not valid_name:
|
|
|
|
msg = _('Invalid key: "%s". Keys may only contain letters, '
|
|
|
|
'numbers, spaces, underscores, periods, colons and '
|
|
|
|
'hyphens.')
|
|
|
|
raise exceptions.CommandError(msg % key)
|
2015-03-18 17:37:54 +08:00
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def record_time(times, enabled, *args):
|
|
|
|
"""Record the time of a specific action.
|
|
|
|
|
|
|
|
:param times: A list of tuples holds time data.
|
|
|
|
:param enabled: Whether timing is enabled.
|
2017-07-04 17:35:31 +01:00
|
|
|
:param args: Other data to be stored besides time data, these args
|
|
|
|
will be joined to a string.
|
2015-03-18 17:37:54 +08:00
|
|
|
"""
|
|
|
|
if not enabled:
|
|
|
|
yield
|
|
|
|
else:
|
|
|
|
start = time.time()
|
|
|
|
yield
|
|
|
|
end = time.time()
|
|
|
|
times.append((' '.join(args), start, end))
|
2016-08-01 17:38:14 +03:00
|
|
|
|
|
|
|
|
|
|
|
def prepare_query_string(params):
|
|
|
|
"""Convert dict params to query string"""
|
2017-10-27 15:28:15 +08:00
|
|
|
# Transform the dict to a sequence of two-element tuples in fixed
|
|
|
|
# order, then the encoded string will be consistent in Python 2&3.
|
|
|
|
if not params:
|
|
|
|
return ''
|
2016-08-01 17:38:14 +03:00
|
|
|
params = sorted(params.items(), key=lambda x: x[0])
|
|
|
|
return '?%s' % parse.urlencode(params) if params else ''
|
2017-10-27 15:28:15 +08:00
|
|
|
|
|
|
|
|
|
|
|
def get_url_with_filter(url, filters):
|
|
|
|
query_string = prepare_query_string(filters)
|
|
|
|
url = "%s%s" % (url, query_string)
|
|
|
|
return url
|