Update APMEC client code

This commit is contained in:
Tung Doan 2018-05-07 14:07:29 +02:00
commit 21839c4c3b
66 changed files with 7945 additions and 0 deletions

26
HACKING.rst Normal file
View File

@ -0,0 +1,26 @@
Apmec Style Commandments
================================
- Step 1: Read the OpenStack Style Commandments
http://docs.openstack.org/developer/hacking/
- Step 2: Read on
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

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
include tox.ini
include LICENSE README.rst HACKING.rst
include AUTHORS
include ChangeLog
include tools/*
recursive-include tests *

1
README.md Normal file
View File

@ -0,0 +1 @@
# python-apmecclient

10
README.rst Normal file
View File

@ -0,0 +1,10 @@
========================
Team and repository tags
========================
.. image:: http://governance.openstack.org/badges/python-apmecclient.svg
:target: http://governance.openstack.org/reference/tags/index.html
.. Change things from this point on
This is the client API library for Apmec.

20
apmec_test.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
set -x
function die() {
local exitcode=$?
set +o xtrace
echo $@
exit $exitcode
}
noauth_tenant_id=me
if [ $1 == 'noauth' ]; then
NOAUTH="--tenant_id $noauth_tenant_id"
else
NOAUTH=
fi
FORMAT=" --request-format xml"
# test the CRUD of xxx
# TODO(yamahata)

0
apmecclient/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,72 @@
# Copyright 2012 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from apmecclient.common._i18n import _
from apmecclient.common import exceptions
from apmecclient.common import utils
API_NAME = 'mec-orchestration'
API_VERSIONS = {
'1.0': 'apmecclient.v1_0.client.Client',
}
def make_client(instance):
"""Returns an apmec client."""
apmec_client = utils.get_client_class(
API_NAME,
instance._api_version[API_NAME],
API_VERSIONS,
)
instance.initialize()
url = instance._url
url = url.rstrip("/")
if '1.0' == instance._api_version[API_NAME]:
client = apmec_client(username=instance._username,
tenant_name=instance._tenant_name,
password=instance._password,
region_name=instance._region_name,
auth_url=instance._auth_url,
endpoint_url=url,
endpoint_type=instance._endpoint_type,
token=instance._token,
auth_strategy=instance._auth_strategy,
insecure=instance._insecure,
ca_cert=instance._ca_cert,
retries=instance._retries,
raise_errors=instance._raise_errors,
session=instance._session,
auth=instance._auth)
return client
else:
raise exceptions.UnsupportedVersion(_("API version %s is not "
"supported") %
instance._api_version[API_NAME])
def Client(api_version, *args, **kwargs):
"""Return an apmec client.
:param api_version: only 1.0 is supported now
"""
apmec_client = utils.get_client_class(
API_NAME,
api_version,
API_VERSIONS,
)
return apmec_client(*args, **kwargs)

View File

@ -0,0 +1,717 @@
# Copyright 2012 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from __future__ import print_function
import abc
import argparse
import logging
import re
from cliff.formatters import table
from cliff import lister
from cliff import show
from oslo_serialization import jsonutils
import six
from apmecclient.common._i18n import _
from apmecclient.common import command
from apmecclient.common import exceptions
from apmecclient.common import utils
HEX_ELEM = '[0-9A-Fa-f]'
UUID_PATTERN = '-'.join([HEX_ELEM + '{8}', HEX_ELEM + '{4}',
HEX_ELEM + '{4}', HEX_ELEM + '{4}',
HEX_ELEM + '{12}'])
def _get_resource_plural(resource, client):
plurals = getattr(client, 'EXTED_PLURALS', [])
for k in plurals:
if plurals[k] == resource:
return k
return resource + 's'
def find_resourceid_by_id(client, resource, resource_id):
resource_plural = _get_resource_plural(resource, client)
obj_lister = getattr(client, "list_%s" % resource_plural)
if resource == 'event':
match = resource_id.isdigit() and resource_id != 0
else:
match = re.match(UUID_PATTERN, resource_id)
collection = resource_plural
if match:
data = obj_lister(id=resource_id, fields='id')
if data and data[collection]:
return data[collection][0]['id']
not_found_message = (_("Unable to find %(resource)s with id "
"'%(id)s'") %
{'resource': resource, 'id': resource_id})
# 404 is used to simulate server side behavior
raise exceptions.ApmecClientException(
message=not_found_message, status_code=404)
def _find_resourceid_by_name(client, resource, name):
resource_plural = _get_resource_plural(resource, client)
obj_lister = getattr(client, "list_%s" % resource_plural)
data = obj_lister(name=name, fields='id')
collection = resource_plural
info = data[collection]
if len(info) > 1:
raise exceptions.ApmecClientNoUniqueMatch(resource=resource,
name=name)
elif len(info) == 0:
not_found_message = (_("Unable to find %(resource)s with name "
"'%(name)s'") %
{'resource': resource, 'name': name})
# 404 is used to simulate server side behavior
raise exceptions.ApmecClientException(
message=not_found_message, status_code=404)
else:
return info[0]['id']
def find_resourceid_by_name_or_id(client, resource, name_or_id):
try:
return find_resourceid_by_id(client, resource, name_or_id)
except exceptions.ApmecClientException:
return _find_resourceid_by_name(client, resource, name_or_id)
def add_show_list_common_argument(parser):
parser.add_argument(
'-D', '--show-details',
help=_('Show detailed info'),
action='store_true',
default=False,)
parser.add_argument(
'--show_details',
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument(
'--fields',
help=argparse.SUPPRESS,
action='append',
default=[])
parser.add_argument(
'-F', '--field',
dest='fields', metavar='FIELD',
help=_('Specify the field(s) to be returned by server. You can '
'repeat this option.'),
action='append',
default=[])
def add_pagination_argument(parser):
parser.add_argument(
'-P', '--page-size',
dest='page_size', metavar='SIZE', type=int,
help=_("Specify retrieve unit of each request, then split one request "
"to several requests"),
default=None)
def add_sorting_argument(parser):
parser.add_argument(
'--sort-key',
dest='sort_key', metavar='FIELD',
action='append',
help=_("Sorts the list by the specified fields in the specified "
"directions. You can repeat this option, but you must "
"specify an equal number of sort_dir and sort_key values. "
"Extra sort_dir options are ignored. Missing sort_dir options "
"use the default asc value."),
default=[])
parser.add_argument(
'--sort-dir',
dest='sort_dir', metavar='{asc,desc}',
help=_("Sorts the list in the specified direction. You can repeat "
"this option."),
action='append',
default=[],
choices=['asc', 'desc'])
def is_number(s):
try:
float(s) # for int, long and float
except ValueError:
try:
complex(s) # for complex
except ValueError:
return False
return True
def _process_previous_argument(current_arg, _value_number, current_type_str,
_list_flag, _values_specs, _clear_flag,
values_specs):
if current_arg is not None:
if _value_number == 0 and (current_type_str or _list_flag):
# This kind of argument should have value
raise exceptions.CommandError(
_("Invalid values_specs %s") % ' '.join(values_specs))
if _value_number > 1 or _list_flag or current_type_str == 'list':
current_arg.update({'nargs': '+'})
elif _value_number == 0:
if _clear_flag:
# if we have action=clear, we use argument's default
# value None for argument
_values_specs.pop()
else:
# We assume non value argument as bool one
current_arg.update({'action': 'store_true'})
def parse_args_to_dict(values_specs):
'''It is used to analyze the extra command options to command.
Besides known options and arguments, our commands also support user to
put more options to the end of command line. For example,
list_nets -- --tag x y --key1 value1, where '-- --tag x y --key1 value1'
is extra options to our list_nets. This feature can support V1.0 API's
fields selection and filters. For example, to list networks which has name
'test4', we can have list_nets -- --name=test4.
value spec is: --key type=int|bool|... value. Type is one of Python
built-in types. By default, type is string. The key without value is
a bool option. Key with two values will be a list option.
'''
# values_specs for example: '-- --tag x y --key1 type=int value1'
# -- is a pseudo argument
values_specs_copy = values_specs[:]
if values_specs_copy and values_specs_copy[0] == '--':
del values_specs_copy[0]
# converted ArgumentParser arguments for each of the options
_options = {}
# the argument part for current option in _options
current_arg = None
# the string after remove meta info in values_specs
# for example, '--tag x y --key1 value1'
_values_specs = []
# record the count of values for an option
# for example: for '--tag x y', it is 2, while for '--key1 value1', it is 1
_value_number = 0
# list=true
_list_flag = False
# action=clear
_clear_flag = False
# the current item in values_specs
current_item = None
# the str after 'type='
current_type_str = None
for _item in values_specs_copy:
if _item.startswith('--'):
# Deal with previous argument if any
_process_previous_argument(
current_arg, _value_number, current_type_str,
_list_flag, _values_specs, _clear_flag, values_specs)
# Init variables for current argument
current_item = _item
_list_flag = False
_clear_flag = False
current_type_str = None
if "=" in _item:
_value_number = 1
_item = _item.split('=')[0]
else:
_value_number = 0
if _item in _options:
raise exceptions.CommandError(
_("Duplicated options %s") % ' '.join(values_specs))
else:
_options.update({_item: {}})
current_arg = _options[_item]
_item = current_item
elif _item.startswith('type='):
if current_arg is None:
raise exceptions.CommandError(
_("Invalid values_specs %s") % ' '.join(values_specs))
if 'type' not in current_arg:
current_type_str = _item.split('=', 2)[1]
current_arg.update({'type': eval(current_type_str)})
if current_type_str == 'bool':
current_arg.update({'type': utils.str2bool})
elif current_type_str == 'dict':
current_arg.update({'type': utils.str2dict})
continue
elif _item == 'list=true':
_list_flag = True
continue
elif _item == 'action=clear':
_clear_flag = True
continue
if not _item.startswith('--'):
# All others are value items
# Make sure '--' occurs first and allow minus value
if (not current_item or '=' in current_item or
_item.startswith('-') and not is_number(_item)):
raise exceptions.CommandError(
_("Invalid values_specs %s") % ' '.join(values_specs))
_value_number += 1
_values_specs.append(_item)
# Deal with last one argument
_process_previous_argument(
current_arg, _value_number, current_type_str,
_list_flag, _values_specs, _clear_flag, values_specs)
# populate the parser with arguments
_parser = argparse.ArgumentParser(add_help=False)
for opt, optspec in _options.items():
_parser.add_argument(opt, **optspec)
_args = _parser.parse_args(_values_specs)
result_dict = {}
for opt in _options.keys():
_opt = opt.split('--', 2)[1]
_opt = _opt.replace('-', '_')
_value = getattr(_args, _opt)
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.items():
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 (not arg_value or
isinstance(arg_value[0], type(value[0]))):
arg_value.extend(value)
_extra_values.pop(key)
def update_dict(obj, dict, attributes):
"""Update dict with fields from obj.attributes
:param obj: the object updated into dict
:param dict: the result dictionary
:param attributes: a list of attributes belonging to obj
"""
for attribute in attributes:
if hasattr(obj, attribute) and getattr(obj, attribute) is not None:
dict[attribute] = getattr(obj, attribute)
class TableFormater(table.TableFormatter):
"""This class is used to keep consistency with prettytable 0.6.
https://bugs.launchpad.net/python-apmecclient/+bug/1165962
"""
def emit_list(self, column_names, data, stdout, parsed_args):
if column_names:
super(TableFormater, self).emit_list(column_names, data, stdout,
parsed_args)
else:
stdout.write('\n')
# command.OpenStackCommand is abstract class so that metaclass of
# subclass must be subclass of metaclass of all its base.
# otherwise metaclass conflict exception is raised.
class ApmecCommandMeta(abc.ABCMeta):
def __new__(cls, name, bases, cls_dict):
if 'log' not in cls_dict:
cls_dict['log'] = logging.getLogger(
cls_dict['__module__'] + '.' + name)
return super(ApmecCommandMeta, cls).__new__(cls,
name, bases, cls_dict)
@six.add_metaclass(ApmecCommandMeta)
class ApmecCommand(command.OpenStackCommand):
api = 'mec-orchestration'
values_specs = []
json_indent = None
def __init__(self, app, app_args):
super(ApmecCommand, self).__init__(app, app_args)
# NOTE(markmcclain): This is no longer supported in cliff version 1.5.2
# see https://bugs.launchpad.net/python-apmecclient/+bug/1265926
# if hasattr(self, 'formatters'):
# self.formatters['table'] = TableFormater()
def get_client(self):
return self.app.client_manager.apmec
def get_parser(self, prog_name):
parser = super(ApmecCommand, self).get_parser(prog_name)
parser.add_argument(
'--request-format',
help=_('The xml or json request format'),
default='json',
choices=['json', 'xml', ], )
parser.add_argument(
'--request_format',
choices=['json', 'xml', ],
help=argparse.SUPPRESS)
return parser
def format_output_data(self, data):
# Modify data to make it more readable
if self.resource in data:
for k, v in data[self.resource].items():
if isinstance(v, list):
value = '\n'.join(jsonutils.dumps(
i, indent=self.json_indent) if isinstance(i, dict)
else str(i) for i in v)
data[self.resource][k] = value
elif isinstance(v, dict):
value = jsonutils.dumps(v, indent=self.json_indent)
data[self.resource][k] = value
elif v is None:
data[self.resource][k] = ''
def add_known_arguments(self, parser):
pass
def args2body(self, parsed_args):
return {}
class CreateCommand(ApmecCommand, show.ShowOne):
"""Create a resource for a given tenant
"""
api = 'mec-orchestration'
resource = None
log = None
remove_output_fields = []
def get_parser(self, prog_name):
parser = super(CreateCommand, self).get_parser(prog_name)
parser.add_argument(
'--tenant-id', metavar='TENANT_ID',
help=_('The owner tenant ID'), )
parser.add_argument(
'--tenant_id',
help=argparse.SUPPRESS)
self.add_known_arguments(parser)
return parser
def get_data(self, parsed_args):
self.log.debug('get_data(%s)', parsed_args)
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
_extra_values = parse_args_to_dict(self.values_specs)
_merge_args(self, parsed_args, _extra_values,
self.values_specs)
body = self.args2body(parsed_args)
body[self.resource].update(_extra_values)
obj_creator = getattr(apmec_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(_('Created a new %s:') % self.resource,
file=self.app.stdout)
for f in self.remove_output_fields:
if f in info:
info.pop(f)
else:
info = {'': ''}
return zip(*sorted(info.items()))
class UpdateCommand(ApmecCommand):
"""Update resource's information."""
api = 'mec-orchestration'
resource = None
log = None
allow_names = True
def get_parser(self, prog_name):
parser = super(UpdateCommand, self).get_parser(prog_name)
parser.add_argument(
'id', metavar=self.resource.upper(),
help=_('ID or name of %s to update') % self.resource)
self.add_known_arguments(parser)
return parser
def run(self, parsed_args):
self.log.debug('run(%s)', parsed_args)
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
_extra_values = parse_args_to_dict(self.values_specs)
_merge_args(self, parsed_args, _extra_values,
self.values_specs)
body = self.args2body(parsed_args)
if self.resource in body:
body[self.resource].update(_extra_values)
else:
body[self.resource] = _extra_values
if not body[self.resource]:
raise exceptions.CommandError(
_("Must specify new values to update %s") % self.resource)
if self.allow_names:
_id = find_resourceid_by_name_or_id(
apmec_client, self.resource, parsed_args.id)
else:
_id = find_resourceid_by_id(
apmec_client, self.resource, parsed_args.id)
obj_updator = getattr(apmec_client,
"update_%s" % self.resource)
obj_updator(_id, body)
print((_('Updated %(resource)s: %(id)s') %
{'id': parsed_args.id, 'resource': self.resource}),
file=self.app.stdout)
return
class DeleteCommand(ApmecCommand):
"""Delete given resource(s)
"""
api = 'mec-orchestration'
resource = None
log = None
allow_names = True
deleted_msg = {}
def get_parser(self, prog_name):
parser = super(DeleteCommand, self).get_parser(prog_name)
if self.allow_names:
help_str = _('IDs or names of %s to delete')
else:
help_str = _('IDs of %s to delete')
parser.add_argument(
'ids', nargs='+',
metavar=self.resource.upper(),
help=help_str % self.resource)
return parser
def run(self, parsed_args):
failure = False
deleted_ids = []
failed_items = {}
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
obj_deleter = getattr(apmec_client,
"delete_%s" % self.resource)
for resource_id in parsed_args.ids:
try:
if self.allow_names:
_id = find_resourceid_by_name_or_id(
apmec_client, self.resource, resource_id)
else:
_id = resource_id
obj_deleter(_id)
deleted_ids.append(resource_id)
except Exception as e:
failure = True
failed_items[resource_id] = e
if failure:
msg = ''
if deleted_ids:
status_msg = self.deleted_msg.get(self.resource, 'deleted')
msg = (_('Successfully %(status_msg)s %(resource)s(s):'
' %(deleted_list)s') % {'status_msg': status_msg,
'deleted_list':
', '.join(deleted_ids),
'resource': self.resource})
err_msg = _("\n\nUnable to delete the below"
" %s(s):") % self.resource
for failed_id, error in failed_items.iteritems():
err_msg += (_('\n Cannot delete %(failed_id)s: %(error)s')
% {'failed_id': failed_id,
'error': error})
msg += err_msg
raise exceptions.CommandError(msg)
else:
print((_('All specified %(resource)s(s) %(msg)s successfully')
% {'msg': self.deleted_msg.get(self.resource, 'deleted'),
'resource': self.resource}))
return
class ListCommand(ApmecCommand, lister.Lister):
"""List resources that belong to a given tenant
"""
api = 'mec-orchestration'
resource = None
log = None
_formatters = {}
list_columns = []
unknown_parts_flag = True
pagination_support = False
sorting_support = False
def get_parser(self, prog_name):
parser = super(ListCommand, self).get_parser(prog_name)
add_show_list_common_argument(parser)
if self.pagination_support:
add_pagination_argument(parser)
if self.sorting_support:
add_sorting_argument(parser)
return parser
def args2search_opts(self, parsed_args):
search_opts = {}
fields = parsed_args.fields
if parsed_args.fields:
search_opts.update({'fields': fields})
if parsed_args.show_details:
search_opts.update({'verbose': 'True'})
return search_opts
def call_server(self, apmec_client, search_opts, parsed_args):
resource_plural = _get_resource_plural(self.resource, apmec_client)
obj_lister = getattr(apmec_client, "list_%s" % resource_plural)
data = obj_lister(**search_opts)
return data
def retrieve_list(self, parsed_args):
"""Retrieve a list of resources from Apmec server"""
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
_extra_values = parse_args_to_dict(self.values_specs)
_merge_args(self, parsed_args, _extra_values,
self.values_specs)
search_opts = self.args2search_opts(parsed_args)
search_opts.update(_extra_values)
if self.pagination_support:
page_size = parsed_args.page_size
if page_size:
search_opts.update({'limit': page_size})
if self.sorting_support:
keys = parsed_args.sort_key
if keys:
search_opts.update({'sort_key': keys})
dirs = parsed_args.sort_dir
len_diff = len(keys) - len(dirs)
if len_diff > 0:
dirs += ['asc'] * len_diff
elif len_diff < 0:
dirs = dirs[:len(keys)]
if dirs:
search_opts.update({'sort_dir': dirs})
data = self.call_server(apmec_client, search_opts, parsed_args)
collection = _get_resource_plural(self.resource, apmec_client)
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 apmec server. For example, you can add subnet cidr information
to a list network.
"""
pass
def setup_columns(self, info, parsed_args):
_columns = len(info) > 0 and sorted(info[0].keys()) or []
if not _columns:
# clean the parsed_args.columns so that cliff will not break
parsed_args.columns = []
elif parsed_args.columns:
_columns = [x for x in parsed_args.columns if x in _columns]
elif self.list_columns:
# if no -c(s) by user and list_columns, we use columns in
# both list_columns and returned resource.
# Also Keep their order the same as in list_columns
_columns = [x for x in self.list_columns if x in _columns]
return (_columns, (utils.get_item_properties(
s, _columns, formatters=self._formatters, )
for s in info), )
def get_data(self, parsed_args):
self.log.debug('get_data(%s)', parsed_args)
data = self.retrieve_list(parsed_args)
self.extend_list(data, parsed_args)
return self.setup_columns(data, parsed_args)
class ShowCommand(ApmecCommand, show.ShowOne):
"""Show information of a given resource
"""
api = 'mec-orchestration'
resource = None
log = None
allow_names = True
def get_id(self):
if self.resource:
return self.resource.upper()
def get_parser(self, prog_name):
parser = super(ShowCommand, self).get_parser(prog_name)
add_show_list_common_argument(parser)
if self.allow_names:
help_str = _('ID or name of %s to look up')
else:
help_str = _('ID of %s to look up')
parser.add_argument(
'id', metavar=self.get_id(),
help=help_str % self.resource)
return parser
def get_data(self, parsed_args):
self.log.debug('get_data(%s)', parsed_args)
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
params = {}
if parsed_args.show_details:
params = {'verbose': 'True'}
if parsed_args.fields:
params = {'fields': parsed_args.fields}
if self.allow_names:
_id = find_resourceid_by_name_or_id(apmec_client, self.resource,
parsed_args.id)
else:
_id = parsed_args.id
obj_shower = getattr(apmec_client, "show_%s" % self.resource)
data = obj_shower(_id, **params)
self.format_output_data(data)
resource = data[self.resource]
if self.resource in data:
return zip(*sorted(resource.items()))
else:
return None

View File

@ -0,0 +1,95 @@
# Copyright 2016 Brocade Communications Systems Inc
# All Rights Reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from apmecclient.apmec import v1_0 as apmecV10
_EVENT = "event"
class ListEventsBase(apmecV10.ListCommand):
"""Base class for list command."""
list_columns = ['id', 'resource_type', 'resource_id',
'resource_state', 'event_type',
'timestamp', 'event_details']
def get_parser(self, prog_name):
parser = super(ListEventsBase, self).get_parser(prog_name)
parser.add_argument('--id',
help='id of the event to look up.')
parser.add_argument('--resource-id',
help='resource id of the events to look up.')
parser.add_argument('--resource-state',
help='resource state of the events to look up.')
parser.add_argument('--event-type',
help='event type of the events to look up.')
return parser
def args2search_opts(self, parsed_args):
search_opts = super(ListEventsBase, self).args2search_opts(
parsed_args)
if parsed_args.id:
search_opts.update({'id': parsed_args.id})
if parsed_args.resource_id:
search_opts.update({'resource_id': parsed_args.resource_id})
if parsed_args.resource_state:
search_opts.update({'resource_state': parsed_args.resource_state})
if parsed_args.event_type:
search_opts.update({'event_type': parsed_args.event_type})
return search_opts
class ListResourceEvents(ListEventsBase):
"""List events of resources."""
resource = _EVENT
def get_parser(self, prog_name):
parser = super(ListResourceEvents, self).get_parser(prog_name)
parser.add_argument('--resource-type',
help='resource type of the events to look up.')
return parser
def args2search_opts(self, parsed_args):
search_opts = super(ListResourceEvents, self).args2search_opts(
parsed_args)
if parsed_args.resource_type:
search_opts.update({'resource_type': parsed_args.resource_type})
return search_opts
class ListMEAEvents(ListEventsBase):
"""List events of MEAs."""
resource = "mea_event"
class ListMEADEvents(ListEventsBase):
"""List events of MEADs."""
resource = "mead_event"
class ListVIMEvents(ListEventsBase):
"""List events of VIMs."""
resource = "vim_event"
class ShowEvent(apmecV10.ShowCommand):
"""Show event given the event id."""
resource = _EVENT

View File

@ -0,0 +1,34 @@
# Copyright 2012 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from apmecclient.apmec import v1_0 as cmd_base
class ListExt(cmd_base.ListCommand):
"""List all extensions."""
resource = 'extension'
list_columns = ['alias', 'name']
class ShowExt(cmd_base.ShowCommand):
"""Show information of a given resource."""
resource = "extension"
allow_names = False
def get_id(self):
return 'EXT-ALIAS'

View File

View File

@ -0,0 +1,304 @@
#
# Copyright 2013 Intel Corporation
# All Rights Reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import yaml
from apmecclient.common import exceptions
from apmecclient.i18n import _
from apmecclient.apmec import v1_0 as apmecV10
_MEA = 'mea'
_RESOURCE = 'resource'
class ListMEA(apmecV10.ListCommand):
"""List MEA that belong to a given tenant."""
resource = _MEA
list_columns = ['id', 'name', 'mgmt_url', 'status',
'vim_id', 'mead_id']
class ShowMEA(apmecV10.ShowCommand):
"""Show information of a given MEA."""
resource = _MEA
class CreateMEA(apmecV10.CreateCommand):
"""Create a MEA."""
resource = _MEA
remove_output_fields = ["attributes"]
def add_known_arguments(self, parser):
parser.add_argument(
'name', metavar='NAME',
help=_('Set a name for the MEA'))
parser.add_argument(
'--description',
help=_('Set description for the MEA'))
mead_group = parser.add_mutually_exclusive_group(required=True)
mead_group.add_argument(
'--mead-id',
help=_('MEAD ID to use as template to create MEA'))
mead_group.add_argument(
'--mead-name',
help=_('MEAD Name to use as template to create MEA'))
mead_group.add_argument(
'--mead-template',
help=_("MEAD file to create MEA"))
vim_group = parser.add_mutually_exclusive_group()
vim_group.add_argument(
'--vim-id',
help=_('VIM ID to use to create MEA on the specified VIM'))
vim_group.add_argument(
'--vim-name',
help=_('VIM name to use to create MEA on the specified VIM'))
parser.add_argument(
'--vim-region-name',
help=_('VIM Region to use to create MEA on the specified VIM'))
parser.add_argument(
'--config-file',
help=_('YAML file with MEA configuration'))
parser.add_argument(
'--param-file',
help=_('Specify parameter yaml file'))
def args2body(self, parsed_args):
args = {'attributes': {}}
body = {self.resource: args}
# config arg passed as data overrides config yaml when both args passed
config = None
if parsed_args.config_file:
with open(parsed_args.config_file) as f:
config_yaml = f.read()
try:
config = yaml.load(
config_yaml, Loader=yaml.SafeLoader)
except yaml.YAMLError as e:
raise exceptions.InvalidInput(e)
if config:
args['attributes']['config'] = config
if parsed_args.vim_region_name:
args.setdefault('placement_attr', {})['region_name'] = \
parsed_args.vim_region_name
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
if parsed_args.vim_name:
_id = apmecV10.find_resourceid_by_name_or_id(apmec_client,
'vim',
parsed_args.
vim_name)
parsed_args.vim_id = _id
if parsed_args.mead_name:
_id = apmecV10.find_resourceid_by_name_or_id(apmec_client,
'mead',
parsed_args.
mead_name)
parsed_args.mead_id = _id
elif parsed_args.mead_template:
with open(parsed_args.mead_template) as f:
template = f.read()
try:
args['mead_template'] = yaml.load(
template, Loader=yaml.SafeLoader)
except yaml.YAMLError as e:
raise exceptions.InvalidInput(e)
if parsed_args.param_file:
with open(parsed_args.param_file) as f:
param_yaml = f.read()
try:
args['attributes']['param_values'] = yaml.load(
param_yaml, Loader=yaml.SafeLoader)
except yaml.YAMLError as e:
raise exceptions.InvalidInput(e)
apmecV10.update_dict(parsed_args, body[self.resource],
['tenant_id', 'name', 'description',
'mead_id', 'vim_id'])
return body
class UpdateMEA(apmecV10.UpdateCommand):
"""Update a given MEA."""
resource = _MEA
def add_known_arguments(self, parser):
parser.add_argument(
'--config-file',
help=_('YAML file with MEA configuration'))
parser.add_argument(
'--config',
help=_('Specify config yaml data'))
def args2body(self, parsed_args):
body = {self.resource: {}}
# config arg passed as data overrides config yaml when both args passed
config = None
if parsed_args.config_file:
with open(parsed_args.config_file) as f:
config_yaml = f.read()
try:
config = yaml.load(config_yaml, Loader=yaml.SafeLoader)
except yaml.YAMLError as e:
raise exceptions.InvalidInput(e)
if parsed_args.config:
config = parsed_args.config
if isinstance(config, str) or isinstance(config, unicode):
config_str = parsed_args.config.decode('unicode_escape')
try:
config = yaml.load(config_str, Loader=yaml.SafeLoader)
except yaml.YAMLError as e:
raise exceptions.InvalidInput(e)
if config:
body[self.resource]['attributes'] = {'config': config}
apmecV10.update_dict(parsed_args, body[self.resource], ['tenant_id'])
return body
class DeleteMEA(apmecV10.DeleteCommand):
"""Delete given MEA(s)."""
resource = _MEA
deleted_msg = {'mea': 'delete initiated'}
class ListMEAResources(apmecV10.ListCommand):
"""List resources of a MEA like VDU, CP, etc."""
list_columns = ['name', 'id', 'type']
allow_names = True
resource = _MEA
def get_id(self):
if self.resource:
return self.resource.upper()
def get_parser(self, prog_name):
parser = super(ListMEAResources, self).get_parser(prog_name)
if self.allow_names:
help_str = _('ID or name of %s to look up')
else:
help_str = _('ID of %s to look up')
parser.add_argument(
'id', metavar=self.get_id(),
help=help_str % self.resource)
return parser
def get_data(self, parsed_args):
self.log.debug('get_data(%s)', parsed_args)
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
if self.allow_names:
_id = apmecV10.find_resourceid_by_name_or_id(apmec_client,
self.resource,
parsed_args.id)
else:
_id = parsed_args.id
data = self.retrieve_list_by_id(_id, parsed_args)
self.extend_list(data, parsed_args)
return self.setup_columns(data, parsed_args)
def retrieve_list_by_id(self, id, parsed_args):
"""Retrieve a list of sub resources from Apmec server"""
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
_extra_values = apmecV10.parse_args_to_dict(self.values_specs)
apmecV10._merge_args(self, parsed_args, _extra_values,
self.values_specs)
search_opts = self.args2search_opts(parsed_args)
search_opts.update(_extra_values)
if self.pagination_support:
page_size = parsed_args.page_size
if page_size:
search_opts.update({'limit': page_size})
if self.sorting_support:
keys = parsed_args.sort_key
if keys:
search_opts.update({'sort_key': keys})
dirs = parsed_args.sort_dir
len_diff = len(keys) - len(dirs)
if len_diff > 0:
dirs += ['asc'] * len_diff
elif len_diff < 0:
dirs = dirs[:len(keys)]
if dirs:
search_opts.update({'sort_dir': dirs})
obj_lister = getattr(apmec_client, "list_mea_resources")
data = obj_lister(id, **search_opts)
return data.get('resources', [])
class ScaleMEA(apmecV10.ApmecCommand):
"""Scale a MEA."""
api = 'mec-orchestration'
resource = None
log = None
def get_parser(self, prog_name):
parser = super(ScaleMEA, self).get_parser(prog_name)
self.add_known_arguments(parser)
return parser
def run(self, parsed_args):
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
body = self.args2body(parsed_args)
obj_creator = getattr(apmec_client,
"scale_mea")
obj_creator(body["scale"].pop('mea_id'), body)
def add_known_arguments(self, parser):
mea_group = parser.add_mutually_exclusive_group(required=True)
mea_group.add_argument(
'--mea-id',
help=_('MEA ID'))
mea_group.add_argument(
'--mea-name',
help=_('MEA name'))
parser.add_argument(
'--scaling-policy-name',
help=_('MEA policy name used to scale'))
parser.add_argument(
'--scaling-type',
help=_('MEA scaling type, it could be either "out" or "in"'))
def args2body(self, parsed_args):
args = {}
body = {"scale": args}
if parsed_args.mea_name:
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
_id = apmecV10.find_resourceid_by_name_or_id(apmec_client,
'mea',
parsed_args.
mea_name)
parsed_args.mea_id = _id
args['mea_id'] = parsed_args.mea_id
args['type'] = parsed_args.scaling_type
args['policy'] = parsed_args.scaling_policy_name
return body

View File

@ -0,0 +1,115 @@
#
# Copyright 2013 Intel Corporation
# All Rights Reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
from oslo_serialization import jsonutils
import yaml
from apmecclient.common import exceptions
from apmecclient.i18n import _
from apmecclient.apmec import v1_0 as apmecV10
_MEAD = "mead"
class ListMEAD(apmecV10.ListCommand):
"""List MEAD that belong to a given tenant."""
resource = _MEAD
list_columns = ['id', 'name', 'template_source', 'description']
def get_parser(self, prog_name):
parser = super(ListMEAD, self).get_parser(prog_name)
parser.add_argument(
'--template-source',
help=_("List MEAD with specified template source. Available \
options are 'onboarded' (default), 'inline' or 'all'"),
action='store',
default='onboarded')
return parser
def args2search_opts(self, parsed_args):
search_opts = super(ListMEAD, self).args2search_opts(parsed_args)
template_source = parsed_args.template_source
if parsed_args.template_source:
search_opts.update({'template_source': template_source})
return search_opts
class ShowMEAD(apmecV10.ShowCommand):
"""Show information of a given MEAD."""
resource = _MEAD
class CreateMEAD(apmecV10.CreateCommand):
"""Create a MEAD."""
resource = _MEAD
remove_output_fields = ["attributes"]
def add_known_arguments(self, parser):
parser.add_argument('--mead-file', help=_('Specify MEAD file'))
parser.add_argument(
'name', metavar='NAME',
help=_('Set a name for the MEAD'))
parser.add_argument(
'--description',
help=_('Set a description for the MEAD'))
def args2body(self, parsed_args):
body = {self.resource: {}}
mead = None
if not parsed_args.mead_file:
raise exceptions.InvalidInput("Invalid input for mead file")
with open(parsed_args.mead_file) as f:
mead = f.read()
try:
mead = yaml.load(mead, Loader=yaml.SafeLoader)
except yaml.YAMLError as e:
raise exceptions.InvalidInput(e)
if not mead:
raise exceptions.InvalidInput("mead file is empty")
body[self.resource]['attributes'] = {'mead': mead}
apmecV10.update_dict(parsed_args, body[self.resource],
['tenant_id', 'name', 'description'])
return body
class DeleteMEAD(apmecV10.DeleteCommand):
"""Delete given MEAD(s)."""
resource = _MEAD
class ShowTemplateMEAD(apmecV10.ShowCommand):
"""Show template of a given MEAD."""
resource = _MEAD
def run(self, parsed_args):
self.log.debug('run(%s)', parsed_args)
template = None
data = self.get_data(parsed_args)
try:
attributes_index = data[0].index('attributes')
attributes_json = data[1][attributes_index]
template = jsonutils.loads(attributes_json).get('mead', None)
except (IndexError, TypeError, ValueError) as e:
self.log.debug('Data handling error: %s', str(e))
print(template or _('Unable to display MEAD template!'))

View File

View File

@ -0,0 +1,124 @@
# 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/LICEMESE-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 CONDITIOMES OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import yaml
from apmecclient.common import exceptions
from apmecclient.i18n import _
from apmecclient.apmec import v1_0 as apmecV10
_MES = 'mes'
_RESOURCE = 'resource'
class ListMES(apmecV10.ListCommand):
"""List MES that belong to a given tenant."""
resource = _MES
list_columns = ['id', 'name', 'mesd_id', 'mgmt_urls', 'status']
class ShowMES(apmecV10.ShowCommand):
"""Show information of a given MES."""
resource = _MES
class CreateMES(apmecV10.CreateCommand):
"""Create a MES."""
resource = _MES
remove_output_fields = ["attributes"]
def add_known_arguments(self, parser):
parser.add_argument(
'name', metavar='NAME',
help=_('Set a name for the MES'))
parser.add_argument(
'--description',
help=_('Set description for the MES'))
mesd_group = parser.add_mutually_exclusive_group(required=True)
mesd_group.add_argument(
'--mesd-id',
help=_('MESD ID to use as template to create MES'))
mesd_group.add_argument(
'--mesd-template',
help=_('MESD file to create MES'))
mesd_group.add_argument(
'--mesd-name',
help=_('MESD name to use as template to create MES'))
vim_group = parser.add_mutually_exclusive_group()
vim_group.add_argument(
'--vim-id',
help=_('VIM ID to use to create MES on the specified VIM'))
vim_group.add_argument(
'--vim-name',
help=_('VIM name to use to create MES on the specified VIM'))
parser.add_argument(
'--vim-region-name',
help=_('VIM Region to use to create MES on the specified VIM'))
parser.add_argument(
'--param-file',
help=_('Specify parameter yaml file'))
def args2body(self, parsed_args):
args = {'attributes': {}}
body = {self.resource: args}
if parsed_args.vim_region_name:
args.setdefault('placement_attr', {})['region_name'] = \
parsed_args.vim_region_name
apmec_client = self.get_client()
apmec_client.format = parsed_args.request_format
if parsed_args.vim_name:
_id = apmecV10.find_resourceid_by_name_or_id(apmec_client,
'vim',
parsed_args.
vim_name)
parsed_args.vim_id = _id
if parsed_args.mesd_name:
_id = apmecV10.find_resourceid_by_name_or_id(apmec_client,
'mesd',
parsed_args.
mesd_name)
parsed_args.mesd_id = _id
elif parsed_args.mesd_template:
with open(parsed_args.mesd_template) as f:
template = f.read()
try:
args['mesd_template'] = yaml.load(
template, Loader=yaml.SafeLoader)
except yaml.YAMLError as e:
raise exceptions.InvalidInput(e)
if not args['mesd_template']:
raise exceptions.InvalidInput('The mesd file is empty')
if parsed_args.param_file:
with open(parsed_args.param_file) as f:
param_yaml = f.read()
try:
args['attributes']['param_values'] = yaml.load(
param_yaml, Loader=yaml.SafeLoader)
except yaml.YAMLError as e:
raise exceptions.InvalidInput(e)
apmecV10.update_dict(parsed_args, body[self.resource],
['tenant_id', 'name', 'description',
'mesd_id', 'vim_id'])
return body
class DeleteMES(apmecV10.DeleteCommand):
"""Delete given MES(s)."""
resource = _MES
deleted_msg = {'mes': 'delete initiated'}

View File

@ -0,0 +1,102 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import yaml
from oslo_serialization import jsonutils
from apmecclient.i18n import _
from apmecclient.apmec import v1_0 as apmecV10
_MESD = "mesd"
class ListMESD(apmecV10.ListCommand):
"""List MESDs that belong to a given tenant."""
resource = _MESD
list_columns = ['id', 'name', 'template_source', 'description']
def get_parser(self, prog_name):
parser = super(ListMESD, self).get_parser(prog_name)
parser.add_argument(
'--template-source',
help=_("List MESD with specified template source. Available \
options are 'onboared' (default), 'inline' or 'all'"),
action='store',
default='onboarded')
return parser
def args2search_opts(self, parsed_args):
search_opts = super(ListMESD, self).args2search_opts(parsed_args)
template_source = parsed_args.template_source
if parsed_args.template_source:
search_opts.update({'template_source': template_source})
return search_opts
class ShowMESD(apmecV10.ShowCommand):
"""Show information of a given MESD."""
resource = _MESD
class CreateMESD(apmecV10.CreateCommand):
"""Create a MESD."""
resource = _MESD
remove_output_fields = ["attributes"]
def add_known_arguments(self, parser):
parser.add_argument('--mesd-file', help='Specify MESD file',
required=True)
parser.add_argument(
'name', metavar='NAME',
help='Set a name for the MESD')
parser.add_argument(
'--description',
help='Set a description for the MESD')
def args2body(self, parsed_args):
body = {self.resource: {}}
mesd = None
with open(parsed_args.mesd_file) as f:
mesd = yaml.safe_load(f.read())
apmecV10.update_dict(parsed_args, body[self.resource],
['tenant_id', 'name', 'description'])
if mesd:
body[self.resource]['attributes'] = {'mesd': mesd}
return body
class DeleteMESD(apmecV10.DeleteCommand):
"""Delete a given MESD."""
resource = _MESD
class ShowTemplateMESD(apmecV10.ShowCommand):
"""Show template of a given MESD."""
resource = _MESD
def run(self, parsed_args):
self.log.debug('run(%s)', parsed_args)
template = None
data = self.get_data(parsed_args)
try:
attributes_index = data[0].index('attributes')
attributes_json = data[1][attributes_index]
template = jsonutils.loads(attributes_json).get('mesd', None)
except (IndexError, TypeError, ValueError) as e:
self.log.debug('Data handling error: %s', str(e))
print(template or _('Unable to display MESD template!'))

View File

@ -0,0 +1,135 @@
# Copyright 2016 Brocade Communications Systems Inc
# All Rights Reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import yaml
from oslo_utils import strutils
from apmecclient.common import exceptions
from apmecclient.i18n import _
from apmecclient.apmec import v1_0 as apmecV10
from apmecclient.apmec.v1_0.meo import vim_utils
_VIM = "vim"
class ListVIM(apmecV10.ListCommand):
"""List VIMs that belong to a given tenant."""
resource = _VIM
list_columns = ['id', 'tenant_id', 'name', 'type', 'is_default',
'placement_attr', 'status']
class ShowVIM(apmecV10.ShowCommand):
"""Show information of a given VIM."""
resource = _VIM
class CreateVIM(apmecV10.CreateCommand):
"""Create a VIM."""
resource = _VIM
def add_known_arguments(self, parser):
parser.add_argument(
'--config-file',
required=True,
help=_('YAML file with VIM configuration parameters'))
parser.add_argument(
'name', metavar='NAME',
help=_('Set a name for the VIM'))
parser.add_argument(
'--description',
help=_('Set a description for the VIM'))
parser.add_argument(
'--is-default',
action='store_true',
default=False,
help=_('Set as default VIM'))
def args2body(self, parsed_args):
body = {self.resource: {}}
if parsed_args.config_file:
with open(parsed_args.config_file) as f:
vim_config = f.read()
try:
config_param = yaml.load(vim_config,
Loader=yaml.SafeLoader)
except yaml.YAMLError as e:
raise exceptions.InvalidInput(e)
vim_obj = body[self.resource]
try:
auth_url = config_param.pop('auth_url')
except KeyError:
raise exceptions.ApmecClientException(message='Auth URL must be '
'specified',
status_code=404)
vim_obj['auth_url'] = vim_utils.validate_auth_url(auth_url).geturl()
vim_obj['type'] = config_param.pop('type', 'openstack')
vim_utils.args2body_vim(config_param, vim_obj)
apmecV10.update_dict(parsed_args, body[self.resource],
['tenant_id', 'name', 'description',
'is_default'])
return body
class UpdateVIM(apmecV10.UpdateCommand):
"""Update a given VIM."""
resource = _VIM
def add_known_arguments(self, parser):
parser.add_argument(
'--config-file',
required=False,
help=_('YAML file with VIM configuration parameters'))
parser.add_argument(
'--name',
help=_('New name for the VIM'))
parser.add_argument(
'--description',
help=_('New description for the VIM'))
parser.add_argument(
'--is-default',
type=strutils.bool_from_string,
metavar='{True,False}',
help=_('Indicate whether the VIM is used as default'))
def args2body(self, parsed_args):
body = {self.resource: {}}
config_param = None
# config arg passed as data overrides config yaml when both args passed
if parsed_args.config_file:
with open(parsed_args.config_file) as f:
config_yaml = f.read()
try:
config_param = yaml.load(config_yaml)
except yaml.YAMLError as e:
raise exceptions.InvalidInput(e)
vim_obj = body[self.resource]
if config_param is not None:
vim_utils.args2body_vim(config_param, vim_obj)
apmecV10.update_dict(parsed_args, body[self.resource],
['tenant_id', 'name', 'description',
'is_default'])
return body
class DeleteVIM(apmecV10.DeleteCommand):
"""Delete given VIM(s)."""
resource = _VIM

View File

@ -0,0 +1,44 @@
# Copyright 2016 Brocade Communications Systems Inc
# All Rights Reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import six.moves.urllib.parse as urlparse
from apmecclient.common import exceptions
def args2body_vim(config_param, vim):
"""Create additional args to vim body
:param vim: vim request object
:return: vim body with args populated
"""
vim['vim_project'] = {'name': config_param.pop('project_name', ''),
'project_domain_name':
config_param.pop('project_domain_name', '')}
if not vim['vim_project']['name']:
raise exceptions.ApmecClientException(message='Project name '
'must be specified',
status_code=404)
vim['auth_cred'] = {'username': config_param.pop('username', ''),
'password': config_param.pop('password', ''),
'user_domain_name':
config_param.pop('user_domain_name', '')}
def validate_auth_url(url):
url_parts = urlparse.urlparse(url)
if not url_parts.scheme or not url_parts.netloc:
raise exceptions.ApmecClientException(message='Invalid auth URL')
return url_parts

390
apmecclient/client.py Normal file
View File

@ -0,0 +1,390 @@
# Copyright 2012 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
try:
import json
except ImportError:
import simplejson as json
import logging
import os
from keystoneclient import access
from keystoneclient import adapter
import requests
from apmecclient.common import exceptions
from apmecclient.common import utils
from apmecclient.i18n import _
_logger = logging.getLogger(__name__)
if os.environ.get('APMECCLIENT_DEBUG'):
ch = logging.StreamHandler()
_logger.setLevel(logging.DEBUG)
_logger.addHandler(ch)
_requests_log_level = logging.DEBUG
else:
_requests_log_level = logging.WARNING
logging.getLogger("requests").setLevel(_requests_log_level)
MAX_URI_LEN = 8192
class HTTPClient(object):
"""Handles the REST calls and responses, include authn."""
USER_AGENT = 'python-apmecclient'
CONTENT_TYPE = 'application/json'
def __init__(self, username=None, user_id=None,
tenant_name=None, tenant_id=None,
password=None, auth_url=None,
token=None, region_name=None, timeout=None,
endpoint_url=None, insecure=False,
endpoint_type='publicURL',
auth_strategy='keystone', ca_cert=None, log_credentials=False,
service_type='mec-orchestration',
**kwargs):
self.username = username
self.user_id = user_id
self.tenant_name = tenant_name
self.tenant_id = tenant_id
self.password = password
self.auth_url = auth_url.rstrip('/') if auth_url else None
self.service_type = service_type
self.endpoint_type = endpoint_type
self.region_name = region_name
self.timeout = timeout
self.auth_token = token
self.auth_tenant_id = None
self.auth_user_id = None
self.endpoint_url = endpoint_url
self.auth_strategy = auth_strategy
self.log_credentials = log_credentials
if insecure:
self.verify_cert = False
else:
self.verify_cert = ca_cert if ca_cert else True
def _cs_request(self, *args, **kwargs):
kargs = {}
kargs.setdefault('headers', kwargs.get('headers', {}))
kargs['headers']['User-Agent'] = self.USER_AGENT
if 'body' in kwargs:
kargs['body'] = kwargs['body']
if self.log_credentials:
log_kargs = kargs
else:
log_kargs = self._strip_credentials(kargs)
utils.http_log_req(_logger, args, log_kargs)
try:
resp, body = self.request(*args, **kargs)
except requests.exceptions.SSLError as e:
raise exceptions.SslCertificateValidationError(reason=e)
except Exception as e:
# Wrap the low-level connection error (socket timeout, redirect
# limit, decompression error, etc) into our custom high-level
# connection exception (it is excepted in the upper layers of code)
_logger.debug("throwing ConnectionFailed : %s", e)
raise exceptions.ConnectionFailed(reason=e)
utils.http_log_resp(_logger, resp, body)
if resp.status_code == 401:
raise exceptions.Unauthorized(message=body)
return resp, body
def _strip_credentials(self, kwargs):
if kwargs.get('body') and self.password:
log_kwargs = kwargs.copy()
log_kwargs['body'] = kwargs['body'].replace(self.password,
'REDACTED')
return log_kwargs
else:
return kwargs
def authenticate_and_fetch_endpoint_url(self):
if not self.auth_token:
self.authenticate()
elif not self.endpoint_url:
self.endpoint_url = self._get_endpoint_url()
def request(self, url, method, body=None, headers=None, **kwargs):
"""Request without authentication."""
content_type = kwargs.pop('content_type', None) or 'application/json'
headers = headers or {}
headers.setdefault('Accept', content_type)
if body:
headers.setdefault('Content-Type', content_type)
headers['User-Agent'] = self.USER_AGENT
resp = requests.request(
method,
url,
data=body,
headers=headers,
verify=self.verify_cert,
timeout=self.timeout,
**kwargs)
return resp, resp.text
def _check_uri_length(self, action):
uri_len = len(self.endpoint_url) + len(action)
if uri_len > MAX_URI_LEN:
raise exceptions.RequestURITooLong(
excess=uri_len - MAX_URI_LEN)
def do_request(self, url, method, **kwargs):
# Ensure client always has correct uri - do not guesstimate anything
self.authenticate_and_fetch_endpoint_url()
self._check_uri_length(url)
# Perform the request once. If we get a 401 back then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
try:
kwargs.setdefault('headers', {})
if self.auth_token is None:
self.auth_token = ""
kwargs['headers']['X-Auth-Token'] = self.auth_token
resp, body = self._cs_request(self.endpoint_url + url, method,
**kwargs)
return resp, body
except exceptions.Unauthorized:
self.authenticate()
resp, body = self._cs_request(
self.endpoint_url + url, method, **kwargs)
return resp, body
def _extract_service_catalog(self, body):
"""Set the client's service catalog from the response data."""
self.auth_ref = access.AccessInfo.factory(body=body)
self.service_catalog = self.auth_ref.service_catalog
self.auth_token = self.auth_ref.auth_token
self.auth_tenant_id = self.auth_ref.tenant_id
self.auth_user_id = self.auth_ref.user_id
if not self.endpoint_url:
self.endpoint_url = self.service_catalog.url_for(
region_name=self.region_name,
service_type=self.service_type,
endpoint_type=self.endpoint_type)
def _authenticate_keystone(self):
if self.user_id:
creds = {'userId': self.user_id,
'password': self.password}
else:
creds = {'username': self.username,
'password': self.password}
if self.tenant_id:
body = {'auth': {'passwordCredentials': creds,
'tenantId': self.tenant_id, }, }
else:
body = {'auth': {'passwordCredentials': creds,
'tenantName': self.tenant_name, }, }
if self.auth_url is None:
raise exceptions.NoAuthURLProvided()
token_url = self.auth_url + "/tokens"
resp, resp_body = self._cs_request(token_url, "POST",
body=json.dumps(body),
content_type="application/json",
allow_redirects=True)
if resp.status_code != 200:
raise exceptions.Unauthorized(message=resp_body)
if resp_body:
try:
resp_body = json.loads(resp_body)
except ValueError:
pass
else:
resp_body = None
self._extract_service_catalog(resp_body)
def _authenticate_noauth(self):
if not self.endpoint_url:
message = _('For "noauth" authentication strategy, the endpoint '
'must be specified either in the constructor or '
'using --os-url')
raise exceptions.Unauthorized(message=message)
def authenticate(self):
if self.auth_strategy == 'keystone':
self._authenticate_keystone()
elif self.auth_strategy == 'noauth':
self._authenticate_noauth()
else:
err_msg = _('Unknown auth strategy: %s') % self.auth_strategy
raise exceptions.Unauthorized(message=err_msg)
def _get_endpoint_url(self):
if self.auth_url is None:
raise exceptions.NoAuthURLProvided()
url = self.auth_url + '/tokens/%s/endpoints' % self.auth_token
try:
resp, body = self._cs_request(url, "GET")
except exceptions.Unauthorized:
# rollback to authenticate() to handle case when apmec client
# is initialized just before the token is expired
self.authenticate()
return self.endpoint_url
body = json.loads(body)
for endpoint in body.get('endpoints', []):
if (endpoint['type'] == 'mec-orchestration' and
endpoint.get('region') == self.region_name):
if self.endpoint_type not in endpoint:
raise exceptions.EndpointTypeNotFound(
type_=self.endpoint_type)
return endpoint[self.endpoint_type]
raise exceptions.EndpointNotFound()
def get_auth_info(self):
return {'auth_token': self.auth_token,
'auth_tenant_id': self.auth_tenant_id,
'auth_user_id': self.auth_user_id,
'endpoint_url': self.endpoint_url}
class SessionClient(adapter.Adapter):
def request(self, *args, **kwargs):
kwargs.setdefault('authenticated', False)
kwargs.setdefault('raise_exc', False)
content_type = kwargs.pop('content_type', None) or 'application/json'
headers = kwargs.setdefault('headers', {})
headers.setdefault('Accept', content_type)
try:
kwargs.setdefault('data', kwargs.pop('body'))
except KeyError:
pass
if kwargs.get('data'):
headers.setdefault('Content-Type', content_type)
resp = super(SessionClient, self).request(*args, **kwargs)
return resp, resp.text
def _check_uri_length(self, url):
uri_len = len(self.endpoint_url) + len(url)
if uri_len > MAX_URI_LEN:
raise exceptions.RequestURITooLong(
excess=uri_len - MAX_URI_LEN)
def do_request(self, url, method, **kwargs):
kwargs.setdefault('authenticated', True)
self._check_uri_length(url)
return self.request(url, method, **kwargs)
@property
def endpoint_url(self):
# NOTE(jamielennox): This is used purely by the CLI and should be
# removed when the CLI gets smarter.
return self.get_endpoint()
@property
def auth_token(self):
# NOTE(jamielennox): This is used purely by the CLI and should be
# removed when the CLI gets smarter.
return self.get_token()
def authenticate(self):
# NOTE(jamielennox): This is used purely by the CLI and should be
# removed when the CLI gets smarter.
self.get_token()
def get_auth_info(self):
auth_info = {'auth_token': self.auth_token,
'endpoint_url': self.endpoint_url}
# NOTE(jamielennox): This is the best we can do here. It will work
# with identity plugins which is the primary case but we should
# deprecate it's usage as much as possible.
try:
get_access = (self.auth or self.session.auth).get_access
except AttributeError:
pass
else:
auth_ref = get_access(self.session)
auth_info['auth_tenant_id'] = auth_ref.project_id
auth_info['auth_user_id'] = auth_ref.user_id
return auth_info
# FIXME(bklei): Should refactor this to use kwargs and only
# explicitly list arguments that are not None.
def construct_http_client(username=None,
user_id=None,
tenant_name=None,
tenant_id=None,
password=None,
auth_url=None,
token=None,
region_name=None,
timeout=None,
endpoint_url=None,
insecure=False,
endpoint_type='publicURL',
log_credentials=None,
auth_strategy='keystone',
ca_cert=None,
service_type='mec-orchestration',
session=None,
**kwargs):
if session:
kwargs.setdefault('user_agent', 'python-apmecclient')
kwargs.setdefault('interface', endpoint_type)
return SessionClient(session=session,
service_type=service_type,
region_name=region_name,
**kwargs)
else:
# FIXME(bklei): username and password are now optional. Need
# to test that they were provided in this mode. Should also
# refactor to use kwargs.
return HTTPClient(username=username,
password=password,
tenant_id=tenant_id,
tenant_name=tenant_name,
user_id=user_id,
auth_url=auth_url,
token=token,
endpoint_url=endpoint_url,
insecure=insecure,
timeout=timeout,
region_name=region_name,
endpoint_type=endpoint_type,
service_type=service_type,
ca_cert=ca_cert,
log_credentials=log_credentials,
auth_strategy=auth_strategy)

View File

View File

@ -0,0 +1,40 @@
# Copyright 2016 OpenStack Foundation
#
# 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.
"""oslo.i18n integration module.
See http://docs.openstack.org/developer/oslo.i18n/usage.html .
"""
import oslo_i18n
DOMAIN = "apmecclient"
_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN)
# The primary translation function using the well-known name "_"
_ = _translators.primary
# The contextual translation function using the name "_C"
# requires oslo.i18n >=2.1.0
_C = _translators.contextual_form
# The plural translation function using the name "_P"
# requires oslo.i18n >=2.1.0
_P = _translators.plural_form
def get_available_languages():
return oslo_i18n.get_available_languages(DOMAIN)

View File

@ -0,0 +1,108 @@
# Copyright 2012 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Manage access to the clients, including authenticating when needed.
"""
from apmecclient import client
from apmecclient.apmec import client as apmec_client
class ClientCache(object):
"""Descriptor class for caching created client handles."""
def __init__(self, factory):
self.factory = factory
self._handle = None
def __get__(self, instance, owner):
# Tell the ClientManager to login to keystone
if self._handle is None:
self._handle = self.factory(instance)
return self._handle
class ClientManager(object):
"""Manages access to API clients, including authentication."""
apmec = ClientCache(apmec_client.make_client)
def __init__(self, token=None, url=None,
auth_url=None,
endpoint_type=None,
tenant_name=None,
tenant_id=None,
username=None,
user_id=None,
password=None,
region_name=None,
api_version=None,
auth_strategy=None,
insecure=False,
ca_cert=None,
log_credentials=False,
service_type=None,
timeout=None,
retries=0,
raise_errors=True,
session=None,
auth=None,
):
self._token = token
self._url = url
self._auth_url = auth_url
self._service_type = service_type
self._endpoint_type = endpoint_type
self._tenant_name = tenant_name
self._tenant_id = tenant_id
self._username = username
self._user_id = user_id
self._password = password
self._region_name = region_name
self._api_version = api_version
self._service_catalog = None
self._auth_strategy = auth_strategy
self._insecure = insecure
self._ca_cert = ca_cert
self._log_credentials = log_credentials
self._timeout = timeout
self._retries = retries
self._raise_errors = raise_errors
self._session = session
self._auth = auth
return
def initialize(self):
if not self._url:
httpclient = client.construct_http_client(
username=self._username,
user_id=self._user_id,
tenant_name=self._tenant_name,
tenant_id=self._tenant_id,
password=self._password,
region_name=self._region_name,
auth_url=self._auth_url,
service_type=self._service_type,
endpoint_type=self._endpoint_type,
insecure=self._insecure,
ca_cert=self._ca_cert,
timeout=self._timeout,
session=self._session,
auth=self._auth,
log_credentials=self._log_credentials)
httpclient.authenticate()
# Populate other password flow attributes
self._token = httpclient.auth_token
self._url = httpclient.endpoint_url

View File

@ -0,0 +1,35 @@
# Copyright 2012 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from cliff import command
class OpenStackCommand(command.Command):
"""Base class for OpenStack commands."""
api = None
def run(self, parsed_args):
if not self.api:
return
else:
return super(OpenStackCommand, self).run(parsed_args)
def get_data(self, parsed_args):
pass
def take_action(self, parsed_args):
return self.get_data(parsed_args)

View File

@ -0,0 +1,38 @@
# Copyright (c) 2012 OpenStack Foundation.
#
# 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_V10 = 'http://openstack.org/apmec/api/v1.0'
XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"
XSI_ATTR = "xsi:nil"
XSI_NIL_ATTR = "xmlns:xsi"
TYPE_XMLNS = "xmlns:apmec"
TYPE_ATTR = "apmec:type"
VIRTUAL_ROOT_KEY = "_v_root"
ATOM_NAMESPACE = "http://www.w3.org/2005/Atom"
ATOM_XMLNS = "xmlns:atom"
ATOM_LINK_NOTATION = "{%s}link" % ATOM_NAMESPACE
TYPE_BOOL = "bool"
TYPE_INT = "int"
TYPE_LONG = "long"
TYPE_FLOAT = "float"
TYPE_LIST = "list"
TYPE_DICT = "dict"
PLURALS = {'templates': 'template',
'devices': 'device'}

View File

@ -0,0 +1,232 @@
# Copyright 2011 VMware, Inc
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from apmecclient.i18n import _
"""
Apmec base exception handling.
Exceptions are classified into three categories:
* Exceptions corresponding to exceptions from apmec server:
This type of exceptions should inherit one of exceptions
in HTTP_EXCEPTION_MAP.
* Exceptions from client library:
This type of exceptions should inherit ApmecClientException.
* Exceptions from CLI code:
This type of exceptions should inherit ApmecCLIError.
"""
class ApmecException(Exception):
"""Base Apmec Exception.
To correctly use this class, inherit from it and define
a 'message' property. That message will get printf'd
with the keyword arguments provided to the constructor.
"""
message = _("An unknown exception occurred.")
def __init__(self, message=None, **kwargs):
if message:
self.message = message
try:
self._error_string = self.message % kwargs
except Exception:
# at least get the core message out if something happened
self._error_string = self.message
def __str__(self):
return self._error_string
class ApmecClientException(ApmecException):
"""Base exception which exceptions from Apmec are mapped into.
NOTE: on the client side, we use different exception types in order
to allow client library users to handle server exceptions in try...except
blocks. The actual error message is the one generated on the server side.
"""
status_code = 0
def __init__(self, message=None, **kwargs):
if 'status_code' in kwargs:
self.status_code = kwargs['status_code']
super(ApmecClientException, self).__init__(message, **kwargs)
# Base exceptions from Apmec
class BadRequest(ApmecClientException):
status_code = 400
class Unauthorized(ApmecClientException):
status_code = 401
message = _("Unauthorized: bad credentials.")
class Forbidden(ApmecClientException):
status_code = 403
message = _("Forbidden: your credentials don't give you access to this "
"resource.")
class NotFound(ApmecClientException):
status_code = 404
class Conflict(ApmecClientException):
status_code = 409
class InternalServerError(ApmecClientException):
status_code = 500
class ServiceUnavailable(ApmecClientException):
status_code = 503
HTTP_EXCEPTION_MAP = {
400: BadRequest,
401: Unauthorized,
403: Forbidden,
404: NotFound,
409: Conflict,
500: InternalServerError,
503: ServiceUnavailable,
}
# Exceptions mapped to Apmec server exceptions
# These are defined if a user of client library needs specific exception.
# Exception name should be <Apmec Exception Name> + 'Client'
# e.g., NetworkNotFound -> NetworkNotFoundClient
class NetworkNotFoundClient(NotFound):
pass
class PortNotFoundClient(NotFound):
pass
class StateInvalidClient(BadRequest):
pass
class NetworkInUseClient(Conflict):
pass
class PortInUseClient(Conflict):
pass
class IpAddressInUseClient(Conflict):
pass
class InvalidIpForNetworkClient(BadRequest):
pass
class OverQuotaClient(Conflict):
pass
class IpAddressGenerationFailureClient(Conflict):
pass
class MacAddressInUseClient(Conflict):
pass
class ExternalIpAddressExhaustedClient(BadRequest):
pass
# Exceptions from client library
class NoAuthURLProvided(Unauthorized):
message = _("auth_url was not provided to the Apmec client")
class EndpointNotFound(ApmecClientException):
message = _("Could not find Service or Region in Service Catalog.")
class EndpointTypeNotFound(ApmecClientException):
message = _("Could not find endpoint type %(type_)s in Service Catalog.")
class AmbiguousEndpoints(ApmecClientException):
message = _("Found more than one matching endpoint in Service Catalog: "
"%(matching_endpoints)")
class RequestURITooLong(ApmecClientException):
"""Raised when a request fails with HTTP error 414."""
def __init__(self, **kwargs):
self.excess = kwargs.get('excess', 0)
super(RequestURITooLong, self).__init__(**kwargs)
class ConnectionFailed(ApmecClientException):
message = _("Connection to apmec failed: %(reason)s")
class SslCertificateValidationError(ApmecClientException):
message = _("SSL certificate validation has failed: %(reason)s")
class MalformedResponseBody(ApmecClientException):
message = _("Malformed response body: %(reason)s")
class InvalidContentType(ApmecClientException):
message = _("Invalid content type %(content_type)s.")
class InvalidInput(ApmecClientException):
message = _("Invalid input: %(reason)s")
# Command line exceptions
class ApmecCLIError(ApmecException):
"""Exception raised when command line parsing fails."""
pass
class CommandError(ApmecCLIError):
pass
class UnsupportedVersion(ApmecCLIError):
"""Unsupported Version.
Indicates that the user is trying to use an unsupported version of
the API.
"""
pass
class ApmecClientNoUniqueMatch(ApmecCLIError):
message = _("Multiple %(resource)s matches found for name '%(name)s',"
" use an ID to be more specific.")

View File

@ -0,0 +1,86 @@
# Copyright 2015 Rackspace Hosting Inc.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from stevedore import extension
from apmecclient.apmec import v1_0 as apmecV10
def _discover_via_entry_points():
emgr = extension.ExtensionManager('apmecclient.extension',
invoke_on_load=False)
return ((ext.name, ext.plugin) for ext in emgr)
class ApmecClientExtension(apmecV10.ApmecCommand):
pagination_support = False
_formatters = {}
sorting_support = False
class ClientExtensionShow(ApmecClientExtension, apmecV10.ShowCommand):
def get_data(self, parsed_args):
# NOTE(mdietz): Calls 'execute' to provide a consistent pattern
# for any implementers adding extensions with
# regard to any other extension verb.
return self.execute(parsed_args)
def execute(self, parsed_args):
return super(ClientExtensionShow, self).get_data(parsed_args)
class ClientExtensionList(ApmecClientExtension, apmecV10.ListCommand):
def get_data(self, parsed_args):
# NOTE(mdietz): Calls 'execute' to provide a consistent pattern
# for any implementers adding extensions with
# regard to any other extension verb.
return self.execute(parsed_args)
def execute(self, parsed_args):
return super(ClientExtensionList, self).get_data(parsed_args)
class ClientExtensionDelete(ApmecClientExtension, apmecV10.DeleteCommand):
def run(self, parsed_args):
# NOTE(mdietz): Calls 'execute' to provide a consistent pattern
# for any implementers adding extensions with
# regard to any other extension verb.
return self.execute(parsed_args)
def execute(self, parsed_args):
return super(ClientExtensionDelete, self).run(parsed_args)
class ClientExtensionCreate(ApmecClientExtension, apmecV10.CreateCommand):
def get_data(self, parsed_args):
# NOTE(mdietz): Calls 'execute' to provide a consistent pattern
# for any implementers adding extensions with
# regard to any other extension verb.
return self.execute(parsed_args)
def execute(self, parsed_args):
return super(ClientExtensionCreate, self).get_data(parsed_args)
class ClientExtensionUpdate(ApmecClientExtension, apmecV10.UpdateCommand):
def run(self, parsed_args):
# NOTE(mdietz): Calls 'execute' to provide a consistent pattern
# for any implementers adding extensions with
# regard to any other extension verb.
return self.execute(parsed_args)
def execute(self, parsed_args):
return super(ClientExtensionUpdate, self).run(parsed_args)

View File

@ -0,0 +1,405 @@
# Copyright 2013 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from xml.etree import ElementTree as etree
from xml.parsers import expat
from oslo_serialization import jsonutils
import six
from apmecclient.common import constants
from apmecclient.common import exceptions as exception
from apmecclient.i18n import _
LOG = logging.getLogger(__name__)
if six.PY3:
long = int
class ActionDispatcher(object):
"""Maps method name to local methods through action name."""
def dispatch(self, *args, **kwargs):
"""Find and call local method."""
action = kwargs.pop('action', 'default')
action_method = getattr(self, str(action), self.default)
return action_method(*args, **kwargs)
def default(self, data):
raise NotImplementedError()
class DictSerializer(ActionDispatcher):
"""Default request body serialization."""
def serialize(self, data, action='default'):
return self.dispatch(data, action=action)
def default(self, data):
return ""
class JSONDictSerializer(DictSerializer):
"""Default JSON request body serialization."""
def default(self, data):
def sanitizer(obj):
return six.text_type(obj)
return jsonutils.dumps(data, default=sanitizer)
class XMLDictSerializer(DictSerializer):
def __init__(self, metadata=None, xmlns=None):
"""XMLDictSerializer constructor.
:param metadata: information needed to deserialize XML into
a dictionary.
:param xmlns: XML namespace to include with serialized XML
"""
super(XMLDictSerializer, self).__init__()
self.metadata = metadata or {}
if not xmlns:
xmlns = self.metadata.get('xmlns')
if not xmlns:
xmlns = constants.XML_NS_V10
self.xmlns = xmlns
def default(self, data):
"""Default serializer of XMLDictSerializer.
:param data: expect data to contain a single key as XML root, or
contain another '*_links' key as atom links. Other
case will use 'VIRTUAL_ROOT_KEY' as XML root.
"""
try:
links = None
has_atom = False
if data is None:
root_key = constants.VIRTUAL_ROOT_KEY
root_value = None
else:
link_keys = [k for k in six.iterkeys(data) or []
if k.endswith('_links')]
if link_keys:
links = data.pop(link_keys[0], None)
has_atom = True
root_key = (len(data) == 1 and
list(data.keys())[0] or constants.VIRTUAL_ROOT_KEY)
root_value = data.get(root_key, data)
doc = etree.Element("_temp_root")
used_prefixes = []
self._to_xml_node(doc, self.metadata, root_key,
root_value, used_prefixes)
if links:
self._create_link_nodes(list(doc)[0], links)
return self.to_xml_string(list(doc)[0], used_prefixes, has_atom)
except AttributeError as e:
LOG.exception(str(e))
return ''
def __call__(self, data):
# Provides a migration path to a cleaner WSGI layer, this
# "default" stuff and extreme extensibility isn't being used
# like originally intended
return self.default(data)
def to_xml_string(self, node, used_prefixes, has_atom=False):
self._add_xmlns(node, used_prefixes, has_atom)
return etree.tostring(node, encoding='UTF-8')
# NOTE(ameade): the has_atom should be removed after all of the
# XML serializers and view builders have been updated to the current
# spec that required all responses include the xmlns:atom, the has_atom
# flag is to prevent current tests from breaking
def _add_xmlns(self, node, used_prefixes, has_atom=False):
node.set('xmlns', self.xmlns)
node.set(constants.TYPE_XMLNS, self.xmlns)
if has_atom:
node.set(constants.ATOM_XMLNS, constants.ATOM_NAMESPACE)
node.set(constants.XSI_NIL_ATTR, constants.XSI_NAMESPACE)
ext_ns = self.metadata.get(constants.EXT_NS, {})
for prefix in used_prefixes:
if prefix in ext_ns:
node.set('xmlns:' + prefix, ext_ns[prefix])
def _to_xml_node(self, parent, metadata, nodename, data, used_prefixes):
"""Recursive method to convert data members to XML nodes."""
result = etree.SubElement(parent, nodename)
if ":" in nodename:
used_prefixes.append(nodename.split(":", 1)[0])
# TODO(bcwaldon): accomplish this without a type-check
if isinstance(data, list):
if not data:
result.set(
constants.TYPE_ATTR,
constants.TYPE_LIST)
return result
singular = metadata.get('plurals', {}).get(nodename, None)
if singular is None:
if nodename.endswith('s'):
singular = nodename[:-1]
else:
singular = 'item'
for item in data:
self._to_xml_node(result, metadata, singular, item,
used_prefixes)
# TODO(bcwaldon): accomplish this without a type-check
elif isinstance(data, dict):
if not data:
result.set(
constants.TYPE_ATTR,
constants.TYPE_DICT)
return result
attrs = metadata.get('attributes', {}).get(nodename, {})
for k, v in sorted(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 = six.text_type(data)
return result
def _create_link_nodes(self, xml_doc, links):
for link in links:
link_node = etree.SubElement(xml_doc, 'atom:link')
link_node.set('rel', link['rel'])
link_node.set('href', link['href'])
class TextDeserializer(ActionDispatcher):
"""Default request body deserialization."""
def deserialize(self, datastring, action='default'):
return self.dispatch(datastring, action=action)
def default(self, datastring):
return {}
class JSONDeserializer(TextDeserializer):
def _from_json(self, datastring):
try:
return jsonutils.loads(datastring)
except ValueError:
msg = _("Cannot understand JSON")
raise exception.MalformedResponseBody(reason=msg)
def default(self, datastring):
return {'body': self._from_json(datastring)}
class XMLDeserializer(TextDeserializer):
def __init__(self, metadata=None):
"""XMLDeserializer constructor.
:param metadata: information needed to deserialize XML into
a dictionary.
"""
super(XMLDeserializer, self).__init__()
self.metadata = metadata or {}
xmlns = self.metadata.get('xmlns')
if not xmlns:
xmlns = constants.XML_NS_V10
self.xmlns = xmlns
def _get_key(self, tag):
tags = tag.split("}", 1)
if len(tags) == 2:
ns = tags[0][1:]
bare_tag = tags[1]
ext_ns = self.metadata.get(constants.EXT_NS, {})
if ns == self.xmlns:
return bare_tag
for prefix, _ns in ext_ns.items():
if ns == _ns:
return prefix + ":" + bare_tag
else:
return tag
def _get_links(self, root_tag, node):
link_nodes = node.findall(constants.ATOM_LINK_NOTATION)
root_tag = self._get_key(node.tag)
link_key = "%s_links" % root_tag
link_list = []
for link in link_nodes:
link_list.append({'rel': link.get('rel'),
'href': link.get('href')})
# Remove link node in order to avoid link node being
# processed as an item in _from_xml_node
node.remove(link)
return link_list and {link_key: link_list} or {}
def _from_xml(self, datastring):
if datastring is None:
return None
plurals = set(self.metadata.get('plurals', {}))
try:
node = etree.fromstring(datastring)
root_tag = self._get_key(node.tag)
links = self._get_links(root_tag, node)
result = self._from_xml_node(node, plurals)
# There is no case where root_tag = constants.VIRTUAL_ROOT_KEY
# and links is not None because of the way data are serialized
if root_tag == constants.VIRTUAL_ROOT_KEY:
return result
return dict({root_tag: result}, **links)
except Exception as e:
parseError = False
# Python2.7
if (hasattr(etree, 'ParseError') and
isinstance(e, getattr(etree, 'ParseError'))):
parseError = True
# Python2.6
elif isinstance(e, expat.ExpatError):
parseError = True
if parseError:
msg = _("Cannot understand XML")
raise exception.MalformedResponseBody(reason=msg)
else:
raise
def _from_xml_node(self, node, listnames):
"""Convert a minidom node to a simple Python type.
:param node: minidom node name
:param listnames: list of XML node names whose subnodes should
be considered list items.
"""
attrNil = node.get(str(etree.QName(constants.XSI_NAMESPACE, "nil")))
attrType = node.get(str(etree.QName(
self.metadata.get('xmlns'), "type")))
if (attrNil and attrNil.lower() == 'true'):
return None
elif not len(node) and not node.text:
if (attrType and attrType == constants.TYPE_DICT):
return {}
elif (attrType and attrType == constants.TYPE_LIST):
return []
else:
return ''
elif (len(node) == 0 and node.text):
converters = {constants.TYPE_BOOL:
lambda x: x.lower() == 'true',
constants.TYPE_INT:
lambda x: int(x),
constants.TYPE_LONG:
lambda x: long(x),
constants.TYPE_FLOAT:
lambda x: float(x)}
if attrType and attrType in converters:
return converters[attrType](node.text)
else:
return node.text
elif self._get_key(node.tag) in listnames:
return [self._from_xml_node(n, listnames) for n in node]
else:
result = dict()
for attr in node.keys():
if (attr == 'xmlns' or
attr.startswith('xmlns:') or
attr == constants.XSI_ATTR or
attr == constants.TYPE_ATTR):
continue
result[self._get_key(attr)] = node.get(attr)
children = list(node)
for child in children:
result[self._get_key(child.tag)] = self._from_xml_node(
child, listnames)
return result
def default(self, datastring):
return {'body': self._from_xml(datastring)}
def __call__(self, datastring):
# Adding a migration path to allow us to remove unncessary classes
return self.default(datastring)
# NOTE(maru): this class is duplicated from apmec.wsgi
class Serializer(object):
"""Serializes and deserializes dictionaries to certain MIME types."""
def __init__(self, metadata=None, default_xmlns=None):
"""Create a serializer based on the given WSGI environment.
'metadata' is an optional dict mapping MIME types to information
needed to serialize a dictionary to that type.
"""
self.metadata = metadata or {}
self.default_xmlns = default_xmlns
def _get_serialize_handler(self, content_type):
handlers = {
'application/json': JSONDictSerializer(),
'application/xml': XMLDictSerializer(self.metadata),
}
try:
return handlers[content_type]
except Exception:
raise exception.InvalidContentType(content_type=content_type)
def serialize(self, data, content_type):
"""Serialize a dictionary into the specified content type."""
return self._get_serialize_handler(content_type).serialize(data)
def deserialize(self, datastring, content_type):
"""Deserialize a string to a dictionary.
The string must be in the format of a supported MIME type.
"""
return self.get_deserialize_handler(content_type).deserialize(
datastring)
def get_deserialize_handler(self, content_type):
handlers = {
'application/json': JSONDeserializer(),
'application/xml': XMLDeserializer(self.metadata),
}
try:
return handlers[content_type]
except Exception:
raise exception.InvalidContentType(content_type=content_type)

186
apmecclient/common/utils.py Normal file
View File

@ -0,0 +1,186 @@
# Copyright 2011, VMware, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Borrowed from nova code base, more utilities will be added/borrowed as and
# when needed.
"""Utilities and helper functions."""
import argparse
import logging
import os
from oslo_log import versionutils
from oslo_utils import encodeutils
from oslo_utils import importutils
import six
from apmecclient.common import exceptions
from apmecclient.i18n import _
def env(*vars, **kwargs):
"""Returns the first environment variable set.
If none are non-empty, defaults to '' or keyword arg default.
"""
for v in vars:
value = os.environ.get(v)
if value:
return value
return kwargs.get('default', '')
def get_client_class(api_name, version, version_map):
"""Returns the client class for the requested API version.
:param api_name: the name of the API, e.g. 'compute', 'image', etc
:param version: the requested API version
:param version_map: a dict of client classes keyed by version
:rtype: a client class for the requested API version
"""
try:
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = _("Invalid %(api_name)s client version '%(version)s'. must be "
"one of: %(map_keys)s")
msg = msg % {'api_name': api_name, 'version': version,
'map_keys': ', '.join(version_map.keys())}
raise exceptions.UnsupportedVersion(msg)
return importutils.import_class(client_path)
def get_item_properties(item, fields, mixed_case_fields=(), formatters=None):
"""Return a tuple containing the item properties.
:param item: a single item resource (e.g. Server, Tenant, etc)
:param fields: tuple of strings with the desired field names
:param mixed_case_fields: tuple of field names to preserve case
:param formatters: dictionary mapping field names to callables
to format the values
"""
if formatters is None:
formatters = {}
row = []
for field in fields:
if field in formatters:
row.append(formatters[field](item))
else:
if field in mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
if not hasattr(item, field_name) and isinstance(item, dict):
data = item[field_name]
else:
data = getattr(item, field_name, '')
if data is None:
data = ''
row.append(data)
return tuple(row)
def str2bool(strbool):
if strbool is None:
return None
return strbool.lower() == 'true'
def str2dict(strdict):
"""Convert key1=value1,key2=value2,... string into dictionary.
:param strdict: key1=value1,key2=value2
"""
if not strdict:
return {}
return dict([kv.split('=', 1) for kv in strdict.split(',')])
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']))
req = encodeutils.safe_encode("".join(string_parts))
_logger.debug("\nREQ: %s\n", req)
def http_log_resp(_logger, resp, body):
if not _logger.isEnabledFor(logging.DEBUG):
return
_logger.debug("RESP:%(code)s %(headers)s %(body)s\n",
{'code': resp.status_code,
'headers': resp.headers,
'body': body})
def _safe_encode_without_obj(data):
if isinstance(data, six.string_types):
return encodeutils.safe_encode(data)
return data
def safe_encode_list(data):
return list(map(_safe_encode_without_obj, data))
def safe_encode_dict(data):
def _encode_item(item):
k, v = item
if isinstance(v, list):
return (k, safe_encode_list(v))
elif isinstance(v, dict):
return (k, safe_encode_dict(v))
return (k, _safe_encode_without_obj(v))
return dict(list(map(_encode_item, data.items())))
def add_boolean_argument(parser, name, **kwargs):
for keyword in ('metavar', 'choices'):
kwargs.pop(keyword, None)
default = kwargs.pop('default', argparse.SUPPRESS)
parser.add_argument(
name,
metavar='{True,False}',
choices=['True', 'true', 'False', 'false'],
default=default,
**kwargs)
def get_file_path(filename):
file_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
'../%s' % filename))
return file_path
def deprecate_warning(what, as_of, in_favor_of=None, remove_in=1):
versionutils.deprecation_warning(as_of=as_of, what=what,
in_favor_of=in_favor_of,
remove_in=remove_in)

View File

@ -0,0 +1,69 @@
# Copyright 2014 NEC Corporation
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import netaddr
from apmecclient.common import exceptions
from apmecclient.i18n import _
def validate_int_range(parsed_args, attr_name, min_value=None, max_value=None):
val = getattr(parsed_args, attr_name, None)
if val is None:
return
try:
if not isinstance(val, int):
int_val = int(val, 0)
else:
int_val = val
if ((min_value is None or min_value <= int_val) and
(max_value is None or int_val <= max_value)):
return
except (ValueError, TypeError):
pass
if min_value is not None and max_value is not None:
msg = (_('%(attr_name)s "%(val)s" should be an integer '
'[%(min)i:%(max)i].') %
{'attr_name': attr_name.replace('_', '-'),
'val': val, 'min': min_value, 'max': max_value})
elif min_value is not None:
msg = (_('%(attr_name)s "%(val)s" should be an integer '
'greater than or equal to %(min)i.') %
{'attr_name': attr_name.replace('_', '-'),
'val': val, 'min': min_value})
elif max_value is not None:
msg = (_('%(attr_name)s "%(val)s" should be an integer '
'smaller than or equal to %(max)i.') %
{'attr_name': attr_name.replace('_', '-'),
'val': val, 'max': max_value})
else:
msg = (_('%(attr_name)s "%(val)s" should be an integer.') %
{'attr_name': attr_name.replace('_', '-'),
'val': val})
raise exceptions.CommandError(msg)
def validate_ip_subnet(parsed_args, attr_name):
val = getattr(parsed_args, attr_name)
if not val:
return
try:
netaddr.IPNetwork(val)
except (netaddr.AddrFormatError, ValueError):
raise exceptions.CommandError(
(_('%(attr_name)s "%(val)s" is not a valid CIDR.') %
{'attr_name': attr_name.replace('_', '-'), 'val': val}))

18
apmecclient/i18n.py Normal file
View File

@ -0,0 +1,18 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import oslo_i18n as i18n
_translators = i18n.TranslatorFactory(domain='apmecclient')
# The primary translation function using the well-known name "_"
_ = _translators.primary

837
apmecclient/shell.py Normal file
View File

@ -0,0 +1,837 @@
# Copyright 2012 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""
Command-line interface to the Apmec APIs
"""
from __future__ import print_function
import argparse
import getpass
import inspect
import itertools
import logging
import os
import sys
from keystoneclient.auth.identity import v2 as v2_auth
from keystoneclient.auth.identity import v3 as v3_auth
from keystoneclient import discover
from keystoneclient import exceptions as ks_exc
from keystoneclient import session
from oslo_utils import encodeutils
import six.moves.urllib.parse as urlparse
from cliff import app
from cliff import commandmanager
from apmecclient.common import clientmanager
from apmecclient.common import command as openstack_command
from apmecclient.common import exceptions as exc
from apmecclient.common import extension as client_extension
from apmecclient.common import utils
from apmecclient.i18n import _
from apmecclient.apmec.v1_0.events import events
from apmecclient.apmec.v1_0 import extension
from apmecclient.apmec.v1_0.meo import mes
from apmecclient.apmec.v1_0.meo import mesd
from apmecclient.apmec.v1_0.meo import vim
from apmecclient.apmec.v1_0.mem import mea
from apmecclient.apmec.v1_0.mem import mead
from apmecclient.version import __version__
VERSION = '1.0'
APMEC_API_VERSION = '1.0'
def run_command(cmd, cmd_parser, sub_argv):
_argv = sub_argv
index = -1
values_specs = []
if '--' in sub_argv:
index = sub_argv.index('--')
_argv = sub_argv[:index]
values_specs = sub_argv[index:]
known_args, _values_specs = cmd_parser.parse_known_args(_argv)
cmd.values_specs = (index == -1 and _values_specs or values_specs)
return cmd.run(known_args)
def env(*_vars, **kwargs):
"""Search for the first defined of possibly many env vars.
Returns the first environment variable defined in vars, or
returns the default defined in kwargs.
"""
for v in _vars:
value = os.environ.get(v, None)
if value:
return value
return kwargs.get('default', '')
def check_non_negative_int(value):
try:
value = int(value)
except ValueError:
raise argparse.ArgumentTypeError(_("invalid int value: %r") % value)
if value < 0:
raise argparse.ArgumentTypeError(_("input value %d is negative") %
value)
return value
class BashCompletionCommand(openstack_command.OpenStackCommand):
"""Prints all of the commands and options for bash-completion."""
resource = "bash_completion"
COMMAND_V1 = {
'bash-completion': BashCompletionCommand,
'ext-list': extension.ListExt,
'ext-show': extension.ShowExt,
# MANO lingo
'mead-create': mead.CreateMEAD,
'mead-delete': mead.DeleteMEAD,
'mead-list': mead.ListMEAD,
'mead-show': mead.ShowMEAD,
'mead-template-show': mead.ShowTemplateMEAD,
'mea-create': mea.CreateMEA,
'mea-update': mea.UpdateMEA,
'mea-delete': mea.DeleteMEA,
'mea-list': mea.ListMEA,
'mea-show': mea.ShowMEA,
'mea-scale': mea.ScaleMEA,
'mea-resource-list': mea.ListMEAResources,
# 'mea-config-create'
# 'mea-config-push'
'vim-register': vim.CreateVIM,
'vim-update': vim.UpdateVIM,
'vim-delete': vim.DeleteVIM,
'vim-list': vim.ListVIM,
'vim-show': vim.ShowVIM,
'events-list': events.ListResourceEvents,
'event-show': events.ShowEvent,
'mea-events-list': events.ListMEAEvents,
'vim-events-list': events.ListVIMEvents,
'mead-events-list': events.ListMEADEvents,
'mesd-create': mesd.CreateMESD,
'mesd-list': mesd.ListMESD,
'mesd-delete': mesd.DeleteMESD,
'mesd-show': mesd.ShowMESD,
'mesd-template-show': mesd.ShowTemplateMESD,
'mes-create': mes.CreateMES,
'mes-list': mes.ListMES,
'mes-delete': mes.DeleteMES,
'mes-show': mes.ShowMES,
}
COMMANDS = {'1.0': COMMAND_V1}
class HelpAction(argparse.Action):
"""Provides a custom action for the -h and --help options.
The commands are determined by checking the CommandManager
instance, passed in as the "default" value for the action.
:returns: a list of the commands
"""
def __call__(self, parser, namespace, values, option_string=None):
outputs = []
max_len = 0
app = self.default
parser.print_help(app.stdout)
app.stdout.write(_('\nCommands for API v%s:\n') % app.api_version)
command_manager = app.command_manager
for name, ep in sorted(command_manager):
factory = ep.load()
cmd = factory(self, None)
one_liner = cmd.get_description().split('\n')[0]
outputs.append((name, one_liner))
max_len = max(len(name), max_len)
for (name, one_liner) in outputs:
app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner))
sys.exit(0)
class ApmecShell(app.App):
# verbose logging levels
WARNING_LEVEL = 0
INFO_LEVEL = 1
DEBUG_LEVEL = 2
CONSOLE_MESSAGE_FORMAT = '%(message)s'
DEBUG_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s'
log = logging.getLogger(__name__)
def __init__(self, apiversion):
super(ApmecShell, self).__init__(
description=__doc__.strip(),
version=VERSION,
command_manager=commandmanager.CommandManager('apmec.cli'), )
self.commands = COMMANDS
for k, v in self.commands[apiversion].items():
self.command_manager.add_command(k, v)
self._register_extensions(VERSION)
# Pop the 'complete' to correct the outputs of 'apmec help'.
self.command_manager.commands.pop('complete')
# This is instantiated in initialize_app() only when using
# password flow auth
self.auth_client = None
self.api_version = apiversion
def build_option_parser(self, description, version):
"""Return an argparse option parser for this application.
Subclasses may override this method to extend
the parser with more global options.
:param description: full description of the application
:paramtype description: str
:param version: version number for the application
:paramtype version: str
"""
parser = argparse.ArgumentParser(
description=description,
add_help=False, )
parser.add_argument(
'--version',
action='version',
version=__version__, )
parser.add_argument(
'-v', '--verbose', '--debug',
action='count',
dest='verbose_level',
default=self.DEFAULT_VERBOSE_LEVEL,
help=_('Increase verbosity of output and show tracebacks on'
' errors. You can repeat this option.'))
parser.add_argument(
'-q', '--quiet',
action='store_const',
dest='verbose_level',
const=0,
help=_('Suppress output except warnings and errors.'))
parser.add_argument(
'-h', '--help',
action=HelpAction,
nargs=0,
default=self, # tricky
help=_("Show this help message and exit."))
parser.add_argument(
'-r', '--retries',
metavar="NUM",
type=check_non_negative_int,
default=0,
help=_("How many times the request to the Apmec server should "
"be retried if it fails."))
# FIXME(bklei): this method should come from python-keystoneclient
self._append_global_identity_args(parser)
return parser
def _append_global_identity_args(self, parser):
# FIXME(bklei): these are global identity (Keystone) arguments which
# should be consistent and shared by all service clients. Therefore,
# they should be provided by python-keystoneclient. We will need to
# refactor this code once this functionality is available in
# python-keystoneclient.
#
# Note: At that time we'll need to decide if we can just abandon
# the deprecated args (--service-type and --endpoint-type).
parser.add_argument(
'--os-service-type', metavar='<os-service-type>',
default=env('OS_APMEC_SERVICE_TYPE',
default='mec-orchestration'),
help=_('Defaults to env[OS_APMEC_SERVICE_TYPE] or \
mec-orchestration.'))
parser.add_argument(
'--os-endpoint-type', metavar='<os-endpoint-type>',
default=env('OS_ENDPOINT_TYPE', default='publicURL'),
help=_('Defaults to env[OS_ENDPOINT_TYPE] or publicURL.'))
# FIXME(bklei): --service-type is deprecated but kept in for
# backward compatibility.
parser.add_argument(
'--service-type', metavar='<service-type>',
default=env('OS_APMEC_SERVICE_TYPE',
default='mec-orchestration'),
help=_('DEPRECATED! Use --os-service-type.'))
# FIXME(bklei): --endpoint-type is deprecated but kept in for
# backward compatibility.
parser.add_argument(
'--endpoint-type', metavar='<endpoint-type>',
default=env('OS_ENDPOINT_TYPE', default='publicURL'),
help=_('DEPRECATED! Use --os-endpoint-type.'))
parser.add_argument(
'--os-auth-strategy', metavar='<auth-strategy>',
default=env('OS_AUTH_STRATEGY', default='keystone'),
help=_('DEPRECATED! Only keystone is supported.'))
parser.add_argument(
'--os_auth_strategy',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-auth-url', metavar='<auth-url>',
default=env('OS_AUTH_URL'),
help=_('Authentication URL, defaults to env[OS_AUTH_URL].'))
parser.add_argument(
'--os_auth_url',
help=argparse.SUPPRESS)
project_name_group = parser.add_mutually_exclusive_group()
project_name_group.add_argument(
'--os-tenant-name', metavar='<auth-tenant-name>',
default=env('OS_TENANT_NAME'),
help=_('Authentication tenant name, defaults to '
'env[OS_TENANT_NAME].'))
project_name_group.add_argument(
'--os-project-name',
metavar='<auth-project-name>',
default=utils.env('OS_PROJECT_NAME'),
help=_('Another way to specify tenant name. '
'This option is mutually exclusive with '
' --os-tenant-name. '
'Defaults to env[OS_PROJECT_NAME].'))
parser.add_argument(
'--os_tenant_name',
help=argparse.SUPPRESS)
project_id_group = parser.add_mutually_exclusive_group()
project_id_group.add_argument(
'--os-tenant-id', metavar='<auth-tenant-id>',
default=env('OS_TENANT_ID'),
help=_('Authentication tenant ID, defaults to '
'env[OS_TENANT_ID].'))
project_id_group.add_argument(
'--os-project-id',
metavar='<auth-project-id>',
default=utils.env('OS_PROJECT_ID'),
help=_('Another way to specify tenant ID. '
'This option is mutually exclusive with '
' --os-tenant-id. '
'Defaults to env[OS_PROJECT_ID].'))
parser.add_argument(
'--os-username', metavar='<auth-username>',
default=utils.env('OS_USERNAME'),
help=_('Authentication username, defaults to env[OS_USERNAME].'))
parser.add_argument(
'--os_username',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-user-id', metavar='<auth-user-id>',
default=env('OS_USER_ID'),
help=_('Authentication user ID (Env: OS_USER_ID)'))
parser.add_argument(
'--os_user_id',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-user-domain-id',
metavar='<auth-user-domain-id>',
default=utils.env('OS_USER_DOMAIN_ID'),
help=_('OpenStack user domain ID. '
'Defaults to env[OS_USER_DOMAIN_ID].'))
parser.add_argument(
'--os_user_domain_id',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-user-domain-name',
metavar='<auth-user-domain-name>',
default=utils.env('OS_USER_DOMAIN_NAME'),
help=_('OpenStack user domain name. '
'Defaults to env[OS_USER_DOMAIN_NAME].'))
parser.add_argument(
'--os_user_domain_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--os_project_id',
help=argparse.SUPPRESS)
parser.add_argument(
'--os_project_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-project-domain-id',
metavar='<auth-project-domain-id>',
default=utils.env('OS_PROJECT_DOMAIN_ID'),
help=_('Defaults to env[OS_PROJECT_DOMAIN_ID].'))
parser.add_argument(
'--os-project-domain-name',
metavar='<auth-project-domain-name>',
default=utils.env('OS_PROJECT_DOMAIN_NAME'),
help=_('Defaults to env[OS_PROJECT_DOMAIN_NAME].'))
parser.add_argument(
'--os-cert',
metavar='<certificate>',
default=utils.env('OS_CERT'),
help=_("Path of certificate file to use in SSL "
"connection. This file can optionally be "
"prepended with the private key. Defaults "
"to env[OS_CERT]."))
parser.add_argument(
'--os-cacert',
metavar='<ca-certificate>',
default=env('OS_CACERT', default=None),
help=_("Specify a CA bundle file to use in "
"verifying a TLS (https) server certificate. "
"Defaults to env[OS_CACERT]."))
parser.add_argument(
'--os-key',
metavar='<key>',
default=utils.env('OS_KEY'),
help=_("Path of client key to use in SSL "
"connection. This option is not necessary "
"if your key is prepended to your certificate "
"file. Defaults to env[OS_KEY]."))
parser.add_argument(
'--os-password', metavar='<auth-password>',
default=utils.env('OS_PASSWORD'),
help=_('Authentication password, defaults to env[OS_PASSWORD].'))
parser.add_argument(
'--os_password',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-region-name', metavar='<auth-region-name>',
default=env('OS_REGION_NAME'),
help=_('Authentication region name, defaults to '
'env[OS_REGION_NAME].'))
parser.add_argument(
'--os_region_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-token', metavar='<token>',
default=env('OS_TOKEN'),
help=_('Authentication token, defaults to env[OS_TOKEN].'))
parser.add_argument(
'--os_token',
help=argparse.SUPPRESS)
parser.add_argument(
'--http-timeout', metavar='<seconds>',
default=env('OS_NETWORK_TIMEOUT', default=None), type=float,
help=_('Timeout in seconds to wait for an HTTP response. Defaults '
'to env[OS_NETWORK_TIMEOUT] or None if not specified.'))
parser.add_argument(
'--os-url', metavar='<url>',
default=env('OS_URL'),
help=_('Defaults to env[OS_URL].'))
parser.add_argument(
'--os_url',
help=argparse.SUPPRESS)
parser.add_argument(
'--insecure',
action='store_true',
default=env('APMECCLIENT_INSECURE', default=False),
help=_("Explicitly allow apmecclient to perform \"insecure\" "
"SSL (https) requests. The server's certificate will "
"not be verified against any certificate authorities. "
"This option should be used with caution."))
def _bash_completion(self):
"""Prints all of the commands and options for bash-completion."""
commands = set()
options = set()
for option, _action in self.parser._option_string_actions.items():
options.add(option)
for command_name, command in self.command_manager:
commands.add(command_name)
cmd_factory = command.load()
cmd = cmd_factory(self, None)
cmd_parser = cmd.get_parser('')
for option, _action in cmd_parser._option_string_actions.items():
options.add(option)
print(' '.join(commands | options))
def _register_extensions(self, version):
for name, module in itertools.chain(
client_extension._discover_via_entry_points()):
self._extend_shell_commands(module, version)
def _extend_shell_commands(self, module, version):
classes = inspect.getmembers(module, inspect.isclass)
for cls_name, cls in classes:
if (issubclass(cls, client_extension.ApmecClientExtension) and
hasattr(cls, 'shell_command')):
cmd = cls.shell_command
if hasattr(cls, 'versions'):
if version not in cls.versions:
continue
try:
self.command_manager.add_command(cmd, cls)
self.commands[version][cmd] = cls
except TypeError:
pass
def run(self, argv):
"""Equivalent to the main program for the application.
:param argv: input arguments and options
:paramtype argv: list of str
"""
try:
index = 0
command_pos = -1
help_pos = -1
help_command_pos = -1
for arg in argv:
if arg == 'bash-completion' and help_command_pos == -1:
self._bash_completion()
return 0
if arg in self.commands[self.api_version]:
if command_pos == -1:
command_pos = index
elif arg in ('-h', '--help'):
if help_pos == -1:
help_pos = index
elif arg == 'help':
if help_command_pos == -1:
help_command_pos = index
index = index + 1
if command_pos > -1 and help_pos > command_pos:
argv = ['help', argv[command_pos]]
if help_command_pos > -1 and command_pos == -1:
argv[help_command_pos] = '--help'
self.options, remainder = self.parser.parse_known_args(argv)
self.configure_logging()
self.interactive_mode = not remainder
self.initialize_app(remainder)
except Exception as err:
if self.options.verbose_level >= self.DEBUG_LEVEL:
self.log.exception(err)
raise
else:
self.log.error(err)
return 1
if self.interactive_mode:
_argv = [sys.argv[0]]
sys.argv = _argv
return self.interact()
return self.run_subcommand(remainder)
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)
try:
self.prepare_to_run_command(cmd)
full_name = (cmd_name
if self.interactive_mode
else ' '.join([self.NAME, cmd_name])
)
cmd_parser = cmd.get_parser(full_name)
return run_command(cmd, cmd_parser, sub_argv)
except Exception as e:
if self.options.verbose_level >= self.DEBUG_LEVEL:
self.log.exception("%s", e)
raise
self.log.error("%s", e)
return 1
def authenticate_user(self):
"""Authentication validation.
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]"
" when providing a service URL"))
if not self.options.os_url:
raise exc.CommandError(
_("You must provide a service URL via"
" either --os-url or env[OS_URL]"
" when providing a token"))
else:
# Validate password flow auth
project_info = (self.options.os_tenant_name or
self.options.os_tenant_id or
(self.options.os_project_name and
(self.options.os_project_domain_name or
self.options.os_project_domain_id)) or
self.options.os_project_id)
if (not self.options.os_username
and not self.options.os_user_id):
raise exc.CommandError(
_("You must provide a username or user ID via"
" --os-username, env[OS_USERNAME] or"
" --os-user-id, env[OS_USER_ID]"))
if not self.options.os_password:
# No password, If we've got a tty, try prompting for it
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
# Check for Ctl-D
try:
self.options.os_password = getpass.getpass(
'OS Password: ')
except EOFError:
pass
# No password because we didn't have a tty or the
# user Ctl-D when prompted.
if not self.options.os_password:
raise exc.CommandError(
_("You must provide a password via"
" either --os-password or env[OS_PASSWORD]"))
if (not project_info):
# tenent is deprecated in Keystone v3. Use the latest
# terminology instead.
raise exc.CommandError(
_("You must provide a project_id or project_name ("
"with project_domain_name or project_domain_id) "
"via "
" --os-project-id (env[OS_PROJECT_ID])"
" --os-project-name (env[OS_PROJECT_NAME]),"
" --os-project-domain-id "
"(env[OS_PROJECT_DOMAIN_ID])"
" --os-project-domain-name "
"(env[OS_PROJECT_DOMAIN_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]"))
auth_session = self._get_keystone_session()
auth = auth_session.auth
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]"))
auth_session = None
auth = None
self.client_manager = clientmanager.ClientManager(
token=self.options.os_token,
url=self.options.os_url,
auth_url=self.options.os_auth_url,
tenant_name=self.options.os_tenant_name,
tenant_id=self.options.os_tenant_id,
username=self.options.os_username,
user_id=self.options.os_user_id,
password=self.options.os_password,
region_name=self.options.os_region_name,
api_version=self.api_version,
auth_strategy=self.options.os_auth_strategy,
# FIXME (bklei) honor deprecated service_type and
# endpoint type until they are removed
service_type=self.options.os_service_type or
self.options.service_type,
endpoint_type=self.options.os_endpoint_type or self.endpoint_type,
insecure=self.options.insecure,
ca_cert=self.options.os_cacert,
timeout=self.options.http_timeout,
retries=self.options.retries,
raise_errors=False,
session=auth_session,
auth=auth,
log_credentials=True)
return
def initialize_app(self, argv):
"""Global app init bits:
* set up API versions
* validate authentication info
"""
super(ApmecShell, self).initialize_app(argv)
self.api_version = {'mec-orchestration':
self.api_version}
# If the user is not asking for help, make sure they
# have given us auth.
cmd_name = None
if argv:
cmd_info = self.command_manager.find_command(argv)
cmd_factory, cmd_name, sub_argv = cmd_info
if self.interactive_mode or cmd_name != 'help':
self.authenticate_user()
def configure_logging(self):
"""Create logging handlers for any log output."""
root_logger = logging.getLogger('')
# Set up logging to a file
root_logger.setLevel(logging.DEBUG)
# Send higher-level messages to the console via stderr
console = logging.StreamHandler(self.stderr)
console_level = {self.WARNING_LEVEL: logging.WARNING,
self.INFO_LEVEL: logging.INFO,
self.DEBUG_LEVEL: logging.DEBUG,
}.get(self.options.verbose_level, logging.DEBUG)
# The default log level is INFO, in this situation, set the
# log level of the console to WARNING, to avoid displaying
# useless messages. This equals using "--quiet"
if console_level == logging.INFO:
console.setLevel(logging.WARNING)
else:
console.setLevel(console_level)
if logging.DEBUG == console_level:
formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT)
else:
formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT)
logging.getLogger('iso8601.iso8601').setLevel(logging.WARNING)
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
console.setFormatter(formatter)
root_logger.addHandler(console)
return
def get_v2_auth(self, v2_auth_url):
return v2_auth.Password(
v2_auth_url,
username=self.options.os_username,
password=self.options.os_password,
tenant_id=self.options.os_tenant_id,
tenant_name=self.options.os_tenant_name)
def get_v3_auth(self, v3_auth_url):
project_id = self.options.os_project_id or self.options.os_tenant_id
project_name = (self.options.os_project_name or
self.options.os_tenant_name)
return v3_auth.Password(
v3_auth_url,
username=self.options.os_username,
password=self.options.os_password,
user_id=self.options.os_user_id,
user_domain_name=self.options.os_user_domain_name,
user_domain_id=self.options.os_user_domain_id,
project_id=project_id,
project_name=project_name,
project_domain_name=self.options.os_project_domain_name,
project_domain_id=self.options.os_project_domain_id
)
def _discover_auth_versions(self, session, auth_url):
# discover the API versions the server is supporting base on the
# given URL
try:
ks_discover = discover.Discover(session=session, auth_url=auth_url)
return (ks_discover.url_for('2.0'), ks_discover.url_for('3.0'))
except ks_exc.ClientException:
# Identity service may not support discover API version.
# Lets try to figure out the API version from the original URL.
url_parts = urlparse.urlparse(auth_url)
(scheme, netloc, path, params, query, fragment) = url_parts
path = path.lower()
if path.startswith('/v3'):
return (None, auth_url)
elif path.startswith('/v2'):
return (auth_url, None)
else:
# not enough information to determine the auth version
msg = _('Unable to determine the Keystone version '
'to authenticate with using the given '
'auth_url. Identity service may not support API '
'version discovery. Please provide a versioned '
'auth_url instead.')
raise exc.CommandError(msg)
def _get_keystone_session(self):
# first create a Keystone session
cacert = self.options.os_cacert or None
cert = self.options.os_cert or None
key = self.options.os_key or None
insecure = self.options.insecure or False
ks_session = session.Session.construct(dict(cacert=cacert,
cert=cert,
key=key,
insecure=insecure))
# discover the supported keystone versions using the given url
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
session=ks_session,
auth_url=self.options.os_auth_url)
# Determine which authentication plugin to use. First inspect the
# auth_url to see the supported version. If both v3 and v2 are
# supported, then use the highest version if possible.
user_domain_name = self.options.os_user_domain_name or None
user_domain_id = self.options.os_user_domain_id or None
project_domain_name = self.options.os_project_domain_name or None
project_domain_id = self.options.os_project_domain_id or None
domain_info = (user_domain_name or user_domain_id or
project_domain_name or project_domain_id)
if (v2_auth_url and not domain_info) or not v3_auth_url:
ks_session.auth = self.get_v2_auth(v2_auth_url)
else:
ks_session.auth = self.get_v3_auth(v3_auth_url)
return ks_session
def main(argv=sys.argv[1:]):
try:
return ApmecShell(APMEC_API_VERSION).run(
list(map(encodeutils.safe_decode, argv)))
except KeyboardInterrupt:
print("... terminating apmec client", file=sys.stderr)
return 130
except exc.ApmecClientException:
return 1
except Exception as e:
print(e)
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

View File

View File

View File

@ -0,0 +1,370 @@
# Copyright 2012 NEC Corporation
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import copy
import json
import uuid
from keystoneclient import exceptions as k_exceptions
import mock
import requests
import testtools
from apmecclient import client
from apmecclient.common import exceptions
USERNAME = 'testuser'
USER_ID = 'testuser_id'
TENANT_NAME = 'testtenant'
TENANT_ID = 'testtenant_id'
PASSWORD = 'password'
AUTH_URL = 'authurl'
ENDPOINT_URL = 'localurl'
ENDPOINT_OVERRIDE = 'otherurl'
TOKEN = 'tokentoken'
REGION = 'RegionTest'
NOAUTH = 'noauth'
KS_TOKEN_RESULT = {
'access': {
'token': {'id': TOKEN,
'expires': '2012-08-11T07:49:01Z',
'tenant': {'id': str(uuid.uuid1())}},
'user': {'id': str(uuid.uuid1())},
'serviceCatalog': [
{'endpoints_links': [],
'endpoints': [{'adminURL': ENDPOINT_URL,
'internalURL': ENDPOINT_URL,
'publicURL': ENDPOINT_URL,
'region': REGION}],
'type': 'mec-orchestration',
'name': 'Apmec Service'}
]
}
}
ENDPOINTS_RESULT = {
'endpoints': [{
'type': 'mec-orchestration',
'name': 'Apmec Service',
'region': REGION,
'adminURL': ENDPOINT_URL,
'internalURL': ENDPOINT_URL,
'publicURL': ENDPOINT_URL
}]
}
def get_response(status_code, headers=None):
response = mock.Mock().CreateMock(requests.Response)
response.headers = headers or {}
response.status_code = status_code
return response
resp_200 = get_response(200)
resp_401 = get_response(401)
headers = {'X-Auth-Token': '',
'User-Agent': 'python-apmecclient'}
expected_headers = {'X-Auth-Token': TOKEN,
'User-Agent': 'python-apmecclient'}
agent_header = {'User-Agent': 'python-apmecclient'}
class CLITestAuthNoAuth(testtools.TestCase):
def setUp(self):
"""Prepare the test environment."""
super(CLITestAuthNoAuth, self).setUp()
self.client = client.HTTPClient(username=USERNAME,
tenant_name=TENANT_NAME,
password=PASSWORD,
endpoint_url=ENDPOINT_URL,
auth_strategy=NOAUTH,
region_name=REGION)
self.addCleanup(mock.patch.stopall)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_get_noauth(self, mock_request):
mock_request.return_value = (resp_200, '')
self.client.do_request('/resource', 'GET',
headers=headers)
mock_request.assert_called_once_with(
ENDPOINT_URL + '/resource',
'GET',
headers=headers)
self.assertEqual(self.client.endpoint_url, ENDPOINT_URL)
class CLITestAuthKeystone(testtools.TestCase):
# Auth Body expected
auth_body = ('{"auth": {"tenantName": "testtenant", '
'"passwordCredentials": '
'{"username": "testuser", "password": "password"}}}')
def setUp(self):
"""Prepare the test environment."""
super(CLITestAuthKeystone, self).setUp()
self.client = client.HTTPClient(username=USERNAME,
tenant_name=TENANT_NAME,
password=PASSWORD,
auth_url=AUTH_URL,
region_name=REGION)
self.addCleanup(mock.patch.stopall)
def test_reused_token_get_auth_info(self):
"""Test Client.get_auth_info().
Test that Client.get_auth_info() works even if client was
instantiated with predefined token.
"""
client_ = client.HTTPClient(username=USERNAME,
tenant_name=TENANT_NAME,
token=TOKEN,
password=PASSWORD,
auth_url=AUTH_URL,
region_name=REGION)
expected = {'auth_token': TOKEN,
'auth_tenant_id': None,
'auth_user_id': None,
'endpoint_url': self.client.endpoint_url}
self.assertEqual(client_.get_auth_info(), expected)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_get_token(self, mock_request):
mock_request.return_value = (resp_200, json.dumps(KS_TOKEN_RESULT))
self.client.do_request('/resource', 'GET')
mock_request.assert_called_with(
ENDPOINT_URL + '/resource', 'GET',
headers=expected_headers)
self.assertEqual(self.client.endpoint_url, ENDPOINT_URL)
self.assertEqual(self.client.auth_token, TOKEN)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_refresh_token(self, mock_request):
self.client.auth_token = TOKEN
self.client.endpoint_url = ENDPOINT_URL
# If a token is expired, apmec server retruns 401
mock_request.return_value = (resp_401, '')
self.assertRaises(exceptions.Unauthorized,
self.client.do_request,
'/resource',
'GET')
mock_request.return_value = (resp_200, json.dumps(KS_TOKEN_RESULT))
self.client.do_request('/resource', 'GET')
mock_request.assert_called_with(
ENDPOINT_URL + '/resource', 'GET',
headers=expected_headers)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_refresh_token_no_auth_url(self, mock_request):
self.client.auth_url = None
self.client.auth_token = TOKEN
self.client.endpoint_url = ENDPOINT_URL
# If a token is expired, apmec server retruns 401
mock_request.return_value = (resp_401, '')
self.assertRaises(exceptions.NoAuthURLProvided,
self.client.do_request,
'/resource',
'GET')
expected_url = ENDPOINT_URL + '/resource'
mock_request.assert_called_with(expected_url, 'GET',
headers=expected_headers)
def test_get_endpoint_url_with_invalid_auth_url(self):
# Handle the case when auth_url is not provided
self.client.auth_url = None
self.assertRaises(exceptions.NoAuthURLProvided,
self.client._get_endpoint_url)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_get_endpoint_url(self, mock_request):
self.client.auth_token = TOKEN
mock_request.return_value = (resp_200, json.dumps(ENDPOINTS_RESULT))
self.client.do_request('/resource', 'GET')
mock_request.assert_called_with(
ENDPOINT_URL + '/resource', 'GET',
headers=expected_headers)
mock_request.return_value = (resp_200, '')
self.client.do_request('/resource', 'GET',
headers=headers)
mock_request.assert_called_with(
ENDPOINT_URL + '/resource', 'GET',
headers=headers)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_use_given_endpoint_url(self, mock_request):
self.client = client.HTTPClient(
username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD,
auth_url=AUTH_URL, region_name=REGION,
endpoint_url=ENDPOINT_OVERRIDE)
self.assertEqual(self.client.endpoint_url, ENDPOINT_OVERRIDE)
self.client.auth_token = TOKEN
mock_request.return_value = (resp_200, '')
self.client.do_request('/resource', 'GET',
headers=headers)
mock_request.assert_called_with(
ENDPOINT_OVERRIDE + '/resource', 'GET',
headers=headers)
self.assertEqual(self.client.endpoint_url, ENDPOINT_OVERRIDE)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_get_endpoint_url_other(self, mock_request):
self.client = client.HTTPClient(
username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD,
auth_url=AUTH_URL, region_name=REGION, endpoint_type='otherURL')
self.client.auth_token = TOKEN
mock_request.return_value = (resp_200, json.dumps(ENDPOINTS_RESULT))
self.assertRaises(exceptions.EndpointTypeNotFound,
self.client.do_request,
'/resource',
'GET')
expected_url = AUTH_URL + '/tokens/%s/endpoints' % TOKEN
headers = {'User-Agent': 'python-apmecclient'}
mock_request.assert_called_with(expected_url, 'GET',
headers=headers)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_get_endpoint_url_failed(self, mock_request):
self.client.auth_token = TOKEN
self.client.auth_url = AUTH_URL + '/tokens/%s/endpoints' % TOKEN
mock_request.return_value = (resp_401, '')
self.assertRaises(exceptions.Unauthorized,
self.client.do_request,
'/resource',
'GET')
def test_endpoint_type(self):
resources = copy.deepcopy(KS_TOKEN_RESULT)
endpoints = resources['access']['serviceCatalog'][0]['endpoints'][0]
endpoints['internalURL'] = 'internal'
endpoints['adminURL'] = 'admin'
endpoints['publicURL'] = 'public'
# Test default behavior is to choose public.
self.client = client.HTTPClient(
username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD,
auth_url=AUTH_URL, region_name=REGION)
self.client._extract_service_catalog(resources)
self.assertEqual(self.client.endpoint_url, 'public')
# Test admin url
self.client = client.HTTPClient(
username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD,
auth_url=AUTH_URL, region_name=REGION, endpoint_type='adminURL')
self.client._extract_service_catalog(resources)
self.assertEqual(self.client.endpoint_url, 'admin')
# Test public url
self.client = client.HTTPClient(
username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD,
auth_url=AUTH_URL, region_name=REGION, endpoint_type='publicURL')
self.client._extract_service_catalog(resources)
self.assertEqual(self.client.endpoint_url, 'public')
# Test internal url
self.client = client.HTTPClient(
username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD,
auth_url=AUTH_URL, region_name=REGION, endpoint_type='internalURL')
self.client._extract_service_catalog(resources)
self.assertEqual(self.client.endpoint_url, 'internal')
# Test url that isn't found in the service catalog
self.client = client.HTTPClient(
username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD,
auth_url=AUTH_URL, region_name=REGION, endpoint_type='privateURL')
self.assertRaises(k_exceptions.EndpointNotFound,
self.client._extract_service_catalog,
resources)
@mock.patch('apmecclient.client.HTTPClient.request')
@mock.patch('apmecclient.common.utils.http_log_req')
def test_strip_credentials_from_log(self, mock_http_log_req,
mock_request,):
body = ('{"auth": {"tenantId": "testtenant_id",'
'"passwordCredentials": {"password": "password",'
'"userId": "testuser_id"}}}')
expected_body = ('{"auth": {"tenantId": "testtenant_id",'
'"REDACTEDCredentials": {"REDACTED": "REDACTED",'
'"userId": "testuser_id"}}}')
_headers = {'headers': expected_headers, 'body': expected_body}
mock_request.return_value = (resp_200, json.dumps(KS_TOKEN_RESULT))
self.client.do_request('/resource', 'GET', body=body)
args, kwargs = mock_http_log_req.call_args
# Check that credentials are stripped while logging.
self.assertEqual(_headers, args[2])
class CLITestAuthKeystoneWithId(CLITestAuthKeystone):
# Auth Body expected
auth_body = ('{"auth": {"passwordCredentials": '
'{"password": "password", "userId": "testuser_id"}, '
'"tenantId": "testtenant_id"}}')
def setUp(self):
"""Prepare the test environment."""
super(CLITestAuthKeystoneWithId, self).setUp()
self.client = client.HTTPClient(user_id=USER_ID,
tenant_id=TENANT_ID,
password=PASSWORD,
auth_url=AUTH_URL,
region_name=REGION)
class CLITestAuthKeystoneWithIdandName(CLITestAuthKeystone):
# Auth Body expected
auth_body = ('{"auth": {"passwordCredentials": '
'{"password": "password", "userId": "testuser_id"}, '
'"tenantId": "testtenant_id"}}')
def setUp(self):
"""Prepare the test environment."""
super(CLITestAuthKeystoneWithIdandName, self).setUp()
self.client = client.HTTPClient(username=USERNAME,
user_id=USER_ID,
tenant_id=TENANT_ID,
tenant_name=TENANT_NAME,
password=PASSWORD,
auth_url=AUTH_URL,
region_name=REGION)

View File

@ -0,0 +1,119 @@
# Copyright 2012 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import testtools
from apmecclient.common import exceptions
from apmecclient.apmec import v1_0 as apmecV10
class CLITestArgs(testtools.TestCase):
def test_empty(self):
_mydict = apmecV10.parse_args_to_dict([])
self.assertEqual({}, _mydict)
def test_default_bool(self):
_specs = ['--my_bool', '--arg1', 'value1']
_mydict = apmecV10.parse_args_to_dict(_specs)
self.assertTrue(_mydict['my_bool'])
def test_bool_true(self):
_specs = ['--my-bool', 'type=bool', 'true', '--arg1', 'value1']
_mydict = apmecV10.parse_args_to_dict(_specs)
self.assertTrue(_mydict['my_bool'])
def test_bool_false(self):
_specs = ['--my_bool', 'type=bool', 'false', '--arg1', 'value1']
_mydict = apmecV10.parse_args_to_dict(_specs)
self.assertFalse(_mydict['my_bool'])
def test_nargs(self):
_specs = ['--tag', 'x', 'y', '--arg1', 'value1']
_mydict = apmecV10.parse_args_to_dict(_specs)
self.assertIn('x', _mydict['tag'])
self.assertIn('y', _mydict['tag'])
def test_badarg(self):
_specs = ['--tag=t', 'x', 'y', '--arg1', 'value1']
self.assertRaises(exceptions.CommandError,
apmecV10.parse_args_to_dict, _specs)
def test_badarg_with_minus(self):
_specs = ['--arg1', 'value1', '-D']
self.assertRaises(exceptions.CommandError,
apmecV10.parse_args_to_dict, _specs)
def test_goodarg_with_minus_number(self):
_specs = ['--arg1', 'value1', '-1', '-1.0']
_mydict = apmecV10.parse_args_to_dict(_specs)
self.assertEqual(['value1', '-1', '-1.0'],
_mydict['arg1'])
def test_badarg_duplicate(self):
_specs = ['--tag=t', '--arg1', 'value1', '--arg1', 'value1']
self.assertRaises(exceptions.CommandError,
apmecV10.parse_args_to_dict, _specs)
def test_badarg_early_type_specification(self):
_specs = ['type=dict', 'key=value']
self.assertRaises(exceptions.CommandError,
apmecV10.parse_args_to_dict, _specs)
def test_arg(self):
_specs = ['--tag=t', '--arg1', 'value1']
self.assertEqual('value1',
apmecV10.parse_args_to_dict(_specs)['arg1'])
def test_dict_arg(self):
_specs = ['--tag=t', '--arg1', 'type=dict', 'key1=value1,key2=value2']
arg1 = apmecV10.parse_args_to_dict(_specs)['arg1']
self.assertEqual('value1', arg1['key1'])
self.assertEqual('value2', arg1['key2'])
def test_dict_arg_with_attribute_named_type(self):
_specs = ['--tag=t', '--arg1', 'type=dict', 'type=value1,key2=value2']
arg1 = apmecV10.parse_args_to_dict(_specs)['arg1']
self.assertEqual('value1', arg1['type'])
self.assertEqual('value2', arg1['key2'])
def test_list_of_dict_arg(self):
_specs = ['--tag=t', '--arg1', 'type=dict',
'list=true', 'key1=value1,key2=value2']
arg1 = apmecV10.parse_args_to_dict(_specs)['arg1']
self.assertEqual('value1', arg1[0]['key1'])
self.assertEqual('value2', arg1[0]['key2'])
def test_clear_action(self):
_specs = ['--anyarg', 'action=clear']
args = apmecV10.parse_args_to_dict(_specs)
self.assertIsNone(args['anyarg'])
def test_bad_values_str(self):
_specs = ['--strarg', 'type=str']
self.assertRaises(exceptions.CommandError,
apmecV10.parse_args_to_dict, _specs)
def test_bad_values_list(self):
_specs = ['--listarg', 'list=true', 'type=str']
self.assertRaises(exceptions.CommandError,
apmecV10.parse_args_to_dict, _specs)
_specs = ['--listarg', 'type=list']
self.assertRaises(exceptions.CommandError,
apmecV10.parse_args_to_dict, _specs)
_specs = ['--listarg', 'type=list', 'action=clear']
self.assertRaises(exceptions.CommandError,
apmecV10.parse_args_to_dict, _specs)

View File

@ -0,0 +1,791 @@
# Copyright 2012 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import urllib
import contextlib
import fixtures
import mock
import six
import six.moves.urllib.parse as urlparse
import sys
import testtools
from apmecclient.common import constants
from apmecclient.common import exceptions
from apmecclient import shell
from apmecclient.apmec import v1_0 as apmecV1_0
from apmecclient.apmec.v1_0 import ApmecCommand
from apmecclient.tests.unit import test_utils
from apmecclient.v1_0 import client
API_VERSION = "1.0"
FORMAT = 'json'
TOKEN = 'testtoken'
ENDURL = 'localurl'
@contextlib.contextmanager
def capture_std_streams():
fake_stdout, fake_stderr = six.StringIO(), six.StringIO()
stdout, stderr = sys.stdout, sys.stderr
try:
sys.stdout, sys.stderr = fake_stdout, fake_stderr
yield fake_stdout, fake_stderr
finally:
sys.stdout, sys.stderr = stdout, stderr
class FakeStdout(object):
def __init__(self):
self.content = []
def write(self, text):
self.content.append(text)
def make_string(self):
result = ''
for line in self.content:
result = result + line
return result
class MyResp(object):
def __init__(self, status_code, headers=None, reason=None):
self.status_code = status_code
self.headers = headers or {}
self.reason = reason
class MyApp(object):
def __init__(self, _stdout):
self.stdout = _stdout
def end_url(path, query=None, format=FORMAT):
_url_str = ENDURL + "/v" + API_VERSION + path + "." + format
return query and _url_str + "?" + query or _url_str
class MyUrlComparator(object):
def __init__(self, lhs, client):
self.lhs = lhs
self.client = client
def equals(self, rhs):
lhsp = urlparse.urlparse(self.lhs)
rhsp = urlparse.urlparse(rhs)
lhs_qs = urlparse.parse_qsl(lhsp.query)
rhs_qs = urlparse.parse_qsl(rhsp.query)
return (lhsp.scheme == rhsp.scheme and
lhsp.netloc == rhsp.netloc and
lhsp.path == rhsp.path and
len(lhs_qs) == len(rhs_qs) and
set(lhs_qs) == set(rhs_qs))
def __str__(self):
if self.client and self.client.format != FORMAT:
lhs_parts = self.lhs.split("?", 1)
if len(lhs_parts) == 2:
lhs = ("%s.%s?%s" % (lhs_parts[0][:-4],
self.client.format,
lhs_parts[1]))
else:
lhs = ("%s.%s" % (lhs_parts[0][:-4],
self.client.format))
return lhs
return self.lhs
def __repr__(self):
return str(self)
def __eq__(self, rhs):
return self.equals(rhs)
def __ne__(self, rhs):
return not self.__eq__(rhs)
class MyComparator(object):
def __init__(self, lhs, client):
self.lhs = lhs
self.client = client
def _com_dict(self, lhs, rhs):
if len(lhs) != len(rhs):
return False
for key, value in lhs.items():
if key not in rhs:
return False
rhs_value = rhs[key]
if not self._com(value, rhs_value):
return False
return True
def _com_list(self, lhs, rhs):
if len(lhs) != len(rhs):
return False
for lhs_value in lhs:
if lhs_value not in rhs:
return False
return True
def _com(self, lhs, rhs):
if lhs is None:
return rhs is None
if isinstance(lhs, dict):
if not isinstance(rhs, dict):
return False
return self._com_dict(lhs, rhs)
if isinstance(lhs, list):
if not isinstance(rhs, list):
return False
return self._com_list(lhs, rhs)
if isinstance(lhs, tuple):
if not isinstance(rhs, tuple):
return False
return self._com_list(lhs, rhs)
return lhs == rhs
def equals(self, rhs):
if self.client:
rhs = self.client.deserialize(rhs, 200)
return self._com(self.lhs, rhs)
def __repr__(self):
if self.client:
return self.client.serialize(self.lhs)
return str(self.lhs)
def __eq__(self, rhs):
return self.equals(rhs)
def __ne__(self, rhs):
return not self.__eq__(rhs)
class CLITestV10Base(testtools.TestCase):
format = 'json'
test_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
id_field = 'id'
def _find_resourceid(self, client, resource, name_or_id):
return name_or_id
def setUp(self, plurals={}):
"""Prepare the test environment."""
super(CLITestV10Base, self).setUp()
client.Client.EXTED_PLURALS.update(constants.PLURALS)
client.Client.EXTED_PLURALS.update(plurals)
self.metadata = {'plurals': client.Client.EXTED_PLURALS,
'xmlns': constants.XML_NS_V10,
constants.EXT_NS: {'prefix':
'http://xxxx.yy.com'}}
self.endurl = ENDURL
self.fake_stdout = FakeStdout()
self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.fake_stdout))
self.useFixture(fixtures.MonkeyPatch(
'apmecclient.apmec.v1_0.find_resourceid_by_name_or_id',
self._find_resourceid))
self.useFixture(fixtures.MonkeyPatch(
'apmecclient.apmec.v1_0.find_resourceid_by_id',
self._find_resourceid))
self.client = client.Client(token=TOKEN, endpoint_url=self.endurl)
@mock.patch.object(ApmecCommand, 'get_client')
def _test_create_resource(self, resource, cmd,
name, myid, args,
position_names, position_values, mock_get,
tenant_id=None, get_client_called_count=1,
tags=None, admin_state_up=True, extra_body=None,
**kwargs):
mock_get.return_value = self.client
non_admin_status_resources = ['mead', 'mea', 'vim']
if (resource in non_admin_status_resources):
body = {resource: {}, }
else:
body = {resource: {'admin_state_up': admin_state_up, }, }
if tenant_id:
body[resource].update({'tenant_id': tenant_id})
if tags:
body[resource].update({'tags': tags})
if extra_body:
body[resource].update(extra_body)
body[resource].update(kwargs)
for i in range(len(position_names)):
body[resource].update({position_names[i]: position_values[i]})
ress = {resource:
{self.id_field: myid}, }
if name:
ress[resource].update({'name': name})
self.client.format = self.format
resstr = self.client.serialize(ress)
# url method body
resource_plural = apmecV1_0._get_resource_plural(resource,
self.client)
path = getattr(self.client, resource_plural + "_path")
# Work around for LP #1217791. XML deserializer called from
# MyComparator does not decodes XML string correctly.
if self.format == 'json':
_body = MyComparator(body, self.client)
else:
_body = self.client.serialize(body)
with mock.patch.object(self.client.httpclient, 'request') as mock_req:
mock_req.return_value = (MyResp(200), resstr)
args.extend(['--request-format', self.format])
cmd_parser = cmd.get_parser('create_' + resource)
shell.run_command(cmd, cmd_parser, args)
mock_req.assert_called_once_with(
end_url(path, format=self.format), 'POST',
body=_body,
headers=test_utils.ContainsKeyValue('X-Auth-Token', TOKEN))
self.assertEqual(get_client_called_count, mock_get.call_count)
_str = self.fake_stdout.make_string()
self.assertIn(myid, _str)
if name:
self.assertIn(name, _str)
@mock.patch.object(ApmecCommand, 'get_client')
def _test_list_columns(self, cmd, resources_collection,
resources_out, mock_get,
args=['-f', 'json']):
mock_get.return_value = self.client
self.client.format = self.format
resstr = self.client.serialize(resources_out)
path = getattr(self.client, resources_collection + "_path")
with mock.patch.object(self.client.httpclient, 'request') as mock_req:
mock_req.return_value = (MyResp(200), resstr)
args.extend(['--request-format', self.format])
cmd_parser = cmd.get_parser("list_" + resources_collection)
shell.run_command(cmd, cmd_parser, args)
mock_req.assert_called_once_with(
end_url(path, format=self.format), 'GET',
body=None,
headers=test_utils.ContainsKeyValue('X-Auth-Token', TOKEN))
mock_get.assert_called_once_with()
def _test_list_resources(self, resources, cmd, detail=False, tags=[],
fields_1=[], fields_2=[], page_size=None,
sort_key=[], sort_dir=[], response_contents=None,
base_args=None, path=None,
template_source=None):
if response_contents is None:
contents = [{self.id_field: 'myid1', },
{self.id_field: 'myid2', }, ]
else:
contents = response_contents
reses = {resources: contents}
self.client.format = self.format
resstr = self.client.serialize(reses)
# url method body
query = ""
args = base_args if base_args is not None else []
if detail:
args.append('-D')
args.extend(['--request-format', self.format])
if fields_1:
for field in fields_1:
args.append('--fields')
args.append(field)
if template_source is not None:
args.append("--template-source")
args.append(template_source)
query += 'template_source=' + template_source
if tags:
args.append('--')
args.append("--tag")
for tag in tags:
args.append(tag)
if isinstance(tag, six.string_types):
tag = urllib.quote(tag.encode('utf-8'))
if query:
query += "&tag=" + tag
else:
query = "tag=" + tag
if (not tags) and fields_2:
args.append('--')
if fields_2:
args.append("--fields")
for field in fields_2:
args.append(field)
if detail:
query = query and query + '&verbose=True' or 'verbose=True'
fields_1.extend(fields_2)
for field in fields_1:
if query:
query += "&fields=" + field
else:
query = "fields=" + field
if page_size:
args.append("--page-size")
args.append(str(page_size))
if query:
query += "&limit=%s" % page_size
else:
query = "limit=%s" % page_size
if sort_key:
for key in sort_key:
args.append('--sort-key')
args.append(key)
if query:
query += '&'
query += 'sort_key=%s' % key
if sort_dir:
len_diff = len(sort_key) - len(sort_dir)
if len_diff > 0:
sort_dir += ['asc'] * len_diff
elif len_diff < 0:
sort_dir = sort_dir[:len(sort_key)]
for dir in sort_dir:
args.append('--sort-dir')
args.append(dir)
if query:
query += '&'
query += 'sort_dir=%s' % dir
if path is None:
path = getattr(self.client, resources + "_path")
with mock.patch.object(self.client.httpclient, 'request') as mock_req:
mock_req.return_value = (MyResp(200), resstr)
with mock.patch.object(ApmecCommand, 'get_client') as mock_get:
mock_get.return_value = self.client
cmd_parser = cmd.get_parser("list_" + resources)
shell.run_command(cmd, cmd_parser, args)
mock_req.assert_called_once_with(
MyUrlComparator(end_url(path, query, format=self.format),
self.client),
'GET',
body=None,
headers=test_utils.ContainsKeyValue('X-Auth-Token', TOKEN))
_str = self.fake_stdout.make_string()
if response_contents is None:
self.assertIn('myid1', _str)
return _str
@mock.patch.object(ApmecCommand, 'get_client')
def _test_list_sub_resources(self, resources, api_resource, cmd, myid,
mock_get, detail=False,
tags=[], fields_1=[], fields_2=[],
page_size=None, sort_key=[], sort_dir=[],
response_contents=None, base_args=None,
path=None):
mock_get.return_value = self.client
if response_contents is None:
contents = [{self.id_field: 'myid1', },
{self.id_field: 'myid2', }, ]
else:
contents = response_contents
reses = {api_resource: contents}
self.client.format = self.format
resstr = self.client.serialize(reses)
# url method body
query = ""
args = base_args if base_args is not None else []
if detail:
args.append('-D')
args.extend(['--request-format', self.format])
if fields_1:
for field in fields_1:
args.append('--fields')
args.append(field)
if tags:
args.append('--')
args.append("--tag")
for tag in tags:
args.append(tag)
if isinstance(tag, six.string_types):
tag = urllib.quote(tag.encode('utf-8'))
if query:
query += "&tag=" + tag
else:
query = "tag=" + tag
if (not tags) and fields_2:
args.append('--')
if fields_2:
args.append("--fields")
for field in fields_2:
args.append(field)
if detail:
query = query and query + '&verbose=True' or 'verbose=True'
fields_1.extend(fields_2)
for field in fields_1:
if query:
query += "&fields=" + field
else:
query = "fields=" + field
if page_size:
args.append("--page-size")
args.append(str(page_size))
if query:
query += "&limit=%s" % page_size
else:
query = "limit=%s" % page_size
if sort_key:
for key in sort_key:
args.append('--sort-key')
args.append(key)
if query:
query += '&'
query += 'sort_key=%s' % key
if sort_dir:
len_diff = len(sort_key) - len(sort_dir)
if len_diff > 0:
sort_dir += ['asc'] * len_diff
elif len_diff < 0:
sort_dir = sort_dir[:len(sort_key)]
for dir in sort_dir:
args.append('--sort-dir')
args.append(dir)
if query:
query += '&'
query += 'sort_dir=%s' % dir
if path is None:
path = getattr(self.client, resources + "_path")
with mock.patch.object(self.client.httpclient, 'request') as mock_req:
mock_req.return_value = (MyResp(200), resstr)
comparator = MyUrlComparator(
end_url(path % myid, query=query, format=self.format),
self.client)
args.extend(['--request-format', self.format])
cmd_parser = cmd.get_parser("list_" + resources)
shell.run_command(cmd, cmd_parser, args)
mock_req.assert_called_once_with(
comparator, 'GET',
body=None,
headers=test_utils.ContainsKeyValue('X-Auth-Token', TOKEN))
_str = self.fake_stdout.make_string()
if response_contents is None:
self.assertIn('myid1', _str)
return _str
# TODO(gongysh) add pagination unit test BUG 1633255
# def _test_list_sub_resources_with_pagination(
# self, resources, api_resource, cmd, myid):
# self.mox.StubOutWithMock(cmd, "get_client")
# self.mox.StubOutWithMock(self.client.httpclient, "request")
# cmd.get_client().MultipleTimes().AndReturn(self.client)
# path = getattr(self.client, resources + "_path")
# fake_query = "marker=myid2&limit=2"
# reses1 = {api_resource: [{'id': 'myid1', },
# {'id': 'myid2', }],
# '%s_links' % api_resource: [
# {'href': end_url(path % myid, fake_query),
# 'rel': 'next'}]
# }
# reses2 = {api_resource: [{'id': 'myid3', },
# {'id': 'myid4', }]}
# self.client.format = self.format
# resstr1 = self.client.serialize(reses1)
# resstr2 = self.client.serialize(reses2)
# self.client.httpclient.request(
# end_url(path % myid, "", format=self.format), 'GET',
# body=None,
# headers=mox.ContainsKeyValue(
# 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr1))
# self.client.httpclient.request(
# MyUrlComparator(end_url(path % myid, fake_query,
# format=self.format), self.client), 'GET',
# body=None, headers=mox.ContainsKeyValue(
# 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr2))
# self.mox.ReplayAll()
# cmd_parser = cmd.get_parser("list_" + resources)
# args = [myid, '--request-format', self.format]
# shell.run_command(cmd, cmd_parser, args)
# self.mox.VerifyAll()
# self.mox.UnsetStubs()
# def _test_list_resources_with_pagination(self, resources, cmd):
# self.mox.StubOutWithMock(cmd, "get_client")
# self.mox.StubOutWithMock(self.client.httpclient, "request")
# cmd.get_client().MultipleTimes().AndReturn(self.client)
# path = getattr(self.client, resources + "_path")
# fake_query = "marker=myid2&limit=2"
# reses1 = {resources: [{'id': 'myid1', },
# {'id': 'myid2', }],
# '%s_links' % resources: [
# {'href': end_url(path, fake_query),
# 'rel': 'next'}]}
# reses2 = {resources: [{'id': 'myid3', },
# {'id': 'myid4', }]}
# self.client.format = self.format
# resstr1 = self.client.serialize(reses1)
# resstr2 = self.client.serialize(reses2)
# self.client.httpclient.request(
# end_url(path, "", format=self.format), 'GET',
# body=None,
# headers=mox.ContainsKeyValue(
# 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr1))
# self.client.httpclient.request(
# MyUrlComparator(end_url(path, fake_query, format=self.format),
# self.client), 'GET', body=None,
# headers=mox.ContainsKeyValue(
# 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr2))
# self.mox.ReplayAll()
# cmd_parser = cmd.get_parser("list_" + resources)
# args = ['--request-format', self.format]
# shell.run_command(cmd, cmd_parser, args)
# self.mox.VerifyAll()
# self.mox.UnsetStubs()
@mock.patch.object(ApmecCommand, 'get_client')
def _test_update_resource(self, resource, cmd, myid, args, extrafields,
mock_get, get_client_called_count=1):
mock_get.return_value = self.client
body = {resource: extrafields}
path = getattr(self.client, resource + "_path")
self.client.format = self.format
# Work around for LP #1217791. XML deserializer called from
# MyComparator does not decodes XML string correctly.
if self.format == 'json':
_body = MyComparator(body, self.client)
else:
_body = self.client.serialize(body)
with mock.patch.object(self.client.httpclient, 'request') as mock_req:
comparator = MyUrlComparator(
end_url(path % myid, format=self.format), self.client)
mock_req.return_value = (MyResp(204), None)
args.extend(['--request-format', self.format])
cmd_parser = cmd.get_parser("update_" + resource)
shell.run_command(cmd, cmd_parser, args)
mock_req.assert_called_once_with(
comparator,
'PUT',
body=_body,
headers=test_utils.ContainsKeyValue('X-Auth-Token', TOKEN))
self.assertEqual(get_client_called_count, mock_get.call_count)
_str = self.fake_stdout.make_string()
self.assertIn(myid, _str)
def _test_show_resource(self, resource, cmd, myid, args, fields=[]):
with mock.patch.object(cmd, 'get_client') as mock_get:
mock_get.return_value = self.client
query = "&".join(["fields=%s" % field for field in fields])
expected_res = {resource:
{self.id_field: myid,
'name': 'myname', }, }
self.client.format = self.format
resstr = self.client.serialize(expected_res)
path = getattr(self.client, resource + "_path")
with mock.patch.object(self.client.httpclient, 'request') as\
mock_req:
mock_req.return_value = (MyResp(200), resstr)
args.extend(['--request-format', self.format])
cmd_parser = cmd.get_parser("show_" + resource)
shell.run_command(cmd, cmd_parser, args)
mock_req.assert_called_once_with(
end_url(path % myid, query, format=self.format), 'GET',
body=None,
headers=test_utils.ContainsKeyValue('X-Auth-Token', TOKEN))
_str = self.fake_stdout.make_string()
mock_get.assert_called_once_with()
self.assertIn(myid, _str)
self.assertIn('myname', _str)
@mock.patch.object(ApmecCommand, 'get_client')
def _test_delete_resource(self, resource, cmd, myid, args, mock_get):
deleted_msg = {'mea': 'delete initiated'}
mock_get.return_value = self.client
path = getattr(self.client, resource + "_path")
with mock.patch.object(self.client.httpclient, 'request') as mock_req:
mock_req.return_value = (MyResp(204), None)
args.extend(['--request-format', self.format])
cmd_parser = cmd.get_parser("delete_" + resource)
shell.run_command(cmd, cmd_parser, args)
mock_req.assert_called_once_with(
end_url(path % myid, format=self.format), 'DELETE',
body=None,
headers=test_utils.ContainsKeyValue('X-Auth-Token', TOKEN))
mock_get.assert_called_once_with()
_str = self.fake_stdout.make_string()
msg = 'All specified %(resource)s(s) %(msg)s successfully\n' % {
'msg': deleted_msg.get(resource, 'deleted'),
'resource': resource}
self.assertEqual(msg, _str)
@mock.patch.object(ApmecCommand, 'get_client')
def _test_update_resource_action(self, resource, cmd, myid, action, args,
body, mock_get, retval=None):
mock_get.return_value = self.client
path = getattr(self.client, resource + "_path")
path_action = '%s/%s' % (myid, action)
with mock.patch.object(self.client.httpclient, 'request') as mock_req:
mock_req.return_value = (MyResp(204), retval)
args.extend(['--request-format', self.format])
cmd_parser = cmd.get_parser("delete_" + resource)
shell.run_command(cmd, cmd_parser, args)
mock_req.assert_called_once_with(
end_url(path % path_action, format=self.format), 'PUT',
body=MyComparator(body, self.client),
headers=test_utils.ContainsKeyValue('X-Auth-Token', TOKEN))
_str = self.fake_stdout.make_string()
self.assertIn(myid, _str)
class ClientV1TestJson(CLITestV10Base):
def test_do_request_unicode(self):
self.client.format = self.format
unicode_text = u'\u7f51\u7edc'
action = u'/test'
params = {'test': unicode_text}
body = params
expect_body = self.client.serialize(body)
self.client.httpclient.auth_token = unicode_text
with mock.patch.object(self.client.httpclient, 'request') as mock_req:
mock_req.return_value = (MyResp(200), expect_body)
res_body = self.client.do_request('PUT', action, body=body,
params=params)
expected_uri = u'localurl/v1.0/test.json?test=%E7%BD%91%E7%BB%9C'
mock_req.assert_called_with(
expected_uri, 'PUT', body=expect_body,
headers={'X-Auth-Token': unicode_text,
'User-Agent': 'python-apmecclient'})
# test response with unicode
self.assertEqual(res_body, body)
def test_do_request_error_without_response_body(self):
self.client.format = self.format
params = {'test': 'value'}
expect_query = urlparse.urlencode(params)
self.client.httpclient.auth_token = 'token'
with mock.patch.object(self.client.httpclient, 'request') as mock_req:
mock_req.return_value = (MyResp(400, reason='An error'), '')
self.client.httpclient.request(
end_url('/test', query=expect_query, format=self.format),
'PUT', body='',
headers={'X-Auth-Token': 'token'}
)
error = self.assertRaises(exceptions.ApmecClientException,
self.client.do_request, 'PUT', '/test',
body='', params=params)
self.assertEqual("An error", str(error))
class CLITestV10ExceptionHandler(CLITestV10Base):
def _test_exception_handler_v10(
self, expected_exception, status_code, expected_msg,
error_type=None, error_msg=None, error_detail=None,
error_content=None):
if error_content is None:
error_content = {'ApmecError': {'type': error_type,
'message': error_msg,
'detail': error_detail}}
e = self.assertRaises(expected_exception,
client.exception_handler_v10,
status_code, error_content)
self.assertEqual(status_code, e.status_code)
self.assertEqual(expected_exception.__name__,
e.__class__.__name__)
if expected_msg is None:
if error_detail:
expected_msg = '\n'.join([error_msg, error_detail])
else:
expected_msg = error_msg
self.assertEqual(expected_msg, e.message)
def test_exception_handler_v10_ip_address_in_use(self):
err_msg = ('Unable to complete operation for network '
'fake-network-uuid. The IP address fake-ip is in use.')
self._test_exception_handler_v10(
exceptions.IpAddressInUseClient, 409, err_msg,
'IpAddressInUse', err_msg, '')
def test_exception_handler_v10_apmec_known_error(self):
known_error_map = [
('NetworkNotFound', exceptions.NetworkNotFoundClient, 404),
('PortNotFound', exceptions.PortNotFoundClient, 404),
('NetworkInUse', exceptions.NetworkInUseClient, 409),
('PortInUse', exceptions.PortInUseClient, 409),
('StateInvalid', exceptions.StateInvalidClient, 400),
('IpAddressInUse', exceptions.IpAddressInUseClient, 409),
('IpAddressGenerationFailure',
exceptions.IpAddressGenerationFailureClient, 409),
('ExternalIpAddressExhausted',
exceptions.ExternalIpAddressExhaustedClient, 400),
('OverQuota', exceptions.OverQuotaClient, 409),
]
error_msg = 'dummy exception message'
error_detail = 'sample detail'
for server_exc, client_exc, status_code in known_error_map:
self._test_exception_handler_v10(
client_exc, status_code,
error_msg + '\n' + error_detail,
server_exc, error_msg, error_detail)
def test_exception_handler_v10_apmec_known_error_without_detail(self):
error_msg = 'Network not found'
error_detail = ''
self._test_exception_handler_v10(
exceptions.NetworkNotFoundClient, 404,
error_msg,
'NetworkNotFound', error_msg, error_detail)
def test_exception_handler_v10_unknown_error_to_per_code_exception(self):
for status_code, client_exc in exceptions.HTTP_EXCEPTION_MAP.items():
error_msg = 'Unknown error'
error_detail = 'This is detail'
self._test_exception_handler_v10(
client_exc, status_code,
error_msg + '\n' + error_detail,
'UnknownError', error_msg, error_detail)
def test_exception_handler_v10_apmec_unknown_status_code(self):
error_msg = 'Unknown error'
error_detail = 'This is detail'
self._test_exception_handler_v10(
exceptions.ApmecClientException, 501,
error_msg + '\n' + error_detail,
'UnknownError', error_msg, error_detail)
def test_exception_handler_v10_bad_apmec_error(self):
error_content = {'ApmecError': {'unknown_key': 'UNKNOWN'}}
self._test_exception_handler_v10(
exceptions.ApmecClientException, 500,
expected_msg={'unknown_key': 'UNKNOWN'},
error_content=error_content)
def test_exception_handler_v10_error_dict_contains_message(self):
error_content = {'message': 'This is an error message'}
self._test_exception_handler_v10(
exceptions.ApmecClientException, 500,
expected_msg='This is an error message',
error_content=error_content)
def test_exception_handler_v10_error_dict_not_contain_message(self):
error_content = {'error': 'This is an error message'}
expected_msg = '%s-%s' % (500, error_content)
self._test_exception_handler_v10(
exceptions.ApmecClientException, 500,
expected_msg=expected_msg,
error_content=error_content)
def test_exception_handler_v10_default_fallback(self):
error_content = 'This is an error message'
expected_msg = '%s-%s' % (500, error_content)
self._test_exception_handler_v10(
exceptions.ApmecClientException, 500,
expected_msg=expected_msg,
error_content=error_content)

View File

@ -0,0 +1,47 @@
# Copyright 2013 NEC Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import sys
from apmecclient.apmec.v1_0.extension import ListExt
from apmecclient.apmec.v1_0.extension import ShowExt
from apmecclient.tests.unit.test_cli10 import CLITestV10Base
from apmecclient.tests.unit.test_cli10 import MyApp
class CLITestV10Extension(CLITestV10Base):
id_field = 'alias'
def test_list_extensions(self):
resources = 'extensions'
cmd = ListExt(MyApp(sys.stdout), None)
contents = [{'alias': 'ext1', 'name': 'name1', 'other': 'other1'},
{'alias': 'ext2', 'name': 'name2', 'other': 'other2'}]
ret = self._test_list_resources(resources, cmd,
response_contents=contents)
ret_words = set(ret.split())
# Check only the default columns are shown.
self.assertIn('name', ret_words)
self.assertIn('alias', ret_words)
self.assertNotIn('other', ret_words)
def test_show_extension(self):
# -F option does not work for ext-show at the moment, so -F option
# is not passed in the commandline args as other tests do.
resource = 'extension'
cmd = ShowExt(MyApp(sys.stdout), None)
args = [self.test_id]
ext_alias = self.test_id
self._test_show_resource(resource, cmd, ext_alias, args, fields=[])

View File

@ -0,0 +1,39 @@
# Copyright 2013 Intel Corporation
# All Rights Reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import testtools
from testtools import helpers
from apmecclient.apmec import v1_0 as apmecV10
class TestCommandMeta(testtools.TestCase):
def test_apmec_command_meta_defines_log(self):
class FakeCommand(apmecV10.ApmecCommand):
pass
self.assertTrue(helpers.safe_hasattr(FakeCommand, 'log'))
self.assertIsInstance(FakeCommand.log, logging.getLoggerClass())
self.assertEqual(FakeCommand.log.name, __name__ + ".FakeCommand")
def test_apmec_command_log_defined_explicitly(self):
class FakeCommand(apmecV10.ApmecCommand):
log = None
self.assertTrue(helpers.safe_hasattr(FakeCommand, 'log'))
self.assertIsNone(FakeCommand.log)

View File

@ -0,0 +1,71 @@
# Copyright (C) 2013 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
import testtools
from apmecclient.client import HTTPClient
from apmecclient.common import exceptions
from apmecclient.tests.unit.test_cli10 import MyResp
AUTH_TOKEN = 'test_token'
END_URL = 'test_url'
METHOD = 'GET'
URL = 'http://test.test:1234/v1.0/test'
headers = {'User-Agent': 'python-apmecclient'}
class TestHTTPClient(testtools.TestCase):
def setUp(self):
super(TestHTTPClient, self).setUp()
self.addCleanup(mock.patch.stopall)
self.http = HTTPClient(token=AUTH_TOKEN, endpoint_url=END_URL)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_request_error(self, mock_request):
mock_request.side_effect = Exception('error msg')
self.assertRaises(
exceptions.ConnectionFailed,
self.http._cs_request,
URL, METHOD
)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_request_success(self, mock_request):
rv_should_be = MyResp(200), 'test content'
mock_request.return_value = rv_should_be
self.assertEqual(rv_should_be, self.http._cs_request(URL, METHOD))
@mock.patch('apmecclient.client.HTTPClient.request')
def test_request_unauthorized(self, mock_request):
mock_request.return_value = MyResp(401), 'unauthorized message'
e = self.assertRaises(exceptions.Unauthorized,
self.http._cs_request, URL, METHOD)
self.assertEqual('unauthorized message', str(e))
mock_request.assert_called_with(URL, METHOD, headers=headers)
@mock.patch('apmecclient.client.HTTPClient.request')
def test_request_forbidden_is_returned_to_caller(self, mock_request):
rv_should_be = MyResp(403), 'forbidden message'
mock_request.return_value = rv_should_be
self.assertEqual(rv_should_be, self.http._cs_request(URL, METHOD))

View File

@ -0,0 +1,190 @@
# Copyright (C) 2013 Yahoo! Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import argparse
import logging
import os
import re
import six
import sys
import fixtures
from keystoneclient import session
import mock
import testtools
from testtools import matchers
from apmecclient.common import clientmanager
from apmecclient import shell as openstack_shell
DEFAULT_USERNAME = 'username'
DEFAULT_PASSWORD = 'password'
DEFAULT_TENANT_ID = 'tenant_id'
DEFAULT_TENANT_NAME = 'tenant_name'
DEFAULT_AUTH_URL = 'http://127.0.0.1:5000/v1.0/'
DEFAULT_TOKEN = '3bcc3d3a03f44e3d8377f9247b0ad155'
DEFAULT_URL = 'http://apmec.example.org:9896/'
DEFAULT_API_VERSION = '1.0'
class ShellTest(testtools.TestCase):
FAKE_ENV = {
'OS_USERNAME': DEFAULT_USERNAME,
'OS_PASSWORD': DEFAULT_PASSWORD,
'OS_TENANT_ID': DEFAULT_TENANT_ID,
'OS_TENANT_NAME': DEFAULT_TENANT_NAME,
'OS_AUTH_URL': DEFAULT_AUTH_URL}
# Patch os.environ to avoid required auth info.
def setUp(self):
super(ShellTest, self).setUp()
for var in self.FAKE_ENV:
self.useFixture(
fixtures.EnvironmentVariable(
var, self.FAKE_ENV[var]))
def shell(self, argstr, check=False):
orig = (sys.stdout, sys.stderr)
clean_env = {}
_old_env, os.environ = os.environ, clean_env.copy()
try:
sys.stdout = six.StringIO()
sys.stderr = six.StringIO()
_shell = openstack_shell.ApmecShell(DEFAULT_API_VERSION)
_shell.run(argstr.split())
except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info()
self.assertEqual(exc_value.code, 0)
finally:
stdout = sys.stdout.getvalue()
stderr = sys.stderr.getvalue()
sys.stdout.close()
sys.stderr.close()
sys.stdout, sys.stderr = orig
os.environ = _old_env
return stdout, stderr
def test_run_unknown_command(self):
self.useFixture(fixtures.FakeLogger(level=logging.DEBUG))
stdout, stderr = self.shell('fake', check=True)
self.assertFalse(stdout)
self.assertEqual("Unknown command ['fake']", stderr.strip())
def test_help(self):
required = 'usage:'
help_text, stderr = self.shell('help')
self.assertThat(
help_text,
matchers.MatchesRegex(required))
self.assertFalse(stderr)
def test_help_on_subcommand(self):
required = [
'.*?^usage: .* mead-list']
stdout, stderr = self.shell('help mead-list')
for r in required:
self.assertThat(
stdout,
matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE))
self.assertFalse(stderr)
def test_help_command(self):
required = 'usage:'
help_text, stderr = self.shell('help mead-create')
self.assertThat(
help_text,
matchers.MatchesRegex(required))
self.assertFalse(stderr)
def test_unknown_auth_strategy(self):
self.useFixture(fixtures.FakeLogger(level=logging.DEBUG))
stdout, stderr = self.shell('--os-auth-strategy fake '
'mead-list')
self.assertFalse(stdout)
def test_auth(self):
with mock.patch.object(openstack_shell.ApmecShell,
'run_subcommand'), \
mock.patch.object(session, 'Session'), \
mock.patch.object(clientmanager, 'ClientManager') as mock_cmgr:
shell = openstack_shell.ApmecShell(DEFAULT_API_VERSION)
shell.options = mock.Mock()
auth_session = shell._get_keystone_session()
cmdline = ('--os-username test '
'--os-password test '
'--os-tenant-name test '
'--os-auth-url http://127.0.0.1:5000/ '
'--os-auth-strategy keystone mead-list')
shell.authenticate_user()
shell.run(cmdline.split())
mock_cmgr.assert_called_with(
raise_errors=False, retries=0, timeout=None,
token='', url='', auth_url='http://127.0.0.1:5000/',
tenant_name='test', tenant_id='tenant_id',
username='test', user_id='',
password='test', region_name='',
api_version={'mec-orchestration': '1.0'},
auth_strategy='keystone',
service_type='mec-orchestration',
endpoint_type='publicURL', insecure=False, ca_cert=None,
log_credentials=True, session=auth_session, auth=auth_session.auth)
def test_build_option_parser(self):
apmec_shell = openstack_shell.ApmecShell(DEFAULT_API_VERSION)
result = apmec_shell.build_option_parser('descr', DEFAULT_API_VERSION)
self.assertIsInstance(result, argparse.ArgumentParser)
@mock.patch.object(openstack_shell.ApmecShell, 'run')
def test_main_with_unicode(self, mock_run):
mock_run.return_value = 0
unicode_text = u'\u7f51\u7edc'
argv = ['net-list', unicode_text, unicode_text.encode('utf-8')]
ret = openstack_shell.main(argv=argv)
mock_run.assert_called_once_with([u'net-list', unicode_text,
unicode_text])
self.assertEqual(0, ret)
def test_endpoint_option(self):
shell = openstack_shell.ApmecShell(DEFAULT_API_VERSION)
parser = shell.build_option_parser('descr', DEFAULT_API_VERSION)
# Neither $OS_ENDPOINT_TYPE nor --endpoint-type
namespace = parser.parse_args([])
self.assertEqual('publicURL', namespace.endpoint_type)
# --endpoint-type but not $OS_ENDPOINT_TYPE
namespace = parser.parse_args(['--endpoint-type=admin'])
self.assertEqual('admin', namespace.endpoint_type)
def test_endpoint_environment_variable(self):
fixture = fixtures.EnvironmentVariable("OS_ENDPOINT_TYPE",
"public")
self.useFixture(fixture)
shell = openstack_shell.ApmecShell(DEFAULT_API_VERSION)
parser = shell.build_option_parser('descr', DEFAULT_API_VERSION)
# $OS_ENDPOINT_TYPE but not --endpoint-type
namespace = parser.parse_args([])
self.assertEqual("public", namespace.endpoint_type)
# --endpoint-type and $OS_ENDPOINT_TYPE
namespace = parser.parse_args(['--endpoint-type=admin'])
self.assertEqual('admin', namespace.endpoint_type)

View File

@ -0,0 +1,82 @@
# Copyright (C) 2013 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import fixtures
from keystoneclient import session
import mock
import requests
import testtools
from apmecclient import client
from apmecclient.common import clientmanager
from apmecclient.common import exceptions
from apmecclient import shell as openstack_shell
AUTH_TOKEN = 'test_token'
END_URL = 'test_url'
METHOD = 'GET'
URL = 'http://test.test:1234/v1.0/'
CA_CERT = '/tmp/test/path'
DEFAULT_API_VERSION = '1.0'
class TestSSL(testtools.TestCase):
def setUp(self):
super(TestSSL, self).setUp()
self.useFixture(fixtures.EnvironmentVariable('OS_TOKEN', AUTH_TOKEN))
self.useFixture(fixtures.EnvironmentVariable('OS_URL', END_URL))
self.addCleanup(mock.patch.stopall)
def _test_verify_client_manager(self, cacert):
with mock.patch.object(session, 'Session'), \
mock.patch.object(clientmanager, 'ClientManager') as mock_cmgr:
mock_cmgr.return_value = 0
shell = openstack_shell.ApmecShell(DEFAULT_API_VERSION)
shell.options = mock.Mock()
auth_session = shell._get_keystone_session()
shell.run(cacert)
mock_cmgr.assert_called_with(
api_version={'mec-orchestration': '1.0'},
auth=auth_session.auth, auth_strategy='keystone',
auth_url='', ca_cert=CA_CERT, endpoint_type='publicURL',
insecure=False, log_credentials=True, password='',
raise_errors=False, region_name='', retries=0,
service_type='mec-orchestration', session=auth_session,
tenant_id='', tenant_name='', timeout=None,
token='test_token', url='test_url', user_id='', username='')
def test_ca_cert_passed(self):
cacert = ['--os-cacert', CA_CERT]
self._test_verify_client_manager(cacert)
def test_ca_cert_passed_as_env_var(self):
self.useFixture(fixtures.EnvironmentVariable('OS_CACERT', CA_CERT))
self._test_verify_client_manager([])
@mock.patch.object(client.HTTPClient, 'request')
def test_proper_exception_is_raised_when_cert_validation_fails(self,
mock_req):
http = client.HTTPClient(token=AUTH_TOKEN, endpoint_url=END_URL)
mock_req.side_effect = requests.exceptions.SSLError()
self.assertRaises(
exceptions.SslCertificateValidationError,
http._cs_request,
URL, METHOD
)

View File

@ -0,0 +1,149 @@
# Copyright (C) 2013 Yahoo! Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import testtools
from apmecclient.common import exceptions
from apmecclient.common import utils
class TestUtils(testtools.TestCase):
def test_string_to_bool_true(self):
self.assertTrue(utils.str2bool('true'))
def test_string_to_bool_false(self):
self.assertFalse(utils.str2bool('false'))
def test_string_to_bool_None(self):
self.assertIsNone(utils.str2bool(None))
def test_string_to_dictionary(self):
input_str = 'key1=value1,key2=value2'
expected = {'key1': 'value1', 'key2': 'value2'}
self.assertEqual(expected, utils.str2dict(input_str))
def test_none_string_to_dictionary(self):
input_str = ''
expected = {}
self.assertEqual(expected, utils.str2dict(input_str))
input_str = None
expected = {}
self.assertEqual(expected, utils.str2dict(input_str))
def test_get_dict_item_properties(self):
item = {'name': 'test_name', 'id': 'test_id'}
fields = ('name', 'id')
actual = utils.get_item_properties(item=item, fields=fields)
self.assertEqual(('test_name', 'test_id'), actual)
def test_get_object_item_properties_mixed_case_fields(self):
class Fake(object):
def __init__(self):
self.id = 'test_id'
self.name = 'test_name'
self.test_user = 'test'
fields = ('name', 'id', 'test user')
mixed_fields = ('test user', 'ID')
item = Fake()
actual = utils.get_item_properties(item, fields, mixed_fields)
self.assertEqual(('test_name', 'test_id', 'test'), actual)
def test_get_object_item_desired_fields_differ_from_item(self):
class Fake(object):
def __init__(self):
self.id = 'test_id_1'
self.name = 'test_name'
self.test_user = 'test'
fields = ('name', 'id', 'test user')
item = Fake()
actual = utils.get_item_properties(item, fields)
self.assertNotEqual(('test_name', 'test_id', 'test'), actual)
def test_get_object_item_desired_fields_is_empty(self):
class Fake(object):
def __init__(self):
self.id = 'test_id_1'
self.name = 'test_name'
self.test_user = 'test'
fields = []
item = Fake()
actual = utils.get_item_properties(item, fields)
self.assertEqual((), actual)
def test_get_object_item_with_formatters(self):
class Fake(object):
def __init__(self):
self.id = 'test_id'
self.name = 'test_name'
self.test_user = 'test'
class FakeCallable(object):
def __call__(self, *args, **kwargs):
return 'pass'
fields = ('name', 'id', 'test user', 'is_public')
formatters = {'is_public': FakeCallable()}
item = Fake()
act = utils.get_item_properties(item, fields, formatters=formatters)
self.assertEqual(('test_name', 'test_id', 'test', 'pass'), act)
class ImportClassTestCase(testtools.TestCase):
def test_get_client_class_invalid_version(self):
self.assertRaises(
exceptions.UnsupportedVersion,
utils.get_client_class, 'image', '2', {'image': '2'})
class ContainsKeyValue(object):
"""Checks whether a key/value pair is in a dict parameter.
The ContainsKeyValue class is a helper for mock.assert_*()
method. It enables strict check than the built in mock.ANY
helper, and is the equivalent of the mox.ContainsKeyValue()
function from the legacy mox library
Example usage could be:
mock_some_method.assert_called_once_with(
"hello",
ContainsKeyValue('foo', bar),
mock.ANY,
"world",
ContainsKeyValue('hello', world))
"""
def __init__(self, wantkey, wantvalue):
self.wantkey = wantkey
self.wantvalue = wantvalue
def __eq__(self, other):
try:
return other[self.wantkey] == self.wantvalue
except (KeyError, TypeError):
return False
def __ne__(self, other):
try:
return other[self.wantkey] != self.wantvalue
except (KeyError, TypeError):
return True
def __repr__(self):
return "<ContainsKeyValue: key " + str(self.wantkey) + \
" and value " + str(self.wantvalue) + ">"

View File

@ -0,0 +1,101 @@
# Copyright 2014 NEC Corporation
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import testtools
from apmecclient.common import exceptions
from apmecclient.common import validators
class FakeParsedArgs(object):
pass
class ValidatorTest(testtools.TestCase):
def _test_validate_int(self, attr_val, attr_name='attr1',
min_value=1, max_value=10):
obj = FakeParsedArgs()
setattr(obj, attr_name, attr_val)
ret = validators.validate_int_range(obj, attr_name,
min_value, max_value)
# Come here only if there is no exception.
self.assertIsNone(ret)
def _test_validate_int_error(self, attr_val, expected_msg,
attr_name='attr1', expected_exc=None,
min_value=1, max_value=10):
if expected_exc is None:
expected_exc = exceptions.CommandError
e = self.assertRaises(expected_exc,
self._test_validate_int,
attr_val, attr_name, min_value, max_value)
self.assertEqual(expected_msg, str(e))
def test_validate_int_min_max(self):
self._test_validate_int(1)
self._test_validate_int(10)
self._test_validate_int('1')
self._test_validate_int('10')
self._test_validate_int('0x0a')
self._test_validate_int_error(
0, 'attr1 "0" should be an integer [1:10].')
self._test_validate_int_error(
11, 'attr1 "11" should be an integer [1:10].')
self._test_validate_int_error(
'0x10', 'attr1 "0x10" should be an integer [1:10].')
def test_validate_int_min_only(self):
self._test_validate_int(1, max_value=None)
self._test_validate_int(10, max_value=None)
self._test_validate_int(11, max_value=None)
self._test_validate_int_error(
0, 'attr1 "0" should be an integer greater than or equal to 1.',
max_value=None)
def test_validate_int_max_only(self):
self._test_validate_int(0, min_value=None)
self._test_validate_int(1, min_value=None)
self._test_validate_int(10, min_value=None)
self._test_validate_int_error(
11, 'attr1 "11" should be an integer smaller than or equal to 10.',
min_value=None)
def test_validate_int_no_limit(self):
self._test_validate_int(0, min_value=None, max_value=None)
self._test_validate_int(1, min_value=None, max_value=None)
self._test_validate_int(10, min_value=None, max_value=None)
self._test_validate_int(11, min_value=None, max_value=None)
self._test_validate_int_error(
'abc', 'attr1 "abc" should be an integer.',
min_value=None, max_value=None)
def _test_validate_subnet(self, attr_val, attr_name='attr1'):
obj = FakeParsedArgs()
setattr(obj, attr_name, attr_val)
ret = validators.validate_ip_subnet(obj, attr_name)
# Come here only if there is no exception.
self.assertIsNone(ret)
def test_validate_ip_subnet(self):
self._test_validate_subnet('192.168.2.0/24')
self._test_validate_subnet('192.168.2.3/20')
self._test_validate_subnet('192.168.2.1')
e = self.assertRaises(exceptions.CommandError,
self._test_validate_subnet,
'192.168.2.256')
self.assertEqual('attr1 "192.168.2.256" is not a valid CIDR.', str(e))

View File

View File

@ -0,0 +1,6 @@
auth_url: 'http://1.2.3.4:5000'
username: 'xyz'
password: '12345'
project_name: 'abc'
project_domain_name: 'prj_domain_name'
user_domain_name: 'user_domain_name'

View File

@ -0,0 +1,5 @@
username: 'xyz'
password: '12345'
project_name: 'abc'
project_domain_name: 'prj_domain_name'
user_domain_name: 'user_domain_name'

View File

@ -0,0 +1,213 @@
# Copyright 2014 Intel Corporation
# All Rights Reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import sys
import mock
from apmecclient import shell
from apmecclient.apmec import v1_0 as apmecV1_0
from apmecclient.apmec.v1_0 import ApmecCommand
from apmecclient.apmec.v1_0.mem import mea
from apmecclient.tests.unit import test_cli10
from apmecclient.tests.unit import test_utils
API_VERSION = "1.0"
FORMAT = 'json'
TOKEN = 'testtoken'
ENDURL = 'localurl'
class CLITestV10VmMEAJSON(test_cli10.CLITestV10Base):
_RESOURCE = 'mea'
_RESOURCES = 'meas'
_MEA_RESOURCES = 'mea_resources'
def setUp(self):
plurals = {'meas': 'mea',
'resources': 'resource'}
super(CLITestV10VmMEAJSON, self).setUp(plurals=plurals)
@mock.patch.object(ApmecCommand, 'get_client')
def _test_create_resource(self, resource, cmd, name, myid, args,
position_names, position_values, mock_get,
tenant_id=None, tags=None, admin_state_up=True,
extra_body=None, **kwargs):
mock_get.return_value = self.client
non_admin_status_resources = ['mead', 'mea']
if (resource in non_admin_status_resources):
body = {resource: {}, }
else:
body = {resource: {'admin_state_up': admin_state_up, }, }
if tenant_id:
body[resource].update({'tenant_id': tenant_id})
if tags:
body[resource].update({'tags': tags})
if extra_body:
body[resource].update(extra_body)
body[resource].update(kwargs)
for i in range(len(position_names)):
body[resource].update({position_names[i]: position_values[i]})
ress = {resource:
{self.id_field: myid}, }
if name:
ress[resource].update({'name': name})
self.client.format = self.format
resstr = self.client.serialize(ress)
# url method body
resource_plural = apmecV1_0._get_resource_plural(resource,
self.client)
path = getattr(self.client, resource_plural + "_path")
# Work around for LP #1217791. XML deserializer called from
# MyComparator does not decodes XML string correctly.
if self.format == 'json':
_body = test_cli10.MyComparator(body, self.client)
else:
_body = self.client.serialize(body)
with mock.patch.object(self.client.httpclient, 'request') as mock_req:
mock_req.return_value = (test_cli10.MyResp(200), resstr)
args.extend(['--request-format', self.format])
args.extend(['--mead-id', 'mead'])
cmd_parser = cmd.get_parser('create_' + resource)
shell.run_command(cmd, cmd_parser, args)
mock_req.assert_called_once_with(
test_cli10.end_url(path, format=self.format), 'POST',
body=_body,
headers=test_utils.ContainsKeyValue('X-Auth-Token', TOKEN))
mock_get.assert_any_call()
def test_create_mea_all_params(self):
cmd = mea.CreateMEA(test_cli10.MyApp(sys.stdout), None)
name = 'my_name'
my_id = 'my-id'
mead_id = 'mead'
vim_id = 'vim_id'
description = 'my-description'
region_name = 'region'
key = 'key'
value = 'value'
args = [
name,
'--mead-id', mead_id,
'--vim-id', vim_id,
'--description', description,
'--vim-region-name', region_name,
'--%s' % key, value]
position_names = [
'name',
'mead_id',
'vim_id',
'description',
'attributes',
]
position_values = [
name,
mead_id,
vim_id,
description,
{},
]
extra_body = {key: value, 'placement_attr': {'region_name':
region_name}}
self._test_create_resource(self._RESOURCE, cmd, name, my_id,
args, position_names, position_values,
extra_body=extra_body)
def test_create_mea_with_mead_id(self):
cmd = mea.CreateMEA(test_cli10.MyApp(sys.stdout), None)
name = 'my_name'
my_id = 'my-id'
mead_id = 'mead'
args = [
name,
'--mead-id', mead_id,
]
position_names = ['name', 'mead_id', 'attributes']
position_values = [name, mead_id, {}]
self._test_create_resource(self._RESOURCE, cmd, name, my_id,
args, position_names, position_values)
def test_create_mea_with_description_param(self):
cmd = mea.CreateMEA(test_cli10.MyApp(sys.stdout), None)
name = 'my_name'
my_id = 'my-id'
mead_id = 'mead'
description = 'my-description'
args = [
name,
'--mead-id', mead_id,
'--description', description,
]
position_names = ['name', 'mead_id', 'description',
'attributes']
position_values = [name, mead_id, description, {}]
self._test_create_resource(self._RESOURCE, cmd, None, my_id,
args, position_names, position_values)
def test_list_meas(self):
cmd = mea.ListMEA(test_cli10.MyApp(sys.stdout), None)
self._test_list_resources(self._RESOURCES, cmd, True)
def test_list_meas_pagenation(self):
cmd = mea.ListMEA(test_cli10.MyApp(sys.stdout), None)
self._test_list_resources(self._RESOURCES, cmd, True)
def test_show_mea_id(self):
cmd = mea.ShowMEA(test_cli10.MyApp(sys.stdout), None)
args = ['--fields', 'id', self.test_id]
self._test_show_resource(self._RESOURCE, cmd, self.test_id, args,
['id'])
def test_show_mea_id_name(self):
cmd = mea.ShowMEA(test_cli10.MyApp(sys.stdout), None)
args = ['--fields', 'id', '--fields', 'name', self.test_id]
self._test_show_resource(self._RESOURCE, cmd, self.test_id,
args, ['id', 'name'])
def test_update_mea(self):
cmd = mea.UpdateMEA(test_cli10.MyApp(sys.stdout), None)
my_id = 'my-id'
key = 'new_key'
value = 'new-value'
self._test_update_resource(self._RESOURCE, cmd, my_id,
[my_id, '--%s' % key, value],
{key: value})
def test_delete_mea(self):
cmd = mea.DeleteMEA(test_cli10.MyApp(sys.stdout), None)
my_id = 'my-id'
args = [my_id]
self._test_delete_resource(self._RESOURCE, cmd, my_id, args)
def test_list_mea_resources(self):
cmd = mea.ListMEAResources(test_cli10.MyApp(sys.stdout), None)
base_args = [self.test_id]
response = [{'name': 'CP11', 'id': 'id1', 'type': 'NeutronPort'},
{'name': 'CP12', 'id': 'id2', 'type': 'NeutronPort'}]
val = self._test_list_sub_resources(self._MEA_RESOURCES, 'resources',
cmd, self.test_id,
response_contents=response,
detail=True, base_args=base_args)
self.assertIn('id1', val)
self.assertIn('NeutronPort', val)
self.assertIn('CP11', val)
def test_multi_delete_mea(self):
cmd = mea.DeleteMEA(test_cli10.MyApp(sys.stdout), None)
mea_ids = 'mea1 mea2 mea3'
args = [mea_ids]
self._test_delete_resource(self._RESOURCE, cmd, mea_ids, args)

View File

@ -0,0 +1,146 @@
# Copyright 2014 Intel Corporation
# All Rights Reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mock import mock_open
from mock import patch
import sys
from apmecclient.common.exceptions import InvalidInput
from apmecclient.apmec.v1_0.mem import mead
from apmecclient.tests.unit import test_cli10
class CLITestV10VmMEADJSON(test_cli10.CLITestV10Base):
_RESOURCE = 'mead'
_RESOURCES = 'meads'
def setUp(self):
plurals = {'meads': 'mead'}
super(CLITestV10VmMEADJSON, self).setUp(plurals=plurals)
@patch("apmecclient.apmec.v1_0.mem.mead.open",
side_effect=mock_open(read_data="mead"),
create=True)
def test_create_mead_all_params(self, mo):
cmd = mead.CreateMEAD(
test_cli10.MyApp(sys.stdout), None)
my_id = 'my-id'
name = 'my-name'
attr_key = 'mead'
attr_val = 'mead'
args = [
name,
'--mead-file', 'mead-file'
]
position_names = ['name']
position_values = [name]
extra_body = {
'service_types': [{'service_type': 'mead'}],
'attributes': {attr_key: attr_val},
}
self._test_create_resource(self._RESOURCE, cmd, None, my_id,
args, position_names, position_values,
extra_body=extra_body)
@patch("apmecclient.apmec.v1_0.mem.mead.open",
side_effect=mock_open(read_data="mead"),
create=True)
def test_create_mead_with_mandatory_params(self, mo):
cmd = mead.CreateMEAD(
test_cli10.MyApp(sys.stdout), None)
name = 'my_name'
my_id = 'my-id'
args = [name, '--mead-file', 'mead-file', ]
position_names = ['name']
position_values = [name]
extra_body = {
'service_types': [{'service_type': 'mead'}],
'attributes': {'mead': 'mead'}
}
self._test_create_resource(self._RESOURCE, cmd, name, my_id,
args, position_names, position_values,
extra_body=extra_body)
@patch("apmecclient.apmec.v1_0.mem.mead.open",
side_effect=mock_open(read_data=""),
create=True)
def test_create_mead_with_empty_file(self, mo):
cmd = mead.CreateMEAD(
test_cli10.MyApp(sys.stdout), None)
name = 'my_name'
my_id = 'my-id'
args = [name, '--mead-file', 'mead-file', ]
position_names = ['name']
position_values = [name]
extra_body = {
'service_types': [{'service_type': 'mead'}],
'attributes': {'mead': 'mead'}
}
err = None
try:
self._test_create_resource(self._RESOURCE, cmd, name, my_id,
args, position_names, position_values,
extra_body=extra_body)
except InvalidInput:
err = True
self.assertEqual(True, err)
def test_list_meads(self):
cmd = mead.ListMEAD(test_cli10.MyApp(sys.stdout), None)
self._test_list_resources(self._RESOURCES, cmd, True,
template_source='onboarded')
def test_list_inline_meads(self):
cmd = mead.ListMEAD(test_cli10.MyApp(sys.stdout), None)
self._test_list_resources(self._RESOURCES, cmd, True,
template_source='inline')
def test_list_all_meads(self):
cmd = mead.ListMEAD(test_cli10.MyApp(sys.stdout), None)
self._test_list_resources(self._RESOURCES, cmd, True,
template_source='all')
def test_list_meads_pagenation(self):
cmd = mead.ListMEAD(test_cli10.MyApp(sys.stdout), None)
print(cmd)
self._test_list_resources(self._RESOURCES, cmd, True,
template_source='onboarded')
def test_show_mead_id(self):
cmd = mead.ShowMEAD(test_cli10.MyApp(sys.stdout), None)
args = ['--fields', 'id', self.test_id]
self._test_show_resource(self._RESOURCE, cmd, self.test_id, args,
['id'])
def test_show_mead_id_name(self):
cmd = mead.ShowMEAD(test_cli10.MyApp(sys.stdout), None)
args = ['--fields', 'id', '--fields', 'name', self.test_id]
self._test_show_resource(self._RESOURCE, cmd, self.test_id,
args, ['id', 'name'])
def test_delete_mead(self):
cmd = mead.DeleteMEAD(
test_cli10.MyApp(sys.stdout), None)
my_id = 'my-id'
args = [my_id]
self._test_delete_resource(self._RESOURCE, cmd, my_id, args)
def test_multi_delete_mead(self):
cmd = mead.DeleteMEAD(
test_cli10.MyApp(sys.stdout), None)
mead_ids = 'my-id1 my-id2 my-id3'
args = [mead_ids]
self._test_delete_resource(self._RESOURCE, cmd, mead_ids, args)

View File

@ -0,0 +1,69 @@
# Copyright 2014 Intel Corporation
# All Rights Reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import sys
from apmecclient.apmec.v1_0.events import events
from apmecclient.tests.unit import test_cli10
API_VERSION = "1.0"
FORMAT = 'json'
TOKEN = 'testtoken'
ENDURL = 'localurl'
class CLITestV10EventJSON(test_cli10.CLITestV10Base):
_EVT_RESOURCE = 'event'
_EVT_RESOURCES = _EVT_RESOURCE + 's'
_MEA_EVT_RESOURCE = "mea_event"
_MEA_EVT_RESOURCES = _MEA_EVT_RESOURCE + 's'
_MEAD_EVT_RESOURCE = "mead_event"
_MEAD_EVT_RESOURCES = _MEAD_EVT_RESOURCE + 's'
_VIM_EVT_RESOURCE = "vim_event"
_VIM_EVT_RESOURCES = _VIM_EVT_RESOURCE + 's'
def setUp(self):
plurals = {'events': 'event', 'mea_events': 'mea_event',
'mead_events': 'mead_event', 'vim_events': 'vim_event'}
super(CLITestV10EventJSON, self).setUp(plurals=plurals)
def test_list_events(self):
cmd = events.ListResourceEvents(test_cli10.MyApp(sys.stdout), None)
self._test_list_resources(self._EVT_RESOURCES, cmd, True)
def test_show_event_id(self):
cmd = events.ShowEvent(test_cli10.MyApp(sys.stdout), None)
args = ['--fields', 'id', self.test_id]
self._test_show_resource(self._EVT_RESOURCE, cmd, self.test_id, args,
['id'])
def notest_list_mea_events(self):
# TODO(vishwanathj): Need to enhance _test_list_resources()
# for supporting filters to get this test working
cmd = events.ListMEAEvents(test_cli10.MyApp(sys.stdout), None)
self._test_list_resources(self._MEA_EVT_RESOURCES, cmd, True)
def notest_list_mead_events(self):
# TODO(vishwanathj): Need to enhance _test_list_resources()
# for supporting filters to get this test working
cmd = events.ListMEADEvents(test_cli10.MyApp(sys.stdout), None)
self._test_list_resources(self._MEAD_EVT_RESOURCES, cmd, True)
def notest_list_vim_events(self):
# TODO(vishwanathj): Need to enhance _test_list_resources()
# for supporting filters to get this test working
cmd = events.ListVIMEvents(test_cli10.MyApp(sys.stdout), None)
self._test_list_resources(self._VIM_EVT_RESOURCES, cmd, True)

View File

@ -0,0 +1,170 @@
# Copyright 2015-2016 Brocade Communications Systems Inc
# All Rights Reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import sys
from apmecclient.common import exceptions
from apmecclient.common import utils
from apmecclient.apmec.v1_0.meo import vim
from apmecclient.tests.unit import test_cli10
API_VERSION = "1.0"
FORMAT = 'json'
TOKEN = 'testtoken'
ENDURL = 'localurl'
class CLITestV10VIMJSON(test_cli10.CLITestV10Base):
_RESOURCE = 'vim'
_RESOURCES = 'vims'
def setUp(self):
plurals = {'vims': 'vim'}
super(CLITestV10VIMJSON, self).setUp(plurals=plurals)
self.vim_project = {
'name': 'abc',
'project_domain_name': 'prj_domain_name'}
self.auth_cred = {'username': 'xyz', 'password': '12345',
'user_domain_name': 'user_domain_name'}
self.auth_url = 'http://1.2.3.4:5000'
def test_register_vim_all_params(self):
cmd = vim.CreateVIM(test_cli10.MyApp(sys.stdout), None)
name = 'my-name'
my_id = 'my-id'
description = 'Vim Description'
vim_config = utils.get_file_path(
'tests/unit/vm/samples/vim_config.yaml')
args = [
name,
'--config-file', vim_config,
'--description', description]
position_names = ['auth_cred', 'vim_project', 'auth_url']
position_values = [self.auth_cred, self.vim_project,
self.auth_url]
extra_body = {'type': 'openstack', 'name': name,
'description': description, 'is_default': False}
self._test_create_resource(self._RESOURCE, cmd, None, my_id,
args, position_names, position_values,
extra_body=extra_body)
def test_register_vim_with_no_auth_url(self):
cmd = vim.CreateVIM(test_cli10.MyApp(sys.stdout), None)
my_id = 'my-id'
name = 'test_vim'
description = 'Vim Description'
vim_config = utils.get_file_path(
'tests/unit/vm/samples/vim_config_without_auth_url.yaml')
args = [
name,
'--config-file', vim_config,
'--description', description]
position_names = ['auth_cred', 'vim_project', 'auth_url']
position_values = [self.auth_cred, self.vim_project,
self.auth_url]
extra_body = {'type': 'openstack', 'name': name,
'description': description, 'is_default': False}
message = 'Auth URL must be specified'
ex = self.assertRaises(exceptions.ApmecClientException,
self._test_create_resource,
self._RESOURCE, cmd, None, my_id, args,
position_names, position_values,
extra_body=extra_body)
self.assertEqual(message, ex.message)
self.assertEqual(404, ex.status_code)
def test_register_vim_with_mandatory_params(self):
cmd = vim.CreateVIM(test_cli10.MyApp(sys.stdout), None)
name = 'my-name'
my_id = 'my-id'
vim_config = utils.get_file_path(
'tests/unit/vm/samples/vim_config.yaml')
args = [
name,
'--config-file', vim_config,
]
position_names = ['auth_cred', 'vim_project', 'auth_url']
position_values = [
self.auth_cred,
self.vim_project,
self.auth_url
]
extra_body = {'type': 'openstack', 'name': name, 'is_default': False}
self._test_create_resource(self._RESOURCE, cmd, name, my_id, args,
position_names, position_values,
extra_body=extra_body)
def test_list_vims(self):
cmd = vim.ListVIM(test_cli10.MyApp(sys.stdout), None)
self._test_list_resources(self._RESOURCES, cmd, True)
def test_show_vim_id(self):
cmd = vim.ShowVIM(test_cli10.MyApp(sys.stdout), None)
args = ['--fields', 'id', self.test_id]
self._test_show_resource(self._RESOURCE, cmd, self.test_id, args,
['id'])
def test_show_vim_id_name(self):
cmd = vim.ShowVIM(test_cli10.MyApp(sys.stdout), None)
args = ['--fields', 'id', '--fields', 'name', self.test_id]
self._test_show_resource(self._RESOURCE, cmd, self.test_id,
args, ['id', 'name'])
def test_update_vim_all_params(self):
cmd = vim.UpdateVIM(test_cli10.MyApp(sys.stdout), None)
update_config = utils.get_file_path(
'tests/unit/vm/samples/vim_config_without_auth_url.yaml')
my_id = 'my-id'
name = 'new_name'
description = 'new_description'
is_default = 'True'
args = [
my_id,
'--config-file', str(update_config),
'--name', name,
'--description', description,
'--is_default', is_default]
extra_fields = {'vim_project': self.vim_project, 'auth_cred':
self.auth_cred, 'is_default': 'True',
'name': name, 'description': description}
self._test_update_resource(self._RESOURCE, cmd, my_id, args,
extra_fields)
def test_update_vim_with_mandatory_params(self):
cmd = vim.UpdateVIM(test_cli10.MyApp(sys.stdout), None)
update_config = utils.get_file_path(
'tests/unit/vm/samples/vim_config_without_auth_url.yaml')
my_id = 'my-id'
args = [
my_id,
'--config-file', str(update_config)]
extra_fields = {'vim_project': self.vim_project,
'auth_cred': self.auth_cred}
self._test_update_resource(self._RESOURCE, cmd, my_id, args,
extra_fields)
def test_delete_vim(self):
cmd = vim.DeleteVIM(test_cli10.MyApp(sys.stdout), None)
my_id = 'my-id'
args = [my_id]
self._test_delete_resource(self._RESOURCE, cmd, my_id, args)
def test_multi_delete_vim(self):
cmd = vim.DeleteVIM(test_cli10.MyApp(sys.stdout), None)
vim_ids = 'my-id1 my-id2 my-id3'
args = [vim_ids]
self._test_delete_resource(self._RESOURCE, cmd, vim_ids, args)

View File

@ -0,0 +1,69 @@
# Copyright 2016 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mock import sentinel
import testtools
from apmecclient.common import exceptions
from apmecclient.apmec.v1_0.meo import vim_utils
class TestVIMUtils(testtools.TestCase):
def test_args2body_vim(self):
config_param = {'project_name': sentinel.prj_name,
'username': sentinel.usrname1,
'password': sentinel.password1,
'project_domain_name': sentinel.prj_domain_name1,
'user_domain_name': sentinel.user_domain.name, }
vim = {}
auth_cred = config_param.copy()
auth_cred.pop('project_name')
auth_cred.pop('project_domain_name')
expected_vim = {'auth_cred': auth_cred,
'vim_project':
{'name': sentinel.prj_name,
'project_domain_name': sentinel.prj_domain_name1}}
vim_utils.args2body_vim(config_param.copy(), vim)
self.assertEqual(expected_vim, vim)
def test_args2body_vim_no_project(self):
config_param = {'username': sentinel.usrname1,
'password': sentinel.password1,
'user_domain_name': sentinel.user_domain.name, }
vim = {}
self.assertRaises(exceptions.ApmecClientException,
vim_utils.args2body_vim,
config_param, vim)
def test_validate_auth_url_with_port(self):
auth_url = "http://localhost:8000/test"
url_parts = vim_utils.validate_auth_url(auth_url)
self.assertEqual('http', url_parts.scheme)
self.assertEqual('localhost:8000', url_parts.netloc)
self.assertEqual(8000, url_parts.port)
def test_validate_auth_url_without_port(self):
auth_url = "http://localhost/test"
url_parts = vim_utils.validate_auth_url(auth_url)
self.assertEqual('http', url_parts.scheme)
self.assertEqual('localhost', url_parts.netloc)
def test_validate_auth_url_exception(self):
auth_url = "localhost/test"
self.assertRaises(exceptions.ApmecClientException,
vim_utils.validate_auth_url,
auth_url)

View File

544
apmecclient/v1_0/client.py Normal file
View File

@ -0,0 +1,544 @@
# Copyright 2012 OpenStack Foundation.
# Copyright 2015 Hewlett-Packard Development Company, L.P.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import logging
import time
import requests
import six.moves.urllib.parse as urlparse
from apmecclient import client
from apmecclient.common import constants
from apmecclient.common import exceptions
from apmecclient.common import serializer
from apmecclient.common import utils
from apmecclient.i18n import _
_logger = logging.getLogger(__name__)
DEFAULT_DESC_LENGTH = 25
DEFAULT_ERROR_REASON_LENGTH = 100
def exception_handler_v10(status_code, error_content):
"""Exception handler for API v1.0 client.
This routine generates the appropriate Apmec exception according to
the contents of the response body.
:param status_code: HTTP error status code
:param error_content: deserialized body of error response
"""
error_dict = None
if isinstance(error_content, dict):
error_dict = error_content.get('ApmecError')
# Find real error type
bad_apmec_error_flag = False
if error_dict:
# If Apmec key is found, it will definitely contain
# a 'message' and 'type' keys?
try:
error_type = error_dict['type']
error_message = error_dict['message']
if error_dict['detail']:
error_message += "\n" + error_dict['detail']
except Exception:
bad_apmec_error_flag = True
if not bad_apmec_error_flag:
# If corresponding exception is defined, use it.
client_exc = getattr(exceptions, '%sClient' % error_type, None)
# Otherwise look up per status-code client exception
if not client_exc:
client_exc = exceptions.HTTP_EXCEPTION_MAP.get(status_code)
if client_exc:
raise client_exc(message=error_message,
status_code=status_code)
else:
raise exceptions.ApmecClientException(
status_code=status_code, message=error_message)
else:
raise exceptions.ApmecClientException(status_code=status_code,
message=error_dict)
else:
message = None
if isinstance(error_content, dict):
message = error_content.get('message')
if message:
raise exceptions.ApmecClientException(status_code=status_code,
message=message)
# If we end up here the exception was not a apmec error
msg = "%s-%s" % (status_code, error_content)
raise exceptions.ApmecClientException(status_code=status_code,
message=msg)
class APIParamsCall(object):
"""A Decorator to support formating and tenant overriding and filters."""
def __init__(self, function):
self.function = function
def __get__(self, instance, owner):
def with_params(*args, **kwargs):
_format = instance.format
if 'format' in kwargs:
instance.format = kwargs['format']
ret = self.function(instance, *args, **kwargs)
instance.format = _format
return ret
return with_params
class ClientBase(object):
"""Client for the OpenStack Apmec v1.0 API.
:param string username: Username for authentication. (optional)
:param string user_id: User ID for authentication. (optional)
:param string password: Password for authentication. (optional)
:param string token: Token for authentication. (optional)
:param string tenant_name: Tenant name. (optional)
:param string tenant_id: Tenant id. (optional)
:param string auth_strategy: 'keystone' by default, 'noauth' for no
authentication against keystone. (optional)
:param string auth_url: Keystone service endpoint for authorization.
:param string service_type: Network service type to pull from the
keystone catalog (e.g. 'network') (optional)
:param string endpoint_type: Network service endpoint type to pull from the
keystone catalog (e.g. 'publicURL',
'internalURL', or 'adminURL') (optional)
:param string region_name: Name of a region to select when choosing an
endpoint from the service catalog.
:param string endpoint_url: A user-supplied endpoint URL for the apmec
service. Lazy-authentication is possible for API
service calls if endpoint is set at
instantiation.(optional)
:param integer timeout: Allows customization of the timeout for client
http requests. (optional)
:param bool insecure: SSL certificate validation. (optional)
:param bool log_credentials: Allow for logging of passwords or not.
Defaults to False. (optional)
:param string ca_cert: SSL CA bundle file to use. (optional)
:param integer retries: How many times idempotent (GET, PUT, DELETE)
requests to Apmec server should be retried if
they fail (default: 0).
:param bool raise_errors: If True then exceptions caused by connection
failure are propagated to the caller.
(default: True)
:param session: Keystone client auth session to use. (optional)
:param auth: Keystone auth plugin to use. (optional)
Example::
from apmecclient.v1_0 import client
apmec = client.Client(username=USER,
password=PASS,
tenant_name=TENANT_NAME,
auth_url=KEYSTONE_URL)
nets = apmec.list_networks()
...
"""
# API has no way to report plurals, so we have to hard code them
# This variable should be overridden by a child class.
EXTED_PLURALS = {}
def __init__(self, **kwargs):
"""Initialize a new client for the Apmec v1.0 API."""
super(ClientBase, self).__init__()
self.retries = kwargs.pop('retries', 0)
self.raise_errors = kwargs.pop('raise_errors', True)
self.httpclient = client.construct_http_client(**kwargs)
self.version = '1.0'
self.format = 'json'
self.action_prefix = "/v%s" % (self.version)
self.retry_interval = 1
def _handle_fault_response(self, status_code, response_body):
# Create exception with HTTP status code and message
_logger.debug("Error message: %s", response_body)
# Add deserialized error message to exception arguments
try:
des_error_body = self.deserialize(response_body, status_code)
except Exception:
# If unable to deserialized body it is probably not a
# Apmec error
des_error_body = {'message': response_body}
# Raise the appropriate exception
exception_handler_v10(status_code, des_error_body)
def do_request(self, method, action, body=None, headers=None, params=None):
# Add format and tenant_id
action += ".%s" % self.format
action = self.action_prefix + action
if type(params) is dict and params:
params = utils.safe_encode_dict(params)
action += '?' + urlparse.urlencode(params, doseq=1)
if body:
body = self.serialize(body)
resp, replybody = self.httpclient.do_request(
action, method, body=body,
content_type=self.content_type())
status_code = resp.status_code
if status_code in (requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
requests.codes.no_content):
return self.deserialize(replybody, status_code)
else:
if not replybody:
replybody = resp.reason
self._handle_fault_response(status_code, replybody)
def get_auth_info(self):
return self.httpclient.get_auth_info()
def serialize(self, data):
"""Serializes a dictionary into either XML or JSON.
A dictionary with a single key can be passed and it can contain any
structure.
"""
if data is None:
return None
elif type(data) is dict:
return serializer.Serializer(
self.get_attr_metadata()).serialize(data, self.content_type())
else:
raise Exception(_("Unable to serialize object of type = '%s'") %
type(data))
def deserialize(self, data, status_code):
"""Deserializes an XML or JSON string into a dictionary."""
if status_code == 204:
return data
return serializer.Serializer(self.get_attr_metadata()).deserialize(
data, self.content_type())['body']
def 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_V10,
constants.EXT_NS: ns}
def content_type(self, _format=None):
"""Returns the mime-type for either 'xml' or 'json'.
Defaults to the currently set format.
"""
_format = _format or self.format
return "application/%s" % (_format)
def retry_request(self, method, action, body=None,
headers=None, params=None):
"""Call do_request with the default retry configuration.
Only idempotent requests should retry failed connection attempts.
:raises: ConnectionFailed if the maximum # of retries is exceeded
"""
max_attempts = self.retries + 1
for i in range(max_attempts):
try:
return self.do_request(method, action, body=body,
headers=headers, params=params)
except exceptions.ConnectionFailed:
# Exception has already been logged by do_request()
if i < self.retries:
_logger.debug('Retrying connection to Apmec service')
time.sleep(self.retry_interval)
elif self.raise_errors:
raise
if self.retries:
msg = (_("Failed to connect to Apmec server after %d attempts")
% max_attempts)
else:
msg = _("Failed to connect Apmec server")
raise exceptions.ConnectionFailed(reason=msg)
def delete(self, action, body=None, headers=None, params=None):
return self.retry_request("DELETE", action, body=body,
headers=headers, params=params)
def get(self, action, body=None, headers=None, params=None):
return self.retry_request("GET", action, body=body,
headers=headers, params=params)
def post(self, action, body=None, headers=None, params=None):
# Do not retry POST requests to avoid the orphan objects problem.
return self.do_request("POST", action, body=body,
headers=headers, params=params)
def put(self, action, body=None, headers=None, params=None):
return self.retry_request("PUT", action, body=body,
headers=headers, params=params)
def list(self, collection, path, retrieve_all=True, **params):
if retrieve_all:
res = []
for r in self._pagination(collection, path, **params):
res.extend(r[collection])
return {collection: res}
else:
return self._pagination(collection, path, **params)
def _pagination(self, collection, path, **params):
if params.get('page_reverse', False):
linkrel = 'previous'
else:
linkrel = 'next'
next = True
while next:
res = self.get(path, params=params)
yield res
next = False
try:
for link in res['%s_links' % collection]:
if link['rel'] == linkrel:
query_str = urlparse.urlparse(link['href']).query
params = urlparse.parse_qs(query_str)
next = True
break
except KeyError:
break
class Client(ClientBase):
extensions_path = "/extensions"
extension_path = "/extensions/%s"
meads_path = '/meads'
mead_path = '/meads/%s'
meas_path = '/meas'
mea_path = '/meas/%s'
mea_scale_path = '/meas/%s/actions'
mea_resources_path = '/meas/%s/resources'
vims_path = '/vims'
vim_path = '/vims/%s'
events_path = '/events'
event_path = '/events/%s'
mesds_path = '/mesds'
mesd_path = '/mesds/%s'
mess_path = '/mess'
mes_path = '/mess/%s'
# API has no way to report plurals, so we have to hard code them
# EXTED_PLURALS = {}
@APIParamsCall
def list_extensions(self, **_params):
"""Fetch a list of all exts on server side."""
return self.get(self.extensions_path, params=_params)
@APIParamsCall
def show_extension(self, ext_alias, **_params):
"""Fetch a list of all exts on server side."""
return self.get(self.extension_path % ext_alias, params=_params)
_MEAD = "mead"
_MESD = "mesd"
@APIParamsCall
def list_meads(self, retrieve_all=True, **_params):
meads_dict = self.list(self._MEAD + 's',
self.meads_path,
retrieve_all,
**_params)
for mead in meads_dict['meads']:
if mead.get('description'):
if len(mead['description']) > DEFAULT_DESC_LENGTH:
mead['description'] = \
mead['description'][:DEFAULT_DESC_LENGTH]
mead['description'] += '...'
return meads_dict
@APIParamsCall
def show_mead(self, mead, **_params):
return self.get(self.mead_path % mead,
params=_params)
@APIParamsCall
def create_mead(self, body):
body[self._MEAD]['service_types'] = [{'service_type': 'mead'}]
return self.post(self.meads_path, body)
@APIParamsCall
def delete_mead(self, mead):
return self.delete(self.mead_path % mead)
@APIParamsCall
def list_meas(self, retrieve_all=True, **_params):
meas = self.list('meas', self.meas_path, retrieve_all, **_params)
for mea in meas['meas']:
error_reason = mea.get('error_reason', None)
if error_reason and \
len(error_reason) > DEFAULT_ERROR_REASON_LENGTH:
mea['error_reason'] = error_reason[
:DEFAULT_ERROR_REASON_LENGTH]
mea['error_reason'] += '...'
return meas
@APIParamsCall
def show_mea(self, mea, **_params):
return self.get(self.mea_path % mea, params=_params)
@APIParamsCall
def create_mea(self, body):
return self.post(self.meas_path, body=body)
@APIParamsCall
def delete_mea(self, mea):
return self.delete(self.mea_path % mea)
@APIParamsCall
def update_mea(self, mea, body):
return self.put(self.mea_path % mea, body=body)
@APIParamsCall
def list_mea_resources(self, mea, retrieve_all=True, **_params):
return self.list('resources', self.mea_resources_path % mea,
retrieve_all, **_params)
@APIParamsCall
def scale_mea(self, mea, body=None):
return self.post(self.mea_scale_path % mea, body=body)
@APIParamsCall
def show_vim(self, vim, **_params):
return self.get(self.vim_path % vim, params=_params)
_VIM = "vim"
@APIParamsCall
def create_vim(self, body):
return self.post(self.vims_path, body=body)
@APIParamsCall
def delete_vim(self, vim):
return self.delete(self.vim_path % vim)
@APIParamsCall
def update_vim(self, vim, body):
return self.put(self.vim_path % vim, body=body)
@APIParamsCall
def list_vims(self, retrieve_all=True, **_params):
return self.list('vims', self.vims_path, retrieve_all, **_params)
@APIParamsCall
def list_events(self, retrieve_all=True, **_params):
events = self.list('events', self.events_path, retrieve_all,
**_params)
return events
@APIParamsCall
def list_mea_events(self, retrieve_all=True, **_params):
_params['resource_type'] = 'mea'
events = self.list('events', self.events_path, retrieve_all,
**_params)
mea_events = {}
mea_events['mea_events'] = events['events']
return mea_events
@APIParamsCall
def list_mead_events(self, retrieve_all=True, **_params):
_params['resource_type'] = 'mead'
events = self.list('events', self.events_path, retrieve_all,
**_params)
mead_events = {}
mead_events['mead_events'] = events['events']
return mead_events
@APIParamsCall
def list_vim_events(self, retrieve_all=True, **_params):
_params['resource_type'] = 'vim'
events = self.list('events', self.events_path, retrieve_all,
**_params)
vim_events = {}
vim_events['vim_events'] = events['events']
return vim_events
@APIParamsCall
def show_event(self, event_id, **_params):
return self.get(self.event_path % event_id, params=_params)
@APIParamsCall
def list_mesds(self, retrieve_all=True, **_params):
mesds_dict = self.list(self._MESD + 's',
self.mesds_path,
retrieve_all,
**_params)
for mesd in mesds_dict['mesds']:
if 'description' in mesd.keys() and \
len(mesd['description']) > DEFAULT_DESC_LENGTH:
mesd['description'] = mesd['description'][:DEFAULT_DESC_LENGTH]
mesd['description'] += '...'
return mesds_dict
@APIParamsCall
def show_mesd(self, mesd, **_params):
return self.get(self.mesd_path % mesd,
params=_params)
@APIParamsCall
def create_mesd(self, body):
return self.post(self.mesds_path, body)
@APIParamsCall
def delete_mesd(self, mesd):
return self.delete(self.mesd_path % mesd)
@APIParamsCall
def list_mess(self, retrieve_all=True, **_params):
mess = self.list('mess', self.mess_path, retrieve_all, **_params)
for mes in mess['mess']:
error_reason = mes.get('error_reason', None)
if error_reason and \
len(error_reason) > DEFAULT_ERROR_REASON_LENGTH:
mes['error_reason'] = error_reason[
:DEFAULT_ERROR_REASON_LENGTH]
mes['error_reason'] += '...'
return mess
@APIParamsCall
def show_mes(self, mes, **_params):
return self.get(self.mes_path % mes, params=_params)
@APIParamsCall
def create_mes(self, body):
return self.post(self.mess_path, body=body)
@APIParamsCall
def delete_mes(self, mes):
return self.delete(self.mes_path % mes)

19
apmecclient/version.py Normal file
View File

@ -0,0 +1,19 @@
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import pbr.version
__version__ = pbr.version.VersionInfo('python-apmecclient').version_string()

66
doc/source/conf.py Normal file
View File

@ -0,0 +1,66 @@
project = 'python-apmecclient'
# -- General configuration ---------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'openstackdocstheme'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
copyright = u'OpenStack Foundation'
# If true, '()' will be appended to :func: etc. cross-reference text.
add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# -- Options for HTML output ---------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'openstackdocs'
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
html_last_updated_fmt = '%Y-%m-%d %H:%M'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author,
# documentclass [howto/manual]).
latex_documents = [
('index',
'%s.tex' % project,
u'%s Documentation' % project,
u'OpenStack Foundation', 'manual'),
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}
# -- Options for openstackdocstheme -------------------------------------------
repository_name = 'openstack/python-apmecclient'
bug_project = 'python-apmecclient'
bug_tag = ''

25
doc/source/index.rst Normal file
View File

@ -0,0 +1,25 @@
Python bindings to the OpenStack Apmec API
============================================
In order to use the python apmec 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.
Command-line Tool
=================
In order to use the CLI, you must provide your OpenStack username, password, tenant, and auth endpoint. Use the corresponding configuration options (``--os-username``, ``--os-password``, ``--os-tenant-name``, and ``--os-auth-url``) or set them in environment variables::
export OS_USERNAME=user
export OS_PASSWORD=pass
export OS_TENANT_NAME=tenant
export OS_AUTH_URL=http://auth.example.com:5000/v2.0
The command line tool will attempt to reauthenticate using your provided credentials for every request. You can override this behavior by manually supplying an auth token using ``--os-url`` and ``--os-auth-token``. You can alternatively set these environment variables::
export OS_URL=http://apmec.example.org:9896/
export OS_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
If apmec server does not require authentication, besides these two arguments or environment variables (We can use any value as token.), we need manually supply ``--os-auth-strategy`` or set the environment variable::
export OS_AUTH_STRATEGY=noauth
Once you've configured your authentication parameters, you can run ``apmec -h`` to see a complete listing of available commands.

18
requirements.txt Normal file
View File

@ -0,0 +1,18 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr!=2.1.0,>=2.0.0 # Apache-2.0
cliff!=2.9.0,>=2.8.0 # Apache-2.0
iso8601>=0.1.11 # MIT
netaddr>=0.7.18 # BSD
requests>=2.14.2 # Apache-2.0
python-keystoneclient>=3.8.0 # Apache-2.0
simplejson>=3.5.1 # MIT
six>=1.9.0 # MIT
stevedore>=1.20.0 # Apache-2.0
Babel!=2.4.0,>=2.3.4 # BSD
oslo.i18n>=3.15.3 # Apache-2.0
oslo.log>=3.30.0 # Apache-2.0
oslo.utils>=3.28.0 # Apache-2.0
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0

45
setup.cfg Normal file
View File

@ -0,0 +1,45 @@
[metadata]
name = python-apmecclient
summary = CLI and Client Library for OpenStack Apmec
description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://docs.openstack.org/developer/apmec/
classifier =
Environment :: OpenStack
Intended Audience :: Developers
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
[files]
packages =
apmecclient
[global]
setup-hooks =
pbr.hooks.setup_hook
[entry_points]
console_scripts =
apmec = apmecclient.shell:main
[build_sphinx]
all_files = 1
build-dir = doc/build
source-dir = doc/source
[build_releasenotes]
all_files = 1
build-dir = releasenotes/build
source-dir = releasenotes/source
[wheel]
universal = 1

29
setup.py Normal file
View File

@ -0,0 +1,29 @@
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# 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.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)

19
test-requirements.txt Normal file
View File

@ -0,0 +1,19 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD
flake8<2.6.0,>=2.5.4 # MIT
pep8==1.5.7 # MIT
pyflakes==0.8.1 # MIT
python-subunit>=0.0.18 # Apache-2.0/BSD
sphinx>=1.6.2 # BSD
testrepository>=0.0.18 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
oslosphinx>=4.7.0 # Apache-2.0
openstackdocstheme>=1.17.0 # Apache-2.0
# releasenotes
reno>=2.5.0 # Apache-2.0
mock>=2.0.0 # BSD

View File

@ -0,0 +1,27 @@
_apmec_opts="" # lazy init
_apmec_flags="" # lazy init
_apmec_opts_exp="" # lazy init
_apmec()
{
local cur prev nbc cflags
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
if [ "x$_apmec_opts" == "x" ] ; then
nbc="`apmec bash-completion`"
_apmec_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`"
_apmec_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`"
_apmec_opts_exp="`echo "$_apmec_opts" | sed -e "s/\s/|/g"`"
fi
if [[ " ${COMP_WORDS[@]} " =~ " "($_apmec_opts_exp)" " && "$prev" != "help" ]] ; then
COMPLETION_CACHE=~/.apmecclient/*/*-cache
cflags="$_apmec_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ')
COMPREPLY=($(compgen -W "${cflags}" -- ${cur}))
else
COMPREPLY=($(compgen -W "${_apmec_opts}" -- ${cur}))
fi
return 0
}
complete -F _apmec apmec

30
tools/tox_install.sh Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Client constraint file contains this client version pin that is in conflict
# with installing the client from source. We should remove the version pin in
# the constraints file before applying it for from-source installation.
CONSTRAINTS_FILE="$1"
shift 1
set -e
# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get
# published to logs.openstack.org for easy debugging.
localfile="$VIRTUAL_ENV/log/upper-constraints.txt"
if [[ "$CONSTRAINTS_FILE" != http* ]]; then
CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE"
fi
# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep
curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile"
pip install -c"$localfile" openstack-requirements
# This is the main purpose of the script: Allow local installation of
# the current repo. It is listed in constraints file and thus any
# install will be constrained and we need to unconstrain it.
edit-constraints "$localfile" -- "$CLIENT_NAME"
pip install -c"$localfile" -U "$@"
exit $?

36
tox.ini Normal file
View File

@ -0,0 +1,36 @@
[tox]
envlist = py35,py27,pypy,pep8
minversion = 2.0
skipsdist = True
[testenv]
setenv = VIRTUAL_ENV={envdir}
BRANCH_NAME=master
CLIENT_NAME=python-apmecclient
LANG=en_US.UTF-8
LANGUAGE=en_US:en
LC_ALL=C
usedevelop = True
install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = python setup.py testr --testr-args='{posargs}'
[testenv:pep8]
commands = flake8
distribute = false
[testenv:venv]
commands = {posargs}
[testenv:releasenotes]
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[testenv:cover]
commands = python setup.py testr --coverage --testr-args='{posargs}'
[flake8]
# E125 continuation line does not distinguish itself from next logical line
ignore = E125
show-source = true
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools