Compare commits

...

26 Commits

Author SHA1 Message Date
8636b3dbb5 Use assertCountEqual instead of assertItemsEqual
The assertItemsEqual method has been removed in Python 3.3 [1] but
it was kept alive by unittest2, imported by testtools. For better
compatibility, change to assertCountEqual.

[1] https://bugs.python.org/issue17866

Change-Id: Iea76915f25eb2e505750d509bdd06758afe324de
(cherry picked from commit b5603cc45c)
2022-12-16 13:33:40 +00:00
Manpreet Kaur
900a1aa78e Fix failing UT in TestListVnfLcmOp
The unit test 'tackerclient.tests.unit.osc.v1.test_vnflcm_op_occs.
TestListVnfLcmOp.test_take_action_with_filter' is failing with below
error message,
DEBUG: TypeError: Object of type 'FormatComplexDataColumn' is not
JSON serializable

Background:
In class TestListVnfLcmOp definition, "create_vnflcm_op_occs" function
is called and list of fake vnflcm op occs dictionary is store in
vnflcm_op_occs_obj.
Now this dictionary is used in two unit test cases "test_take_action"
and "test_take_action_with_filter".
In order to evaluate test results, in "test_take_action" test case,
"get_vnflcm_op_occ_data" function is called using "vnflcm_op_occs_obj"
which appends the data in dictionary.

Later this dictionary "vnflcm_op_occs_obj" is again used in
"test_take_action_with_filter".

Implementation:
This patch creates a separate list of fake vnflcm op occs dictionary for
both the test cases.

This issue has been impacting below reviews as well,
[1] https://review.opendev.org/c/openstack/python-tackerclient/+/636893
[2] https://review.opendev.org/c/openstack/python-tackerclient/+/781314

Co-Authored: Yasufumi Ogawa <yasufum.o@gmail.com>
Closes-Bug: #1919350
Change-Id: I0d62f77cf5d1e9ec0b0a7c404abab83f97b708ba
2022-01-06 06:45:09 +00:00
21abb7ffd0 Update TOX_CONSTRAINTS_FILE for stable/wallaby
Update the URL to the upper-constraints file to point to the redirect
rule on releases.openstack.org so that anyone working on this branch
will switch to the correct upper-constraints list automatically when
the requirements repository branches.

Until the requirements repository has as stable/wallaby branch, tests will
continue to use the upper-constraints list on master.

Change-Id: I8f54797a47e23e4cf549792e1888db3774274405
2021-09-23 17:56:18 +00:00
ecdf16a9b7 Update .gitreview for stable/wallaby
Change-Id: I9d2601aec639061a339b2ebdfe8826e208b90d97
2021-09-23 17:55:58 +00:00
Yasufumi Ogawa
1a457e074a Drop test for lower constraints
As we agreed, drop lower constraints test from stable branches to avoid
difficulty of maintaining package depencencies for the recent pip
dependency resolver.

Signed-off-by: Yasufumi Ogawa <yasufum.o@gmail.com>
Change-Id: I24f675a0836eacef264254bb5da05286a1edf161
2021-09-22 16:27:23 +09:00
Aldinson Esto
4e6dc4c031 CLI for Individual VNF LCM Operation Occurrence
Add ``openstack vnflcm op show <vnf-lcm-op-occ-id>``
to python-tackerclient. This command can execute the
get Individual VNF LCM Operation Occurrence by
specifying vnf-lcm-op-occ-id as parameter.

Implements: blueprint support-fundamental-lcm
https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-fundamental-vnf-lcm-based-on-ETSI-NFV.html
Change-Id: Ibd24a2aa3ec90fbca4caabbcfb3c8a3883e4eae8
2021-03-11 09:54:10 +09:00
Aldinson Esto
6b29bb78b1 Support CLI for Getting List of VNF LCM Operation Occurrences
Add ``openstack vnflcm op list`` to python-tackerclient.
This command can execute getting the list of VNF LCM
Operation Occurrences. User can specify filters for more
specific results.

Note:
- Filtering for the following attributes:
  operationParams, error, resourceChanges and
  changedInfo is only limited to the parent
  attribute. Currently, child attributes/nested
  attributes are not searchable.

Implements: blueprint support-fundamental-lcm
Spec: https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-fundamental-vnf-lcm-based-on-ETSI-NFV.html
Change-Id: Ie0b3399946d2a705011269025102d9380102ca92
2021-03-10 22:24:11 +09:00
Wataru Juso
1299e15f35 Support of Retry VNF command in openstackclient
Add ``openstack vnflcm op retry`` to python-tackerclient.
This command can execute Retry operation.
This API re-executes an LCM that is stopped in the FAILED_TEMP state
from where it was interrupted.

Implements: blueprint support-error-handling
Spec: https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-error-handling-based-on-ETSI-NFV.html
Change-Id: I6731d0bcbcd5a4e597596e63c2cd883b3897a145
2021-03-10 11:14:11 +09:00
Wataru Juso
cfe14110d1 Support CLI of Change External VNF Connectivity
Add ``openstack vnflcm op chg-ext-conn`` to python-tackerclient.
This command can execute Change external VNF Connectivity operation.
This API can change VL setting.

Implements: blueprint support-change-external-connectivity
Spec: https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-change-external-VNF-connectivity-operation.html
Change-Id: I3a935296646361032665082a4060bc21ff51c1b1
2021-03-10 09:54:49 +09:00
Wataru Juso
58167535c7 Support of Fail VNF command in openstackclient
Add ``openstack vnflcm op fail`` to python-tackerclient.
This command can execute Fail operation.
The API can change operationStatus from "FAILED_TEMP" to "FAILED".

Implements: blueprint support-error-handling
Spec: https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-error-handling-based-on-ETSI-NFV.html
Change-Id: I83395e159e28c7e831dfe0ecd90435b3fb18c196
2021-03-08 09:26:29 +09:00
Zuul
b7968d815e Merge "Fix error message for nonexistent vnf package" 2021-03-05 06:56:21 +00:00
Manpreet Kaur
e7a0c96920 Fix error message for nonexistent vnf package
In Victoria release, the error handling of non-existing VNF package
was modified, please refer to [1].
On passing a non-existing vnf package uuid to "vnf package upload"
command, the tacker server sends an HTTP response with "Content-Type"
header as "application/problem+json" and the reason for a failure
described in the JSON problem details object.

This patch extracts the ETSI error message in method
exception_handler_v10.

Fixes the issue and now it will output below error message:

  $ openstack vnf package upload --path sample_vnf_pkg.zip dummy-id
  Can not find requested vnf package: dummy-id

Note: This bug was earlier address in Ussuri release, please refer [2].

[1] https://review.opendev.org/c/openstack/tacker/+/747678/37/tacker/api/vnfpkgm/v1/controller.py
[2] https://review.opendev.org/c/openstack/python-tackerclient/+/688886

Co-Authored-By: Wataru Juso  w-juso@nec.com
Closes-Bug: #1847726
Change-Id: I25e1bdc32e0b91bbe02b82f79918c02b98e5f110
2021-03-03 08:03:26 +05:30
zhangboye
68df82918a Dropping explicit unicode literal
In python 3, all strings are considered as unicode string.

This patch drops the explicit unicode literal (u'...')
or (u"..") appearances from the unicode strings.

Note: The scope of the patch is to drop unicode literal prefix
from python source code files, documentation and comment are overlook.

Co-Authored-By: Manpreet Kaur kaurmanpreet2620@gmail.com
Change-Id: I5316037871109838a03ff4561b7b38dcc56bc447
2021-02-26 10:41:44 +05:30
Zuul
76da3dbd9d Merge "Fix old links in installation guide" 2021-02-23 06:24:29 +00:00
Manpreet Kaur
17f4bcac1a Fix old links in installation guide
The installation guide refers to old github links in order to
install python-tackerclient.

This patch replaces old links [1] with new source code links [2].
Additionally, restructure document content for readability.

[1] https://github.com/openstack/python-tackerclient
[2] https://opendev.org/openstack/python-tackerclient

Closes-Bug: #1914534
Change-Id: I4876a9a2f61d5539b724a888ee2d941a989be717
2021-02-08 17:12:32 +05:30
Manpreet Kaur
69d9668282 Update TOX_CONSTRAINTS_FILE
UPPER_CONSTRAINTS_FILE is old name and deprecated, refer [1].

This allows to use lower-constraints file as more readable way
instead of UPPER_CONSTRAINTS_FILE=<lower-constraints file>.

[1] https://zuul-ci.org/docs/zuul-jobs/python-roles.html#rolevar-tox.tox_constraints_file

Change-Id: Ie55041121ab9a743285c6bcfd27233b1b60db636
2021-01-29 06:01:02 +05:30
Wataru Juso
bbe6d2c7e0 Modify operation of scale parameters
Scale command cannot be executed without '--additional-param-file'
option, because there is a problem with branch process. Modify branch
process when using this parameter.

And, I uniform cardinality mismatches of specification definition [1]
in some arguments are resolved. This change has no impact of the order
of setting.

[1]https://specs.openstack.org/openstack/tacker-specs/specs/victoria/support-scale-api-based-on-etsi-nfv-sol.html

Closes-Bug: #1903280
Change-Id: I1dd2c71760112982abd2b4b7da6dbaafd7077614
2021-01-05 14:48:27 +09:00
Manpreet Kaur
c6df687c94 Move python-tackerclient to new hacking 4.0.0
New rule enforcement has been enabled in the latest hacking code.

This patch bumps new version of hacking in test-requirements file,
for early detection and to avoid code breakage later when hacking
changes are released.

Change-Id: Ic54af7096b58259d6ee434a743ce9b75b7b61f28
2020-12-07 07:07:25 +00:00
Yasufumi Ogawa
754f6df5a7 Drop six support
This update is to drop all of six support to move

Closes-bug: #1900389

Change-Id: Ia6c61751203e98d432344dc9a52fe65bdb062af0
Signed-off-by: Yasufumi Ogawa <yasufum.o@gmail.com>
2020-11-01 18:43:00 +00:00
Zuul
727493c7bf Merge "Remove six.moves.urllib" 2020-10-31 16:02:38 +00:00
LiangLu
6bdb64f464 Modify the description of the command --help
The description of the "openstack vnf package artifact download --help"
command is incorrect.Modify VNF Package or VNFD data to VNF Package
artifact file data.

Change-Id: Ib70efb7eefdb6eacea24dec67bb4ff996b6178d3
Closes-Bug: #1901103
2020-10-28 02:54:27 -04:00
wu.chunyang
cfbc810bdc Remove the unused coding style modules
Python modules related to coding style checks (listed in blacklist.txt in
openstack/requirements repo) are dropped from lower-constraints.txt
they are not needed during installation.

Change-Id: Ibc511139dca54a68b85b6d6731f491534fc1a909
2020-10-23 14:11:25 +08:00
Yasufumi Ogawa
8e2f5f854e Add py38 as a runtime in tox.ini
Drop py37 and add py38 for the latest python runtimes [1], although it
had to be updated in victoria.

[1] https://governance.openstack.org/tc/reference/runtimes/wallaby.html

Change-Id: I48642a703d6d1fe8be3055ee5a48eaafd0a18067
Signed-off-by: Yasufumi Ogawa <yasufum.o@gmail.com>
2020-10-19 07:21:18 +00:00
wangzihao
97f0903242 Remove six.moves.urllib
Remove six.moves.urllib replace with python3 urllib.

Change-Id: Ifd9d1dadfdc78ab7ef21bb648f71ca0e62830264
2020-09-23 19:33:29 +08:00
b69220634f Add Python3 wallaby unit tests
This is an automatically generated patch to ensure unit testing
is in place for all the of the tested runtimes for wallaby.

See also the PTI in governance [1].

[1]: https://governance.openstack.org/tc/reference/project-testing-interface.html

Change-Id: Id16db69d4df0115f3bc22d6d867727ff270e583f
2020-09-16 22:01:05 +00:00
e628157a7d Update master for stable/victoria
Add file to the reno documentation build to show release notes for
stable/victoria.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/victoria.

Change-Id: I8136cb5c1422833a95b38e4e0545863ceb9a287e
Sem-Ver: feature
2020-09-16 22:01:03 +00:00
31 changed files with 1210 additions and 201 deletions

View File

@@ -2,3 +2,4 @@
host=review.opendev.org
port=29418
project=openstack/python-tackerclient.git
defaultbranch=stable/wallaby

View File

@@ -1,7 +1,6 @@
- project:
templates:
- check-requirements
- openstack-lower-constraints-jobs
- openstack-python3-victoria-jobs
- openstack-python3-wallaby-jobs
- publish-openstack-docs-pti
- release-notes-jobs-python3

View File

@@ -71,8 +71,8 @@ htmlhelp_basename = 'tackerclientdoc'
# -- Options for manual page output -------------------------------------------
man_pages = [
('cli/index', 'tacker', u'Client for Tacker API',
[u'OpenStack Contributors'], 1),
('cli/index', 'tacker', 'Client for Tacker API',
['OpenStack Contributors'], 1),
]
# -- Options for openstackdocstheme -------------------------------------------

View File

@@ -11,59 +11,53 @@
License for the specific language governing permissions and limitations
under the License.
Convention for heading levels in Neutron devref:
======= Heading 0 (reserved for the title in a document)
------- Heading 1
~~~~~~~ Heading 2
+++++++ Heading 3
''''''' Heading 4
(Avoid deeper levels because they do not render well.)
============
Installation
============
**Note:** The paths we are using for configuration files in these steps
are with reference to Ubuntu Operating System. The paths may vary for
other Operating Systems.
This document describes how to install python-tackerclient.
The branch_name which is used in commands, specify the branch_name
as stable/<branch> for any stable branch installation. For eg:
stable/queens, stable/pike. If unspecified the default will be
master branch.
.. note::
This installation guide contents are specific to Ubuntu distro.
Using python install
====================
1. Clone python-tackerclient repository.
::
#. Clone python-tackerclient repository.
$ cd ~/
$ git clone https://github.com/openstack/python-tackerclient -b <branch_name>
You can use -b for specific release, optionally.
.. code-block:: console
2. Install python-tackerclient.
$ cd ~/
$ git clone https://opendev.org/openstack/python-tackerclient -b <branch_name>
::
.. note::
$ cd python-tackerclient
$ sudo python setup.py install
Make sure to replace the ``<branch_name>`` in command example with
specific branch name, such as ``stable/victoria``.
#. Install python-tackerclient.
.. code-block:: console
$ cd python-tackerclient
$ sudo python3 setup.py install
Using pip
=========
You can also install the latest version by using ``pip`` command:
::
$ pip install python-tackerclient
.. code-block:: console
$ pip3 install python-tackerclient
Or, if it is needed to install ``python-tackerclient`` from master branch,
type
::
.. code-block:: console
$ pip install git+https://github.com/openstack/python-tackerclient.git
$ pip3 install git+https://opendev.org/openstack/python-tackerclient

View File

@@ -1,62 +0,0 @@
appdirs==1.3.0
Babel==2.3.4
cliff==2.8.0
cmd2==0.8.0
coverage==4.0
ddt==1.0.1
debtcollector==1.2.0
decorator==3.4.0
deprecation==1.0
dogpile.cache==0.6.2
extras==1.0.0
fixtures==3.0.0
flake8==2.5.5
hacking==0.12.0
iso8601==0.1.11
jmespath==0.9.0
jsonpatch==1.16
jsonpointer==1.13
keystoneauth1==3.4.0
linecache2==1.0.0
mccabe==0.2.1
monotonic==0.6
msgpack-python==0.4.0
munch==2.1.0
netaddr==0.7.18
netifaces==0.10.4
openstacksdk==0.11.2
os-client-config==1.28.0
os-service-types==1.2.0
osc-lib==1.8.0
oslo.config==5.2.0
oslo.context==2.19.2
oslo.i18n==3.15.3
oslo.log==3.36.0
oslo.serialization==2.18.0
oslo.utils==3.40.0
pbr==2.0.0
pep8==1.5.7
positional==1.2.1
prettytable==0.7.2
pyflakes==0.8.1
pyinotify==0.9.6
pyparsing==2.1.0
pyperclip==1.5.27
python-dateutil==2.5.3
python-keystoneclient==3.8.0
python-mimeparse==1.6.0
python-subunit==1.0.0
pytz==2013.6
PyYAML==3.12
requests==2.14.2
requests-mock==1.2.0
requestsexceptions==1.2.0
rfc3986==0.3.1
simplejson==3.5.1
six==1.10.0
stestr==2.0.0
stevedore==1.20.0
testtools==2.2.0
traceback2==1.4.0
unittest2==1.1.0
wrapt==1.7.0

View File

@@ -53,8 +53,8 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'Tacker Client Release Notes'
copyright = u'2016, Tacker Developers'
project = 'Tacker Client Release Notes'
copyright = '2016, Tacker Developers'
# Release notes are version independent.
release = ''
@@ -190,8 +190,8 @@ latex_elements = {
# [howto/manual]).
latex_documents = [
('index', 'TackerClientReleaseNotes.tex',
u'Tacker Client Release Notes Documentation',
u'Tacker Developers', 'manual'),
'Tacker Client Release Notes Documentation',
'Tacker Developers', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -221,8 +221,8 @@ latex_documents = [
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'tackerreleasenotes',
u'Tacker Client Release Notes Documentation',
[u'Tacker Developers'], 1)
'Tacker Client Release Notes Documentation',
['Tacker Developers'], 1)
]
# If true, show URL addresses after external links.
@@ -236,8 +236,8 @@ man_pages = [
# dir menu entry, description, category)
texinfo_documents = [
('index', 'TackerClientReleaseNotes',
u'Tacker Client Release Notes Documentation',
u'Tacker Developers', 'TackerClientReleaseNotes',
'Tacker Client Release Notes Documentation',
'Tacker Developers', 'TackerClientReleaseNotes',
'Tacker Client Project.',
'Miscellaneous'),
]

View File

@@ -7,6 +7,7 @@ Contents:
:maxdepth: 2
unreleased
victoria
ussuri
train
stein

View File

@@ -0,0 +1,6 @@
=============================
Victoria Series Release Notes
=============================
.. release-notes::
:branch: stable/victoria

View File

@@ -8,7 +8,6 @@ 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.10.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

View File

@@ -94,4 +94,9 @@ openstack.tackerclient.v1 =
vnflcm_heal = tackerclient.osc.v1.vnflcm.vnflcm:HealVnfLcm
vnflcm_update = tackerclient.osc.v1.vnflcm.vnflcm:UpdateVnfLcm
vnflcm_scale = tackerclient.osc.v1.vnflcm.vnflcm:ScaleVnfLcm
vnflcm_change-ext-conn = tackerclient.osc.v1.vnflcm.vnflcm:ChangeExtConnVnfLcm
vnflcm_op_rollback = tackerclient.osc.v1.vnflcm.vnflcm_op_occs:RollbackVnfLcmOp
vnflcm_op_fail = tackerclient.osc.v1.vnflcm.vnflcm_op_occs:FailVnfLcmOp
vnflcm_op_retry = tackerclient.osc.v1.vnflcm.vnflcm_op_occs:RetryVnfLcmOp
vnflcm_op_list = tackerclient.osc.v1.vnflcm.vnflcm_op_occs:ListVnfLcmOp
vnflcm_op_show = tackerclient.osc.v1.vnflcm.vnflcm_op_occs:ShowVnfLcmOp

View File

@@ -28,7 +28,6 @@ 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"

View File

@@ -18,7 +18,6 @@ from xml.etree import ElementTree as etree
from xml.parsers import expat
from oslo_serialization import jsonutils
import six
from tackerclient.common import constants
from tackerclient.common import exceptions as exception
@@ -26,9 +25,6 @@ from tackerclient.i18n import _
LOG = logging.getLogger(__name__)
if six.PY3:
long = int
class ActionDispatcher(object):
"""Maps method name to local methods through action name."""
@@ -58,7 +54,7 @@ class JSONDictSerializer(DictSerializer):
def default(self, data):
def sanitizer(obj):
return six.text_type(obj)
return str(obj)
return jsonutils.dumps(data, default=sanitizer)
@@ -93,7 +89,7 @@ class XMLDictSerializer(DictSerializer):
root_key = constants.VIRTUAL_ROOT_KEY
root_value = None
else:
link_keys = [k for k in six.iterkeys(data) or []
link_keys = [k for k in data.keys() or []
if k.endswith('_links')]
if link_keys:
links = data.pop(link_keys[0], None)
@@ -183,10 +179,6 @@ class XMLDictSerializer(DictSerializer):
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,
@@ -194,7 +186,7 @@ class XMLDictSerializer(DictSerializer):
LOG.debug("Data %(data)s type is %(type)s",
{'data': data,
'type': type(data)})
result.text = six.text_type(data)
result.text = str(data)
return result
def _create_link_nodes(self, xml_doc, links):
@@ -323,8 +315,6 @@ class XMLDeserializer(TextDeserializer):
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:

View File

@@ -24,7 +24,6 @@ import os
from oslo_log import versionutils
from oslo_utils import encodeutils
from oslo_utils import importutils
import six
from tackerclient.common import exceptions
from tackerclient.i18n import _
@@ -141,7 +140,7 @@ def http_log_resp(_logger, resp, body):
def _safe_encode_without_obj(data):
if isinstance(data, six.string_types):
if isinstance(data, str):
return encodeutils.safe_encode(data)
return data

View File

@@ -217,7 +217,7 @@ class ShowTemplateNSD(command.ShowOne):
obj[_NSD]['attributes']['nsd'])
data = utils.get_item_properties(
sdk_utils.DictModel(obj[_NSD]),
(u'attributes',),
('attributes',),
formatters=_formatters)
data = (data or _('Unable to display NSD template!'))
return ((u'attributes',), data)
return (('attributes',), data)

View File

@@ -211,7 +211,7 @@ class ShowTemplateVNFFGD(command.ShowOne):
obj = client.show_vnffgd(obj_id)
data = utils.get_item_properties(
sdk_utils.DictModel(obj[_VNFFGD]),
(u'template',),
('template',),
formatters=_formatters)
data = (data or _('Unable to display VNFFGD template!'))
return ((u'template',), data)
return (('template',), data)

View File

@@ -0,0 +1,69 @@
{
"extVirtualLinks": [
{
"id": "ext-vl-uuid-VL1",
"resourceId": "neutron-network-uuid_VL1",
"extCps": [
{
"cpdId": "CP1",
"cpConfig": [
{
"cpProtocolData": [
{
"layerProtocol": "IP_OVER_ETHERNET",
"ipOverEthernet": {
"ipAddresses": [
{
"type": "IPV4",
"numDynamicAddresses": 1,
"subnetId": "subnet-uuid"
}
]
}
}
]
}
]
},
{
"cpdId": "CP2",
"cpConfig": [
{
"cpProtocolData": [
{
"layerProtocol": "IP_OVER_ETHERNET",
"ipOverEthernet": {
"ipAddresses": [
{
"type": "IPV4",
"fixedAddresses": [
"10.0.0.1"
],
"subnetId": "subnet-uuid"
}
]
}
}
]
}
]
}
]
}
],
"vimConnectionInfo": [
{
"id": "vim-uuid",
"vimType": "ETSINFV.OPENSTACK_KEYSTONE.v_2",
"vimConnectionId": "dummy-vimid",
"interfaceInfo": {
"key1":"value1",
"key2":"value2"
},
"accessInfo": {
"key1":"value1",
"key2":"value2"
}
}
]
}

View File

@@ -476,46 +476,51 @@ class ScaleVnfLcm(command.Command):
_VNF_INSTANCE,
metavar="<vnf-instance>",
help=_('VNF instance ID to scale'))
parser.add_argument(
'--I',
metavar="<param-file>",
help=_("Specify scale request parameters in a json file."))
parser.add_argument(
'--type',
metavar="<type>",
choices=['SCALE_OUT', 'SCALE_IN'],
help=_("Indicates the type of the scale operation requested"))
parser.add_argument(
'--aspect-id',
metavar="<aspect-id>",
help=_("Identifier of the scaling aspect."))
parser.add_argument(
'--number-of-steps',
metavar="<number-of-steps>",
type=int,
help=_("Number of scaling steps to be executed as part of"
help=_("Number of scaling steps to be executed as part of "
"this Scale VNF operation."))
parser.add_argument(
'--additional-param-file',
metavar="<additional-param-file>",
help=_("Additional parameters passed by the NFVO as input"
help=_("Additional parameters passed by the NFVO as input "
"to the scaling process."))
scale_require_parameters = parser.add_argument_group(
"require arguments"
)
scale_require_parameters.add_argument(
'--type',
metavar="<type>",
required=True,
choices=['SCALE_OUT', 'SCALE_IN'],
help=_("SCALE_OUT or SCALE_IN for type of scale operation."))
scale_require_parameters.add_argument(
'--aspect-id',
required=True,
metavar="<aspect-id>",
help=_("Identifier of the scaling aspect."))
return parser
def args2body(self, file_path=None):
def args2body(self, parsed_args):
"""To store request body, call jsonfile2body.
Args:
file_path ([string], optional): file path of param file(json).
Defaults to None.
parsed_args ([Namespace]): arguments of CLI.
Returns:
body[dict]: [description]
body ([dict]): Request body is stored
"""
body = {}
body = {'type': parsed_args.type, 'aspectId': parsed_args.aspect_id}
if file_path:
return jsonfile2body(file_path)
if parsed_args.number_of_steps:
body['numberOfSteps'] = parsed_args.number_of_steps
if parsed_args.additional_param_file:
body.update(jsonfile2body(parsed_args.additional_param_file))
return body
@@ -523,13 +528,39 @@ class ScaleVnfLcm(command.Command):
"""Execute scale_vnf_instance and output result comment.
Args:
parsed_args ([Namespace]): [description]
parsed_args ([Namespace]): arguments of CLI.
"""
client = self.app.client_manager.tackerclient
if parsed_args.additional_param_file:
result = client.scale_vnf_instance(
parsed_args.vnf_instance,
self.args2body(file_path=parsed_args.additional_param_file))
if not result:
print((_('Scale request for VNF Instance %(id)s has been'
' accepted.') % {'id': parsed_args.vnf_instance}))
result = client.scale_vnf_instance(
parsed_args.vnf_instance,
self.args2body(parsed_args))
if not result:
print((_('Scale request for VNF Instance %s has been accepted.')
% parsed_args.vnf_instance))
class ChangeExtConnVnfLcm(command.Command):
_description = _("Change External VNF Connectivity")
def get_parser(self, prog_name):
parser = super(ChangeExtConnVnfLcm, self).get_parser(prog_name)
parser.add_argument(
_VNF_INSTANCE,
metavar="<vnf-instance>",
help=_("VNF instance ID to Change External VNF Connectivity"))
parser.add_argument(
'request_file',
metavar="<param-file>",
help=_("Specify change-ext-conn request parameters "
"in a json file."))
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.tackerclient
result = client.change_ext_conn_vnf_instance(
parsed_args.vnf_instance, jsonfile2body(
parsed_args.request_file))
if not result:
print((_('Change External VNF Connectivity for VNF Instance %s '
'has been accepted.') % parsed_args.vnf_instance))

View File

@@ -11,7 +11,63 @@
# under the License.
from osc_lib.command import command
from osc_lib import utils
from tackerclient.i18n import _
from tackerclient.osc import sdk_utils
from tackerclient.osc import utils as tacker_osc_utils
_VNF_LCM_OP_OCC_ID = 'vnf_lcm_op_occ_id'
_MIXED_CASE_FIELDS = ['operationState', 'stateEnteredTime', 'startTime',
'vnfInstanceId', 'grantId', 'isAutomaticInvocation',
'isCancelPending', 'cancelMode', 'operationParams',
'resourceChanges', 'changedInfo',
'changedExtConnectivity']
_FORMATTERS = {
'operationParams': tacker_osc_utils.FormatComplexDataColumn,
'error': tacker_osc_utils.FormatComplexDataColumn,
'resourceChanges': tacker_osc_utils.FormatComplexDataColumn,
'changedInfo': tacker_osc_utils.FormatComplexDataColumn,
'changedExtConnectivity': tacker_osc_utils.FormatComplexDataColumn,
'_links': tacker_osc_utils.FormatComplexDataColumn
}
_ATTR_MAP = (
('id', 'id', tacker_osc_utils.LIST_BOTH),
('operationState', 'operationState', tacker_osc_utils.LIST_BOTH),
('vnfInstanceId', 'vnfInstanceId', tacker_osc_utils.LIST_BOTH),
('operation', 'operation', tacker_osc_utils.LIST_BOTH)
)
def _get_columns(vnflcm_op_occ_obj, action=None):
column_map = {
'id': 'ID',
'operationState': 'Operation State',
'stateEnteredTime': 'State Entered Time',
'startTime': 'Start Time',
'vnfInstanceId': 'VNF Instance ID',
'operation': 'Operation',
'isAutomaticInvocation': 'Is Automatic Invocation',
'isCancelPending': 'Is Cancel Pending',
'error': 'Error',
'_links': 'Links'
}
if action == 'show':
column_map.update(
{'operationParams': 'Operation Parameters',
'grantId': 'Grant ID',
'resourceChanges': 'Resource Changes',
'changedInfo': 'Changed Info',
'cancelMode': 'Cancel Mode',
'changedExtConnectivity': 'Changed External Connectivity'}
)
return sdk_utils.get_osc_show_columns_for_sdk_resource(vnflcm_op_occ_obj,
column_map)
class RollbackVnfLcmOp(command.Command):
@@ -26,7 +82,7 @@ class RollbackVnfLcmOp(command.Command):
"""
parser = super(RollbackVnfLcmOp, self).get_parser(prog_name)
parser.add_argument(
'vnf_lcm_op_occ_id',
_VNF_LCM_OP_OCC_ID,
metavar="<vnf-lcm-op-occ-id>",
help=_('VNF lifecycle management operation occurrence ID.'))
@@ -43,3 +99,208 @@ class RollbackVnfLcmOp(command.Command):
if not result:
print((_('Rollback request for LCM operation %(id)s has been'
' accepted') % {'id': parsed_args.vnf_lcm_op_occ_id}))
class FailVnfLcmOp(command.ShowOne):
_description = _("Fail VNF Instance")
def get_parser(self, prog_name):
"""Add arguments to parser.
Args:
prog_name ([type]): program name
Returns:
parser([ArgumentParser]):
"""
parser = super(FailVnfLcmOp, self).get_parser(prog_name)
parser.add_argument(
_VNF_LCM_OP_OCC_ID,
metavar="<vnf-lcm-op-occ-id>",
help=_('VNF lifecycle management operation occurrence ID.'))
return parser
def take_action(self, parsed_args):
"""Execute fail_vnf_instance and output response.
Args:
parsed_args ([Namespace]): arguments of CLI.
"""
client = self.app.client_manager.tackerclient
obj = client.fail_vnf_instance(parsed_args.vnf_lcm_op_occ_id)
display_columns, columns = _get_columns(obj)
data = utils.get_item_properties(
sdk_utils.DictModel(obj),
columns, formatters=_FORMATTERS,
mixed_case_fields=_MIXED_CASE_FIELDS)
return (display_columns, data)
class RetryVnfLcmOp(command.Command):
_description = _("Retry VNF Instance")
def get_parser(self, prog_name):
"""Add arguments to parser.
Args:
prog_name ([type]): program name
Returns:
parser([ArgumentParser]):
"""
parser = super(RetryVnfLcmOp, self).get_parser(prog_name)
parser.add_argument(
_VNF_LCM_OP_OCC_ID,
metavar="<vnf-lcm-op-occ-id>",
help=_('VNF lifecycle management operation occurrence ID.'))
return parser
def take_action(self, parsed_args):
"""Execute retry_vnf_instance and output comment.
Args:
parsed_args ([Namespace]): arguments of CLI.
"""
client = self.app.client_manager.tackerclient
result = client.retry_vnf_instance(parsed_args.vnf_lcm_op_occ_id)
if not result:
print((_('Retry request for LCM operation %(id)s has been'
' accepted') % {'id': parsed_args.vnf_lcm_op_occ_id}))
class ListVnfLcmOp(command.Lister):
_description = _("List LCM Operation Occurrences")
def get_parser(self, program_name):
"""Add arguments to parser.
Args:
program_name ([type]): program name
Returns:
parser([ArgumentParser]):
"""
parser = super(ListVnfLcmOp, self).get_parser(program_name)
parser.add_argument(
"--filter",
metavar="<filter>",
help=_("Attribute-based-filtering parameters"),
)
fields_exclusive_group = parser.add_mutually_exclusive_group(
required=False)
fields_exclusive_group.add_argument(
"--fields",
metavar="<fields>",
help=_("Complex attributes to be included into the response"),
)
fields_exclusive_group.add_argument(
"--exclude-fields",
metavar="<exclude-fields>",
help=_("Complex attributes to be excluded from the response"),
)
return parser
def get_attributes(self):
"""Get attributes.
Returns:
attributes([attributes]): a list of table entry definitions.
Each entry should be a tuple consisting of
(API attribute name, header name, listing mode).
"""
fields = [
{
"key": "id",
"value": "ID"
},
{
"key": "operationState",
"value": "Operation State"
},
{
"key": "vnfInstanceId",
"value": "VNF Instance ID"
},
{
"key": "operation",
"value": "Operation"
}
]
attributes = []
for field in fields:
attributes.extend([(field['key'], field['value'],
tacker_osc_utils.LIST_BOTH)])
return tuple(attributes)
def take_action(self, parsed_args):
"""Execute list_vnflcm_op_occs and output response.
Args:
parsed_args ([Namespace]): arguments of CLI.
"""
params = {}
exclude_fields = []
extra_fields = []
if parsed_args.filter:
params['filter'] = parsed_args.filter
if parsed_args.fields:
params['fields'] = parsed_args.fields
fields = parsed_args.fields.split(',')
for field in fields:
extra_fields.append(field.split('/')[0])
if parsed_args.exclude_fields:
params['exclude-fields'] = parsed_args.exclude_fields
fields = parsed_args.exclude_fields.split(',')
exclude_fields.extend(fields)
client = self.app.client_manager.tackerclient
vnflcm_op_occs = client.list_vnf_lcm_op_occs(**params)
headers, columns = tacker_osc_utils.get_column_definitions(
self.get_attributes(),
long_listing=True)
dictionary_properties = (utils.get_dict_properties(
s, columns, mixed_case_fields=_MIXED_CASE_FIELDS)
for s in vnflcm_op_occs
)
return (headers, dictionary_properties)
class ShowVnfLcmOp(command.ShowOne):
_description = _("Display Operation Occurrence details")
def get_parser(self, program_name):
"""Add arguments to parser.
Args:
program_name ([type]): program name
Returns:
parser([ArgumentParser]):
"""
parser = super(ShowVnfLcmOp, self).get_parser(program_name)
parser.add_argument(
_VNF_LCM_OP_OCC_ID,
metavar="<vnf-lcm-op-occ-id>",
help=_('VNF lifecycle management operation occurrence ID.'))
return parser
def take_action(self, parsed_args):
"""Execute show_vnf_lcm_op_occs and output response.
Args:
parsed_args ([Namespace]): arguments of CLI.
"""
client = self.app.client_manager.tackerclient
obj = client.show_vnf_lcm_op_occs(parsed_args.vnf_lcm_op_occ_id)
display_columns, columns = _get_columns(obj, action='show')
data = utils.get_item_properties(
sdk_utils.DictModel(obj),
columns, formatters=_FORMATTERS,
mixed_case_fields=_MIXED_CASE_FIELDS)
return (display_columns, data)

View File

@@ -218,7 +218,7 @@ class ShowTemplateVNFD(command.ShowOne):
obj[_VNFD]['attributes']['vnfd'])
data = utils.get_item_properties(
sdk_utils.DictModel(obj[_VNFD]),
(u'attributes',),
('attributes',),
formatters=_formatters)
data = (data or _('Unable to display VNFD template!'))
return ((u'attributes',), data)
return (('attributes',), data)

View File

@@ -434,9 +434,9 @@ class DownloadVnfPackageArtifact(command.Command):
parser.add_argument(
"--file",
metavar="<FILE>",
help=_("Local file to save downloaded VNF Package or VNFD data. "
"If this is not specified and there is no redirection "
"then data will not be saved.")
help=_("Local file to save downloaded VNF Package artifact "
"file data. If this is not specified and "
"there is no redirection then data will not be saved.")
)
return parser

View File

@@ -25,6 +25,10 @@ import itertools
import logging
import os
import sys
from urllib import parse as urlparse
from cliff import app
from cliff import commandmanager
from keystoneclient.auth.identity import v2 as v2_auth
from keystoneclient.auth.identity import v3 as v3_auth
@@ -32,10 +36,6 @@ 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 tackerclient.common import clientmanager
from tackerclient.common import command as openstack_command

View File

@@ -23,7 +23,6 @@ from cliff.formatters import table
from cliff import lister
from cliff import show
from oslo_serialization import jsonutils
import six
from tackerclient.common._i18n import _
from tackerclient.common import command
@@ -354,8 +353,7 @@ class TackerCommandMeta(abc.ABCMeta):
name, bases, cls_dict)
@six.add_metaclass(TackerCommandMeta)
class TackerCommand(command.OpenStackCommand):
class TackerCommand(command.OpenStackCommand, metaclass=TackerCommandMeta):
api = 'nfv-orchestration'
values_specs = []

View File

@@ -13,7 +13,8 @@
# 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 urllib import parse as urlparse
from tackerclient.common import exceptions

View File

@@ -20,7 +20,6 @@ from unittest import mock
import ddt
from oslo_utils.fixture import uuidsentinel
import six
from tackerclient.common import exceptions
from tackerclient.osc import utils as tacker_osc_utils
@@ -448,7 +447,7 @@ class TestTerminateVnfLcm(TestVnfLcm):
"delete vnf instance %(id)s"
% {'timeout': 15, 'id': vnf_instance['id']})
self.assertIn(expected_message, six.text_type(result))
self.assertIn(expected_message, str(result))
self.assertNotCalled(mock_delete)
def test_terminate_no_options(self):
@@ -652,6 +651,38 @@ class TestScaleVnfLcm(TestVnfLcm):
self.assertEqual(expected_message, actual_message)
@ddt.data('SCALE_IN', 'SCALE_OUT')
def test_take_action_no_param_file(self, scale_type):
vnf_instance = vnflcm_fakes.vnf_instance_response()
arglist = [vnf_instance['id'],
'--aspect-id', uuidsentinel.aspect_id,
'--number-of-steps', '1',
'--type', scale_type]
verifylist = [('vnf_instance', vnf_instance['id']),
('aspect_id', uuidsentinel.aspect_id),
('number_of_steps', 1),
('type', scale_type)]
parsed_args = self.check_parser(self.scale_vnf_lcm, arglist,
verifylist)
url = os.path.join(self.url, 'vnflcm/v1/vnf_instances',
vnf_instance['id'], 'scale')
self.requests_mock.register_uri(
'POST', url, headers=self.header, json={})
sys.stdout = buffer = StringIO()
self.scale_vnf_lcm.take_action(parsed_args)
actual_message = buffer.getvalue().strip()
expected_message = ("Scale request for VNF Instance %s has been "
"accepted.") % vnf_instance['id']
self.assertEqual(expected_message, actual_message)
@ddt.data('SCALE_IN', 'SCALE_OUT')
def test_take_action_param_file_not_exists(self, scale_type):
vnf_instance = vnflcm_fakes.vnf_instance_response()
@@ -708,3 +739,110 @@ class TestScaleVnfLcm(TestVnfLcm):
self.assertRaises(exceptions.TackerClientException,
self.scale_vnf_lcm.take_action,
parsed_args)
class TestChangeExtConnVnfLcm(TestVnfLcm):
def setUp(self):
super(TestChangeExtConnVnfLcm, self).setUp()
self.change_ext_conn_vnf_lcm = vnflcm.ChangeExtConnVnfLcm(
self.app, self.app_args,
cmd_name='vnflcm change-ext-conn')
def test_take_action(self):
vnf_instance = vnflcm_fakes.vnf_instance_response()
sample_param_file = ("./tackerclient/osc/v1/vnflcm/samples/"
"change_ext_conn_vnf_instance_param_sample.json")
arglist = [vnf_instance['id'], sample_param_file]
verifylist = [('vnf_instance', vnf_instance['id']),
('request_file', sample_param_file)]
# command param
parsed_args = self.check_parser(self.change_ext_conn_vnf_lcm,
arglist,
verifylist)
url = os.path.join(self.url, 'vnflcm/v1/vnf_instances',
vnf_instance['id'], 'change_ext_conn')
self.requests_mock.register_uri(
'POST', url, headers=self.header, json={})
sys.stdout = buffer = StringIO()
with mock.patch.object(proxy_client.ClientBase,
'_handle_fault_response') as m:
self.change_ext_conn_vnf_lcm.take_action(parsed_args)
# check no fault response is received
self.assertNotCalled(m)
self.assertEqual(
('Change External VNF Connectivity for VNF Instance {0} '
'has been accepted.'.format(vnf_instance['id'])),
buffer.getvalue().strip())
def test_take_action_vnf_instance_not_found(self):
vnf_instance = vnflcm_fakes.vnf_instance_response()
sample_param_file = ("./tackerclient/osc/v1/vnflcm/samples/"
"change_ext_conn_vnf_instance_param_sample.json")
arglist = [vnf_instance['id'], sample_param_file]
verifylist = [('vnf_instance', vnf_instance['id']),
('request_file', sample_param_file)]
# command param
parsed_args = self.check_parser(self.change_ext_conn_vnf_lcm,
arglist,
verifylist)
url = os.path.join(self.url, 'vnflcm/v1/vnf_instances',
vnf_instance['id'], 'change_ext_conn')
self.requests_mock.register_uri(
'POST', url, headers=self.header, status_code=404, json={})
self.assertRaises(exceptions.TackerClientException,
self.change_ext_conn_vnf_lcm.take_action,
parsed_args)
def test_take_action_param_file_not_exists(self):
vnf_instance = vnflcm_fakes.vnf_instance_response()
sample_param_file = "./not_exists.json"
arglist = [vnf_instance['id'], sample_param_file]
verifylist = [('vnf_instance', vnf_instance['id']),
('request_file', sample_param_file)]
# command param
parsed_args = self.check_parser(
self.change_ext_conn_vnf_lcm,
arglist,
verifylist)
ex = self.assertRaises(
exceptions.InvalidInput,
self.change_ext_conn_vnf_lcm.take_action,
parsed_args)
expected_msg = ("Invalid input: File %s does not exist "
"or user does not have read privileges to it")
self.assertEqual(expected_msg % sample_param_file, str(ex))
@mock.patch("os.open")
@mock.patch("os.access")
def test_take_action_invalid_format_param_file(self, mock_open,
mock_access):
vnf_instance = vnflcm_fakes.vnf_instance_response()
sample_param_file = "./invalid_param_file.json"
arglist = [vnf_instance['id'], sample_param_file]
verifylist = [('vnf_instance', vnf_instance['id']),
('request_file', sample_param_file)]
mock_open.return_value = "invalid_json_data"
# command param
parsed_args = self.check_parser(self.change_ext_conn_vnf_lcm,
arglist,
verifylist)
ex = self.assertRaises(
exceptions.InvalidInput,
self.change_ext_conn_vnf_lcm.take_action,
parsed_args)
expected_msg = "Failed to load parameter file."
self.assertIn(expected_msg, str(ex))

View File

@@ -18,9 +18,30 @@ from oslo_utils.fixture import uuidsentinel
from unittest import mock
from tackerclient.common import exceptions
from tackerclient.osc import utils as tacker_osc_utils
from tackerclient.osc.v1.vnflcm import vnflcm_op_occs
from tackerclient.tests.unit.osc import base
from tackerclient.tests.unit.osc.v1.fixture_data import client
from tackerclient.tests.unit.osc.v1 import vnflcm_op_occs_fakes
def _get_columns_vnflcm_op_occs(action='show'):
if action == 'fail':
return ['ID', 'Operation State', 'State Entered Time',
'Start Time', 'VNF Instance ID', 'Operation',
'Is Automatic Invocation', 'Is Cancel Pending',
'Error', 'Links']
elif action == 'list':
return ['ID', 'Operation State', 'VNF Instance ID',
'Operation']
else:
return ['ID', 'Operation State', 'State Entered Time',
'Start Time', 'VNF Instance ID', 'Grant ID',
'Operation', 'Is Automatic Invocation',
'Operation Parameters', 'Is Cancel Pending',
'Cancel Mode', 'Error', 'Resource Changes',
'Changed Info', 'Changed External Connectivity', 'Links']
class TestVnfLcm(base.FixturedTestCase):
@@ -92,3 +113,391 @@ class TestRollbackVnfLcmOp(TestVnfLcm):
self.assertRaises(exceptions.TackerClientException,
self.rollback_vnf_lcm.take_action,
parsed_args)
class TestFailVnfLcmOp(TestVnfLcm):
def setUp(self):
super(TestFailVnfLcmOp, self).setUp()
self.fail_vnf_lcm = vnflcm_op_occs.FailVnfLcmOp(
self.app, self.app_args, cmd_name='vnflcm op fail')
def test_take_action(self):
"""Test of take_action()"""
vnflcm_op_occ = vnflcm_op_occs_fakes.vnflcm_op_occ_response(
action='fail')
arg_list = [vnflcm_op_occ['id']]
verify_list = [('vnf_lcm_op_occ_id', vnflcm_op_occ['id'])]
# command param
parsed_args = self.check_parser(
self.fail_vnf_lcm, arg_list, verify_list)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
vnflcm_op_occ['id'],
'fail')
self.requests_mock.register_uri(
'POST', url, headers=self.header, json=vnflcm_op_occ)
columns, data = (self.fail_vnf_lcm.take_action(parsed_args))
expected_columns = _get_columns_vnflcm_op_occs(action='fail')
self.assertCountEqual(expected_columns, columns)
def test_take_action_vnf_lcm_op_occ_id_not_found(self):
"""Test if vnf-lcm-op-occ-id does not find"""
arg_list = [uuidsentinel.vnf_lcm_op_occ_id]
verify_list = [('vnf_lcm_op_occ_id', uuidsentinel.vnf_lcm_op_occ_id)]
# command param
parsed_args = self.check_parser(
self.fail_vnf_lcm, arg_list, verify_list)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
uuidsentinel.vnf_lcm_op_occ_id,
'fail')
self.requests_mock.register_uri(
'POST', url, headers=self.header, status_code=404, json={})
self.assertRaises(exceptions.TackerClientException,
self.fail_vnf_lcm.take_action,
parsed_args)
def test_take_action_vnf_lcm_op_occ_state_is_conflict(self):
"""Test if vnf-lcm-op-occ state is conflict"""
arg_list = [uuidsentinel.vnf_lcm_op_occ_id]
verify_list = [('vnf_lcm_op_occ_id', uuidsentinel.vnf_lcm_op_occ_id)]
# command param
parsed_args = self.check_parser(
self.fail_vnf_lcm, arg_list, verify_list)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
uuidsentinel.vnf_lcm_op_occ_id,
'fail')
self.requests_mock.register_uri(
'POST', url, headers=self.header, status_code=409, json={})
self.assertRaises(exceptions.TackerClientException,
self.fail_vnf_lcm.take_action,
parsed_args)
def test_take_action_vnf_lcm_op_occ_internal_server_error(self):
"""Test if request is internal server error"""
arg_list = [uuidsentinel.vnf_lcm_op_occ_id]
verify_list = [('vnf_lcm_op_occ_id', uuidsentinel.vnf_lcm_op_occ_id)]
# command param
parsed_args = self.check_parser(
self.fail_vnf_lcm, arg_list, verify_list)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
uuidsentinel.vnf_lcm_op_occ_id,
'fail')
self.requests_mock.register_uri(
'POST', url, headers=self.header, status_code=500, json={})
self.assertRaises(exceptions.TackerClientException,
self.fail_vnf_lcm.take_action,
parsed_args)
def test_take_action_vnf_lcm_op_occ_missing_vnf_lcm_op_occ_id_argument(
self):
"""Test if vnflcm_op_occ_id is not provided"""
arg_list = []
verify_list = [('vnf_lcm_op_occ_id', arg_list)]
self.assertRaises(base.ParserException, self.check_parser,
self.fail_vnf_lcm, arg_list, verify_list)
class TestRetryVnfLcmOp(TestVnfLcm):
def setUp(self):
super(TestRetryVnfLcmOp, self).setUp()
self.retry_vnf_lcm = vnflcm_op_occs.RetryVnfLcmOp(
self.app, self.app_args, cmd_name='vnflcm op retry')
def test_take_action(self):
"""Test of take_action()"""
arg_list = [uuidsentinel.vnf_lcm_op_occ_id]
verify_list = [('vnf_lcm_op_occ_id', uuidsentinel.vnf_lcm_op_occ_id)]
# command param
parsed_args = self.check_parser(
self.retry_vnf_lcm, arg_list, verify_list)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
uuidsentinel.vnf_lcm_op_occ_id,
'retry')
self.requests_mock.register_uri(
'POST', url, headers=self.header, json={})
sys.stdout = buffer = StringIO()
self.retry_vnf_lcm.take_action(parsed_args)
actual_message = buffer.getvalue().strip()
expected_message = (
'Retry request for LCM operation ' +
uuidsentinel.vnf_lcm_op_occ_id +
' has been accepted')
self.assertEqual(expected_message, actual_message)
def test_take_action_vnf_lcm_op_occ_id_not_found(self):
"""Test if vnf-lcm-op-occ-id is not found."""
arglist = [uuidsentinel.vnf_lcm_op_occ_id]
verifylist = [('vnf_lcm_op_occ_id', uuidsentinel.vnf_lcm_op_occ_id)]
# command param
parsed_args = self.check_parser(
self.retry_vnf_lcm, arglist, verifylist)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
uuidsentinel.vnf_lcm_op_occ_id,
'retry')
self.requests_mock.register_uri(
'POST', url, headers=self.header, status_code=404, json={})
self.assertRaises(exceptions.TackerClientException,
self.retry_vnf_lcm.take_action,
parsed_args)
def test_take_action_vnf_lcm_op_occ_state_is_conflict(self):
"""Test if vnf-lcm-op-occ state is conflict"""
arg_list = [uuidsentinel.vnf_lcm_op_occ_id]
verify_list = [('vnf_lcm_op_occ_id', uuidsentinel.vnf_lcm_op_occ_id)]
# command param
parsed_args = self.check_parser(
self.retry_vnf_lcm, arg_list, verify_list)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
uuidsentinel.vnf_lcm_op_occ_id,
'retry')
self.requests_mock.register_uri(
'POST', url, headers=self.header, status_code=409, json={})
self.assertRaises(exceptions.TackerClientException,
self.retry_vnf_lcm.take_action,
parsed_args)
def test_take_action_vnf_lcm_op_occ_internal_server_error(self):
"""Test if request is internal server error"""
arg_list = [uuidsentinel.vnf_lcm_op_occ_id]
verify_list = [('vnf_lcm_op_occ_id', uuidsentinel.vnf_lcm_op_occ_id)]
# command param
parsed_args = self.check_parser(
self.retry_vnf_lcm, arg_list, verify_list)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
uuidsentinel.vnf_lcm_op_occ_id,
'retry')
self.requests_mock.register_uri(
'POST', url, headers=self.header, status_code=500, json={})
self.assertRaises(exceptions.TackerClientException,
self.retry_vnf_lcm.take_action,
parsed_args)
def test_take_action_vnf_lcm_op_occ_missing_vnf_lcm_op_occ_id_argument(
self):
"""Test if vnflcm_op_occ_id is not provided"""
arg_list = []
verify_list = [('vnf_lcm_op_occ_id', arg_list)]
self.assertRaises(base.ParserException, self.check_parser,
self.retry_vnf_lcm, arg_list, verify_list)
class TestListVnfLcmOp(TestVnfLcm):
def setUp(self):
super(TestListVnfLcmOp, self).setUp()
self.list_vnflcm_op_occ = vnflcm_op_occs.ListVnfLcmOp(
self.app, self.app_args, cmd_name='vnflcm op list')
def test_take_action(self):
vnflcm_op_occs_obj = vnflcm_op_occs_fakes.create_vnflcm_op_occs(
count=3)
parsed_args = self.check_parser(self.list_vnflcm_op_occ, [], [])
self.requests_mock.register_uri(
'GET', os.path.join(self.url,
'vnflcm/v1/vnf_lcm_op_occs'),
json=vnflcm_op_occs_obj, headers=self.header)
actual_columns, data = self.list_vnflcm_op_occ.take_action(parsed_args)
headers, columns = tacker_osc_utils.get_column_definitions(
self.list_vnflcm_op_occ.get_attributes(), long_listing=True)
expected_data = []
for vnflcm_op_occ_obj_idx in vnflcm_op_occs_obj:
expected_data.append(vnflcm_op_occs_fakes.get_vnflcm_op_occ_data(
vnflcm_op_occ_obj_idx, columns=columns))
self.assertCountEqual(_get_columns_vnflcm_op_occs(action='list'),
actual_columns)
self.assertCountEqual(expected_data, list(data))
def test_take_action_with_filter(self):
vnflcm_op_occs_obj = vnflcm_op_occs_fakes.create_vnflcm_op_occs(
count=3)
parsed_args = self.check_parser(
self.list_vnflcm_op_occ,
["--filter", '(eq,operationState,STARTING)'],
[('filter', '(eq,operationState,STARTING)')])
self.requests_mock.register_uri(
'GET', os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs?'
'filter=(eq,operationState,STARTING)'),
json=vnflcm_op_occs_obj, headers=self.header)
actual_columns, data = self.list_vnflcm_op_occ.take_action(parsed_args)
headers, columns = tacker_osc_utils.get_column_definitions(
self.list_vnflcm_op_occ.get_attributes(), long_listing=True)
expected_data = []
for vnflcm_op_occ_obj_idx in vnflcm_op_occs_obj:
expected_data.append(vnflcm_op_occs_fakes.get_vnflcm_op_occ_data(
vnflcm_op_occ_obj_idx, columns=columns))
self.assertCountEqual(_get_columns_vnflcm_op_occs(action='list'),
actual_columns)
self.assertListItemsEqual(expected_data, list(data))
def test_take_action_with_incorrect_filter(self):
parsed_args = self.check_parser(
self.list_vnflcm_op_occ,
["--filter", '(operationState)'],
[('filter', '(operationState)')])
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs?filter=(operationState)')
self.requests_mock.register_uri(
'POST', url, headers=self.header, status_code=400, json={})
self.assertRaises(exceptions.TackerClientException,
self.list_vnflcm_op_occ.take_action,
parsed_args)
def test_take_action_internal_server_error(self):
parsed_args = self.check_parser(
self.list_vnflcm_op_occ,
["--filter", '(eq,operationState,STARTING)'],
[('filter', '(eq,operationState,STARTING)')])
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs?'
'filter=(eq,operationState,STARTING)')
self.requests_mock.register_uri(
'POST', url, headers=self.header, status_code=500, json={})
self.assertRaises(exceptions.TackerClientException,
self.list_vnflcm_op_occ.take_action,
parsed_args)
class TestShowVnfLcmOp(TestVnfLcm):
def setUp(self):
super(TestShowVnfLcmOp, self).setUp()
self.show_vnf_lcm_op_occs = vnflcm_op_occs.ShowVnfLcmOp(
self.app, self.app_args, cmd_name='vnflcm op show')
def test_take_action(self):
"""Test of take_action()"""
vnflcm_op_occ = vnflcm_op_occs_fakes.vnflcm_op_occ_response()
arglist = [vnflcm_op_occ['id']]
verifylist = [('vnf_lcm_op_occ_id', vnflcm_op_occ['id'])]
# command param
parsed_args = self.check_parser(
self.show_vnf_lcm_op_occs, arglist, verifylist)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
vnflcm_op_occ['id'])
self.requests_mock.register_uri(
'GET', url, headers=self.header, json=vnflcm_op_occ)
columns, data = (self.show_vnf_lcm_op_occs.take_action(parsed_args))
self.assertCountEqual(_get_columns_vnflcm_op_occs(),
columns)
def test_take_action_vnf_lcm_op_occ_id_not_found(self):
"""Test if vnf-lcm-op-occ-id does not find."""
arglist = [uuidsentinel.vnf_lcm_op_occ_id]
verifylist = [('vnf_lcm_op_occ_id', uuidsentinel.vnf_lcm_op_occ_id)]
# command param
parsed_args = self.check_parser(
self.show_vnf_lcm_op_occs, arglist, verifylist)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
uuidsentinel.vnf_lcm_op_occ_id)
self.requests_mock.register_uri(
'GET', url, headers=self.header, status_code=404, json={})
self.assertRaises(exceptions.TackerClientException,
self.show_vnf_lcm_op_occs.take_action,
parsed_args)
def test_take_action_internal_server_error(self):
"""Test for internal server error."""
arglist = [uuidsentinel.vnf_lcm_op_occ_id]
verifylist = [('vnf_lcm_op_occ_id', uuidsentinel.vnf_lcm_op_occ_id)]
# command param
parsed_args = self.check_parser(
self.show_vnf_lcm_op_occs, arglist, verifylist)
url = os.path.join(
self.url,
'vnflcm/v1/vnf_lcm_op_occs',
uuidsentinel.vnf_lcm_op_occ_id)
self.requests_mock.register_uri(
'GET', url, headers=self.header, status_code=500, json={})
self.assertRaises(exceptions.TackerClientException,
self.show_vnf_lcm_op_occs.take_action,
parsed_args)

View File

@@ -0,0 +1,107 @@
# 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 oslo_utils.fixture import uuidsentinel
from oslo_utils import uuidutils
from tackerclient.osc import utils as tacker_osc_utils
def vnflcm_op_occ_response(attrs=None, action=''):
"""Create a fake vnflcm op occurrence.
:param Dictionary attrs:
A dictionary with all attributes
:return:
A vnf lcm op occs dict
"""
attrs = attrs or {}
# Set default attributes.
dummy_vnf_lcm_op_occ = {
"id": uuidsentinel.vnflcm_op_occ_id,
"operationState": "STARTING",
"stateEnteredTime": "2018-12-22T16:59:45.187Z",
"startTime": "2018-12-22T16:59:45.187Z",
"vnfInstanceId": "376f37f3-d4e9-4d41-8e6a-9b0ec98695cc",
"grantId": "",
"operation": "INSTANTIATE",
"isAutomaticInvocation": "true",
"operationParams": {
"flavourId": "default",
"instantiationLevelId": "n-mme-min"
},
"isCancelPending": "true",
"cancelMode": "",
"error": {
"status": "500",
"detail": "internal server error"
},
"resourceChanges": [],
"changedInfo": [],
"changedExtConnectivity": [],
"_links": {
"self": ""
}
}
if action == 'fail':
fail_not_needed_columns = [
'grantId', 'operationParams',
'cancelMode', 'resourceChanges', 'changedInfo',
'changedExtConnectivity']
for key in fail_not_needed_columns:
del dummy_vnf_lcm_op_occ[key]
# Overwrite default attributes.
dummy_vnf_lcm_op_occ.update(attrs)
return dummy_vnf_lcm_op_occ
def get_vnflcm_op_occ_data(vnf_lcm_op_occ, columns=None):
"""Get the vnflcm op occurrence.
:return:
A tuple object sorted based on the name of the columns.
"""
complex_attributes = [
'operationParams', 'error', 'resourceChanges',
'changedInfo', 'changedExtConnectivity', 'links']
for attribute in complex_attributes:
if vnf_lcm_op_occ.get(attribute):
vnf_lcm_op_occ.update(
{attribute: tacker_osc_utils.FormatComplexDataColumn(
vnf_lcm_op_occ[attribute])})
# return the list of data as per column order
if columns:
return tuple([vnf_lcm_op_occ[key] for key in columns])
return tuple([vnf_lcm_op_occ[key] for key in sorted(
vnf_lcm_op_occ.keys())])
def create_vnflcm_op_occs(count=2):
"""Create multiple fake vnflcm op occs.
:param count: The number of vnflcm op occs to fake
:return:
A list of fake vnflcm op occs dictionary
"""
vnflcm_op_occs = []
for i in range(0, count):
unique_id = uuidutils.generate_uuid()
vnflcm_op_occs.append(vnflcm_op_occ_response(attrs={'id': unique_id}))
return vnflcm_op_occs

View File

@@ -14,15 +14,14 @@
# under the License.
#
from unittest import mock
import urllib
import contextlib
import fixtures
import six
import six.moves.urllib.parse as urlparse
import io
import sys
import testtools
from unittest import mock
import urllib
from urllib import parse as urlparse
from tackerclient.common import constants
from tackerclient.common import exceptions
@@ -40,7 +39,7 @@ ENDURL = 'localurl'
@contextlib.contextmanager
def capture_std_streams():
fake_stdout, fake_stderr = six.StringIO(), six.StringIO()
fake_stdout, fake_stderr = io.StringIO(), io.StringIO()
stdout, stderr = sys.stdout, sys.stderr
try:
sys.stdout, sys.stderr = fake_stdout, fake_stderr
@@ -317,7 +316,7 @@ class CLITestV10Base(testtools.TestCase):
args.append("--tag")
for tag in tags:
args.append(tag)
if isinstance(tag, six.string_types):
if isinstance(tag, str):
tag = urllib.quote(tag.encode('utf-8'))
if query:
query += "&tag=" + tag
@@ -414,7 +413,7 @@ class CLITestV10Base(testtools.TestCase):
args.append("--tag")
for tag in tags:
args.append(tag)
if isinstance(tag, six.string_types):
if isinstance(tag, str):
tag = urllib.quote(tag.encode('utf-8'))
if query:
query += "&tag=" + tag
@@ -654,8 +653,8 @@ class CLITestV10Base(testtools.TestCase):
class ClientV1TestJson(CLITestV10Base):
def test_do_request_unicode(self):
self.client.format = self.format
unicode_text = u'\u7f51\u7edc'
action = u'/test'
unicode_text = '\u7f51\u7edc'
action = '/test'
params = {'test': unicode_text}
body = params
expect_body = self.client.serialize(body)
@@ -664,7 +663,7 @@ class ClientV1TestJson(CLITestV10Base):
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'
expected_uri = '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,
@@ -799,3 +798,27 @@ class CLITestV10ExceptionHandler(CLITestV10Base):
exceptions.TackerClientException, 500,
expected_msg=expected_msg,
error_content=error_content)
def test_exception_handler_v10_tacker_etsi_error(self):
"""Test ETSI error response"""
known_error_map = [
({
"status": "status 1",
"detail": "sample 1"
}, 400),
({
"status": "status 2",
"detail": "sample 2"
}, 404),
({
"status": "status 3",
"detail": "sample 3"
}, 409)
]
for error_content, status_code in known_error_map:
self._test_exception_handler_v10(
exceptions.TackerClientException, status_code,
expected_msg=error_content['detail'],
error_content=error_content)

View File

@@ -14,18 +14,17 @@
# under the License.
import argparse
import fixtures
import io
import logging
import os
import re
from unittest import mock
import six
import sys
import fixtures
from keystoneclient import session
import testtools
from testtools import matchers
from unittest import mock
from keystoneclient import session
from tackerclient.common import clientmanager
from tackerclient import shell as openstack_shell
@@ -63,8 +62,8 @@ class ShellTest(testtools.TestCase):
clean_env = {}
_old_env, os.environ = os.environ, clean_env.copy()
try:
sys.stdout = six.StringIO()
sys.stderr = six.StringIO()
sys.stdout = io.StringIO()
sys.stderr = io.StringIO()
_shell = openstack_shell.TackerShell(DEFAULT_API_VERSION)
_shell.run(argstr.split())
except SystemExit:
@@ -155,10 +154,10 @@ class ShellTest(testtools.TestCase):
@mock.patch.object(openstack_shell.TackerShell, 'run')
def test_main_with_unicode(self, mock_run):
mock_run.return_value = 0
unicode_text = u'\u7f51\u7edc'
unicode_text = '\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,
mock_run.assert_called_once_with(['net-list', unicode_text,
unicode_text])
self.assertEqual(0, ret)

View File

@@ -19,7 +19,7 @@ import logging
import time
import requests
import six.moves.urllib.parse as urlparse
from urllib import parse as urlparse
from tackerclient import client
from tackerclient.common import constants
@@ -54,6 +54,7 @@ def exception_handler_v10(status_code, error_content):
:param status_code: HTTP error status code
:param error_content: deserialized body of error response
"""
etsi_error_content = error_content
error_dict = None
if isinstance(error_content, dict):
error_dict = error_content.get('TackerError')
@@ -94,6 +95,13 @@ def exception_handler_v10(status_code, error_content):
if message:
raise exceptions.TackerClientException(status_code=status_code,
message=message)
# ETSI error response
if isinstance(etsi_error_content, dict):
if etsi_error_content.get('status') and \
etsi_error_content.get('detail'):
message = etsi_error_content.get('detail')
raise exceptions.TackerClientException(status_code=status_code,
message=message)
# If we end up here the exception was not a tacker error
msg = "%s-%s" % (status_code, error_content)
@@ -868,6 +876,7 @@ class VnfLCMClient(ClientBase):
vnf_instances_path = '/vnflcm/v1/vnf_instances'
vnf_instance_path = '/vnflcm/v1/vnf_instances/%s'
vnf_lcm_op_occurrences_path = '/vnflcm/v1/vnf_lcm_op_occs'
vnf_lcm_op_occs_path = '/vnflcm/v1/vnf_lcm_op_occs/%s'
def build_action(self, action):
@@ -919,6 +928,29 @@ class VnfLCMClient(ClientBase):
def rollback_vnf_instance(self, occ_id):
return self.post((self.vnf_lcm_op_occs_path + "/rollback") % occ_id)
@APIParamsCall
def fail_vnf_instance(self, occ_id):
return self.post((self.vnf_lcm_op_occs_path + "/fail") % occ_id)
@APIParamsCall
def change_ext_conn_vnf_instance(self, vnf_id, body):
return self.post((self.vnf_instance_path + "/change_ext_conn") %
vnf_id, body=body)
@APIParamsCall
def retry_vnf_instance(self, occ_id):
return self.post((self.vnf_lcm_op_occs_path + "/retry") % occ_id)
@APIParamsCall
def list_vnf_lcm_op_occs(self, retrieve_all=True, **_params):
vnf_lcm_op_occs = self.list(None, self.vnf_lcm_op_occurrences_path,
retrieve_all, **_params)
return vnf_lcm_op_occs
@APIParamsCall
def show_vnf_lcm_op_occs(self, occ_id):
return self.get(self.vnf_lcm_op_occs_path % occ_id)
class Client(object):
"""Unified interface to interact with multiple applications of tacker service.
@@ -1192,6 +1224,9 @@ class Client(object):
def scale_vnf_instance(self, vnf_id, body):
return self.vnf_lcm_client.scale_vnf_instance(vnf_id, body)
def change_ext_conn_vnf_instance(self, vnf_id, body):
return self.vnf_lcm_client.change_ext_conn_vnf_instance(vnf_id, body)
def delete_vnf_instance(self, vnf_id):
return self.vnf_lcm_client.delete_vnf_instance(vnf_id)
@@ -1201,6 +1236,12 @@ class Client(object):
def rollback_vnf_instance(self, occ_id):
return self.vnf_lcm_client.rollback_vnf_instance(occ_id)
def fail_vnf_instance(self, occ_id):
return self.vnf_lcm_client.fail_vnf_instance(occ_id)
def retry_vnf_instance(self, occ_id):
return self.vnf_lcm_client.retry_vnf_instance(occ_id)
def update_vnf_package(self, vnf_package, body):
return self.vnf_package_client.update_vnf_package(vnf_package, body)
@@ -1215,3 +1256,10 @@ class Client(object):
def download_vnf_package(self, vnf_package):
return self.vnf_package_client.download_vnf_package(vnf_package)
def list_vnf_lcm_op_occs(self, retrieve_all=True, **_params):
return self.vnf_lcm_client.list_vnf_lcm_op_occs(
retrieve_all=retrieve_all, **_params)
def show_vnf_lcm_op_occs(self, occ_id):
return self.vnf_lcm_client.show_vnf_lcm_op_occs(occ_id)

View File

@@ -2,7 +2,7 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking>=3.0.1,<3.1.0 # Apache-2.0
hacking>=4.0.0,<4.1.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
ddt>=1.0.1 # MIT
fixtures>=3.0.0 # Apache-2.0/BSD

12
tox.ini
View File

@@ -1,5 +1,5 @@
[tox]
envlist = py37,py36,pep8,docs
envlist = py38,py36,pep8,docs
minversion = 3.1.1
skipsdist = True
ignore_basepython_conflict = True
@@ -13,7 +13,7 @@ setenv = VIRTUAL_ENV={envdir}
usedevelop = True
install_command = pip install {opts} {packages}
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/wallaby}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
@@ -31,7 +31,7 @@ commands = sphinx-build -W -b html doc/source doc/build/html
[testenv:releasenotes]
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/wallaby}
-r{toxinidir}/doc/requirements.txt
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
@@ -53,9 +53,3 @@ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools
# F821 undefined name 'unicode'
# if isinstance(config, str) or isinstance(config, unicode):
builtins = unicode
[testenv:lower-constraints]
deps =
-c{toxinidir}/lower-constraints.txt
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt