diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..94e40afbb --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = neutronclient +omit = neutronclient/tests/* + +[report] +ignore_errors = True diff --git a/.gitignore b/.gitignore index 2fc28f418..abdf7b022 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,22 @@ *.pyc *.DS_Store +*.egg +*.sw? +AUTHORS +ChangeLog build/* build-stamp -.coverage cover/* -python_quantumclient.egg-info/* -quantum/vcsversion.py +doc/build/ +doc/source/api/ +releasenotes/build/ +python_neutronclient.egg-info/* +neutron/vcsversion.py +neutronclient/versioninfo run_tests.err.log run_tests.log -.venv/ +.autogenerated +.coverage +.stestr/ .tox/ +.venv/ diff --git a/.gitreview b/.gitreview index 40817febd..faf5d1f5b 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] -host=review.openstack.org +host=review.opendev.org port=29418 -project=openstack/python-quantumclient.git +project=openstack/python-neutronclient.git diff --git a/.pylintrc b/.pylintrc index bd677fd6a..7e4a0716c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -20,7 +20,7 @@ variable-rgx=[a-z_][a-z0-9_]{0,30}$ argument-rgx=[a-z_][a-z0-9_]{1,30}$ # Method names should be at least 3 characters long -# and be lowecased with underscores +# and be lowercased with underscores method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$ # Module names matching quantum-* are ok (files in bin/) diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 000000000..875bb1461 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=${OS_TEST_PATH:-./neutronclient/tests/unit} +top_dir=./ diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 000000000..7c8822186 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,39 @@ +- project: + templates: + - openstack-cover-jobs + - openstack-python3-jobs + - publish-openstack-docs-pti + - check-requirements + - lib-forward-testing-python3 + - release-notes-jobs-python3 + - openstackclient-plugin-jobs + experimental: + jobs: + - neutronclient-grenade-neutron-lib: + irrelevant-files: + - ^(test-|)requirements.txt$ + - ^setup.cfg$ + +- job: + name: neutronclient-grenade-neutron-lib + parent: grenade + description: | + neutron-lib grenade job. + The version of this job on the current branch is py3 based, + while any branch before ussuri needs to use the py2 version, + which is defined in openstack-zuul-jobs with the old name + (legacy-grenade-dsvm-neutron-libs). + Users of this job needs to pay attention of the version used. + Former names for this job were: + * legacy-grenade-dsvm-neutron-libs + * neutron-lib-grenade-dsvm + required-projects: + - openstack/keystoneauth + - openstack/neutron + - openstack/neutron-lib + - openstack/python-cinderclient + - openstack/python-glanceclient + - openstack/python-ironicclient + - openstack/python-keystoneclient + - openstack/python-neutronclient + - openstack/python-novaclient diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..b726f1d6b --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,16 @@ +If you would like to contribute to the development of OpenStack, +you must follow the steps documented at: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Once those steps have been completed, changes to OpenStack +should be submitted for review via the Gerrit tool, following +the workflow documented at: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/python-neutronclient diff --git a/HACKING.rst b/HACKING.rst index d99a07f9a..7230971af 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -1,187 +1,26 @@ -QuantumClient Style Commandments +Neutron Style Commandments ================================ -- Step 1: Read http://www.python.org/dev/peps/pep-0008/ -- Step 2: Read http://www.python.org/dev/peps/pep-0008/ again -- Step 3: Read on - - -General -------- -- Put two newlines between top-level code (funcs, classes, etc) -- Put one newline between methods in classes and anywhere else -- Do not write "except:", use "except Exception:" at the very least -- Include your name with TODOs as in "#TODO(termie)" -- Do not shadow a built-in or reserved word. Example:: - - def list(): - return [1, 2, 3] - - mylist = list() # BAD, shadows `list` built-in - - class Foo(object): - def list(self): - return [1, 2, 3] - - mylist = Foo().list() # OKAY, does not shadow built-in - - -Imports -------- -- Do not make relative imports -- Order your imports by the full module path -- Organize your imports according to the following template - -Example:: - - # vim: tabstop=4 shiftwidth=4 softtabstop=4 - {{stdlib imports in human alphabetical order}} - \n - {{third-party lib imports in human alphabetical order}} - \n - {{quantum imports in human alphabetical order}} - \n - \n - {{begin your code}} - - -Human Alphabetical Order Examples ---------------------------------- -Example:: - - import httplib - import logging - import random - import StringIO - import time - import unittest - - import eventlet - import webob.exc - - import quantum.api.networks - from quantum.api import ports - from quantum.db import models - from quantum.extensions import multiport - import quantum.manager - from quantum import service - - -Docstrings ----------- -Example:: - - """A one line docstring looks like this and ends in a period.""" - - - """A multiline docstring has a one-line summary, less than 80 characters. - - Then a new paragraph after a newline that explains in more detail any - general information about the function, class or method. Example usages - are also great to have here if it is a complex class for function. - - When writing the docstring for a class, an extra line should be placed - after the closing quotations. For more in-depth explanations for these - decisions see http://www.python.org/dev/peps/pep-0257/ - - If you are going to describe parameters and return values, use Sphinx, the - appropriate syntax is as follows. - - :param foo: the foo parameter - :param bar: the bar parameter - :returns: return_type -- description of the return value - :returns: description of the return value - :raises: AttributeError, KeyError - """ - - -Dictionaries/Lists ------------------- -If a dictionary (dict) or list object is longer than 80 characters, its items -should be split with newlines. Embedded iterables should have their items -indented. Additionally, the last item in the dictionary should have a trailing -comma. This increases readability and simplifies future diffs. - -Example:: - - my_dictionary = { - "image": { - "name": "Just a Snapshot", - "size": 2749573, - "properties": { - "user_id": 12, - "arch": "x86_64", - }, - "things": [ - "thing_one", - "thing_two", - ], - "status": "ACTIVE", - }, - } - - -Calling Methods ---------------- -Calls to methods 80 characters or longer should format each argument with -newlines. This is not a requirement, but a guideline:: - - unnecessarily_long_function_name('string one', - 'string two', - kwarg1=constants.ACTIVE, - kwarg2=['a', 'b', 'c']) - - -Rather than constructing parameters inline, it is better to break things up:: - - list_of_strings = [ - 'what_a_long_string', - 'not as long', - ] - - dict_of_numbers = { - 'one': 1, - 'two': 2, - 'twenty four': 24, - } - - object_one.call_a_method('string three', - 'string four', - kwarg1=list_of_strings, - kwarg2=dict_of_numbers) - - -Internationalization (i18n) Strings ------------------------------------ -In order to support multiple languages, we have a mechanism to support -automatic translations of exception and log strings. - -Example:: - - msg = _("An error occurred") - raise HTTPBadRequest(explanation=msg) - -If you have a variable to place within the string, first internationalize the -template string then do the replacement. - -Example:: - - msg = _("Missing parameter: %s") % ("flavor",) - LOG.error(msg) - -If you have multiple variables to place in the string, use keyword parameters. -This helps our translators reorder parameters when needed. - -Example:: - - msg = _("The server with id %(s_id)s has no key %(m_key)s") - LOG.error(msg % {"s_id": "1234", "m_key": "imageId"}) - - -Creating Unit Tests -------------------- -For every new feature, unit tests should be created that both test and -(implicitly) document the usage of said feature. If submitting a patch for a -bug that had no unit test, a new passing unit test should be added. If a -submitted bug fix does have a unit test, be sure to add a new one that fails -without the patch and passes with the patch. +- Step 1: Read the OpenStack Style Commandments + https://docs.openstack.org/hacking/latest/ +- 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 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 94058d438..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include tox.ini -include LICENSE README HACKING.rst -include version.py -include tools/* -include quantumclient/tests/* -include quantumclient/tests/unit/* diff --git a/README b/README deleted file mode 100644 index 20b7a8978..000000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -This is the client API library for Quantum. diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..eaac15c2c --- /dev/null +++ b/README.rst @@ -0,0 +1,43 @@ +======================== +Team and repository tags +======================== + +.. image:: https://governance.openstack.org/tc/badges/python-neutronclient.svg + :target: https://governance.openstack.org/tc/reference/tags/index.html + +.. Change things from this point on + +Python bindings to the Neutron API +================================== + +.. image:: https://img.shields.io/pypi/v/python-neutronclient.svg + :target: https://pypi.org/project/python-neutronclient/ + :alt: Latest Version + +This is a client library for Neutron built on the Neutron API. It +provides a Python API (the ``neutronclient`` module). + +.. note:: This project has been deprecated. The CLI code has been deleted + and is not accessible anymore. The Python bindings are still in use by + other projects but no new features will be added to this project. + All projects under Openstack governance migrating to use OpenstackSDK. + Any new feature should be proposed to OpenStack SDK and OpenStack + Client. + +* License: Apache License, Version 2.0 +* `PyPi`_ - package installation +* `Online Documentation`_ +* `Launchpad project`_ - release management +* `Blueprints`_ - feature specifications +* `Bugs`_ - issue tracking +* `Source`_ +* `Developer's Guide`_ + +.. _PyPi: https://pypi.org/project/python-neutronclient +.. _Online Documentation: https://docs.openstack.org/python-neutronclient/latest/ +.. _Launchpad project: https://launchpad.net/python-neutronclient +.. _Blueprints: https://blueprints.launchpad.net/python-neutronclient +.. _Bugs: https://bugs.launchpad.net/python-neutronclient +.. _Source: https://opendev.org/openstack/python-neutronclient +.. _Developer's Guide: http://docs.openstack.org/infra/manual/developers.html +.. _Release Notes: https://docs.openstack.org/releasenotes/python-neutronclient diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 000000000..718a2395a --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,7 @@ +# 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. +openstackdocstheme>=2.2.0 # Apache-2.0 +reno>=3.1.0 # Apache-2.0 +sphinx>=2.0.0,!=2.1.0 # BSD +cliff>=3.4.0 # Apache-2.0 diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst new file mode 100644 index 000000000..4b1f4720a --- /dev/null +++ b/doc/source/cli/index.rst @@ -0,0 +1,55 @@ +.. + 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. + + + 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.) + +========= +Using CLI +========= + +There is `OpenStackClient (OSC) +`__ +which support the Networking API + +OpenStackClient +--------------- + +OpenStackClient provides +`the basic network commands `__ +and python-neutronclient provides :doc:`extensions ` +(aka OSC plugins) for advanced networking services. + +.. toctree:: + :maxdepth: 1 + + Basic network commands + Network commands for advanced networking services + Mapping Guide from neutron CLI + +neutron CLI +----------- + +.. warning:: + + neutron CLI is removed. Use openstack CLI instead. See `openstack CLI command list + `__ + and :doc:`its extensions for advanced networking services `. + The command mapping from neutron CLI to openstack CLI is available + `here `__. diff --git a/doc/source/cli/osc/v2/bgp-dynamic-routing.rst b/doc/source/cli/osc/v2/bgp-dynamic-routing.rst new file mode 100644 index 000000000..c86bb45ee --- /dev/null +++ b/doc/source/cli/osc/v2/bgp-dynamic-routing.rst @@ -0,0 +1,47 @@ +=================== +BGP Dynamic Routing +=================== + +BGP dynamic routing enables announcement of project subnet prefixes +via BGP. Admins create BGP speakers and BGP peers. BGP peers can be +associated with BGP speakers, thereby enabling peering sessions with +operator infrastructure. BGP speakers can be associated with networks, +which controls which routes are announced to peers. + +Network v2 + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp speaker create + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp speaker delete + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp speaker list + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp speaker set + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp speaker show + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp speaker add network + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp speaker remove network + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp speaker add peer + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp speaker remove peer + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp speaker list advertised routes + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp peer * + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgp dragent * diff --git a/doc/source/cli/osc/v2/network-log.rst b/doc/source/cli/osc/v2/network-log.rst new file mode 100644 index 000000000..df3a32729 --- /dev/null +++ b/doc/source/cli/osc/v2/network-log.rst @@ -0,0 +1,15 @@ +=========== +network log +=========== + +A **network log** is a container to group security groups or ports for logging. +Specified resources can be logged via these event (``ALL``, ``ACCEPT`` or +``DROP``). + +Network v2 + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: network loggable resources list + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: network log * diff --git a/doc/source/cli/osc/v2/networking-sfc.rst b/doc/source/cli/osc/v2/networking-sfc.rst new file mode 100644 index 000000000..4f4242d74 --- /dev/null +++ b/doc/source/cli/osc/v2/networking-sfc.rst @@ -0,0 +1,39 @@ +============== +networking sfc +============== + +**Service Function Chaining** is a mechanism for overriding the basic destination based forwarding +that is typical of IP networks. Service Function Chains consist of an ordered sequence of +Service Functions (SFs). SFs are virtual machines (or potentially physical devices) that perform a +network function such as firewall, content cache, packet inspection, or any other function that +requires processing of packets in a flow from point A to point B even though the SFs are not +literally between point A and B from a routing table perspective. + +Network v2 + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: sfc flow classifier * + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: sfc port chain * + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: sfc port pair create + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: sfc port pair delete + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: sfc port pair list + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: sfc port pair set + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: sfc port pair show + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: sfc port pair group * + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: sfc service graph * diff --git a/doc/source/cli/osc/v2/subnet-onboard.rst b/doc/source/cli/osc/v2/subnet-onboard.rst new file mode 100644 index 000000000..006970372 --- /dev/null +++ b/doc/source/cli/osc/v2/subnet-onboard.rst @@ -0,0 +1,17 @@ +======================= +network onboard subnets +======================= + +**network onboard subnets** enables a subnet to be adopted or +"onboarded" into an existing subnet pool. The CIDR of the subnet +is checked for uniqueness across any applicable address scopes +and all subnets allocated from the target subnet pool. Once +onboarded, the subnet CIDR is added to the prefix list of the +subnet pool and the subnet appears as though it has been allocated +from the subnet pool. The subnet also begins participating in the +applicable address scope if the subnet pool belongs to one. + +Network v2 + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: network onboard subnets diff --git a/doc/source/cli/osc/v2/vpn-endpoint-group.rst b/doc/source/cli/osc/v2/vpn-endpoint-group.rst new file mode 100644 index 000000000..9581d8b4a --- /dev/null +++ b/doc/source/cli/osc/v2/vpn-endpoint-group.rst @@ -0,0 +1,11 @@ +================== +VPN Endpoint Group +================== + +The **Endpoint Group** is used to configure multiple local and remote subnets +in vpnservice object. + +Network v2 + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: vpn endpoint group * diff --git a/doc/source/cli/osc/v2/vpn-ike-policy.rst b/doc/source/cli/osc/v2/vpn-ike-policy.rst new file mode 100644 index 000000000..3e38a5c7c --- /dev/null +++ b/doc/source/cli/osc/v2/vpn-ike-policy.rst @@ -0,0 +1,12 @@ +============== +VPN IKE Policy +============== + +The **IKE Policy** is used for phases one and two negotiation of the +VPN connection. You can specify both the authentication and encryption +algorithms for connections. + +Network v2 + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: vpn ike policy * diff --git a/doc/source/cli/osc/v2/vpn-ipsec-policy.rst b/doc/source/cli/osc/v2/vpn-ipsec-policy.rst new file mode 100644 index 000000000..c8a5f3beb --- /dev/null +++ b/doc/source/cli/osc/v2/vpn-ipsec-policy.rst @@ -0,0 +1,11 @@ +================ +VPN IPsec Policy +================ + +The **IPsec Policy** specifies the authentication and encryption algorithms +and encapsulation mode to use for the established VPN connection. + +Network v2 + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: vpn ipsec policy * diff --git a/doc/source/cli/osc/v2/vpn-ipsec-site-connection.rst b/doc/source/cli/osc/v2/vpn-ipsec-site-connection.rst new file mode 100644 index 000000000..0244e73ee --- /dev/null +++ b/doc/source/cli/osc/v2/vpn-ipsec-site-connection.rst @@ -0,0 +1,10 @@ +========================= +VPN IPsec Site Connection +========================= + +Creates a site-to-site **IPsec Site Connection** for a VPN service. + +Network v2 + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: vpn ipsec site connection * diff --git a/doc/source/cli/osc/v2/vpn-service.rst b/doc/source/cli/osc/v2/vpn-service.rst new file mode 100644 index 000000000..b412555c8 --- /dev/null +++ b/doc/source/cli/osc/v2/vpn-service.rst @@ -0,0 +1,11 @@ +=========== +VPN Service +=========== + +The **VPN Service** is associated with a router. After you +create the service, it can contain multiple VPN connections. + +Network v2 + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: vpn service * diff --git a/doc/source/cli/osc_plugins.rst b/doc/source/cli/osc_plugins.rst new file mode 100644 index 000000000..0cd7e26ba --- /dev/null +++ b/doc/source/cli/osc_plugins.rst @@ -0,0 +1,37 @@ +.. + 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. + + + 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.) + +Advanced Network Commands in OpenStack Client +============================================= + +The following list covers the extended commands for advanced network +services available in ``openstack`` command. + +These commands can be referenced by doing ``openstack help`` and +the detail of individual command can be referred by +``openstack help ``. + +.. toctree:: + :glob: + :maxdepth: 2 + + osc/v2/* diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 000000000..70fd0de2f --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# + +# -- 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', + 'reno.sphinxext', + 'openstackdocstheme', + 'cliff.sphinxext', +] + +# openstackdocstheme options +openstackdocs_repo_name = 'openstack/python-neutronclient' +openstackdocs_pdf_link = True +openstackdocs_bug_project = 'python-neutronclient' +openstackdocs_bug_tag = 'doc' + +# 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 = '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 = 'neutronclientdoc' + +# -- Options for LaTeX output ------------------------------------------------ + +latex_documents = [ + ('index', 'doc-python-neutronclient.tex', + 'python-neutronclient Documentation', + 'Neutron Contributors', 'manual'), +] + +# Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 +latex_use_xindy = False + +latex_domain_indices = False + +latex_elements = { + 'makeindex': '', + 'printindex': '', + 'preamble': r'\setcounter{tocdepth}{5}', +} + +# -- Options for cliff.sphinxext plugin --------------------------------------- + +autoprogram_cliff_application = 'openstack' diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 000000000..fae3f6b82 --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,34 @@ +.. + 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. + + + 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.) + +================= +Contributor Guide +================= + +In the Contributor Guide, you will find information on neutronclient's +lower level programming details or APIs as well as the transition to +OpenStack client. + +.. toctree:: + :maxdepth: 2 + + transition_to_osc diff --git a/doc/source/contributor/transition_to_osc.rst b/doc/source/contributor/transition_to_osc.rst new file mode 100644 index 000000000..9bb952902 --- /dev/null +++ b/doc/source/contributor/transition_to_osc.rst @@ -0,0 +1,222 @@ +.. + 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. + + + 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.) + +Transition to OpenStack Client +============================== + +This document details the transition roadmap for moving the neutron client's +OpenStack Networking API support, both the Python library and the ``neutron`` +command-line interface (CLI), to the +`OpenStack Client (OSC) `_ +and the `OpenStack Python SDK `_. +This transition is being guided by the +`Deprecate individual CLIs in favour of OSC `_ +OpenStack spec. See the `Neutron RFE `_, +`OSC neutron support etherpad `_ and +details below for the overall progress of this transition. + +Overview +-------- + +This transition will result in the neutron client's ``neutron`` CLI being +deprecated and then eventually removed. The ``neutron`` CLI will be replaced +by OSC's networking support available via the ``openstack`` CLI. This is +similar to the deprecation and removal process for the +`keystone client's `_ +``keystone`` CLI. The neutron client's Python library won't be deprecated. +It will be available along side the networking support provided by the +OpenStack Python SDK. + +Users of the neutron client's command extensions will need to transition to the +`OSC plugin system `_ +before the ``neutron`` CLI is removed. Such users will maintain their OSC plugin +commands within their own project and will be responsible for deprecating and +removing their ``neutron`` CLI extension. + +Transition Steps +---------------- + +1. **Done:** OSC adds OpenStack Python SDK as a dependency. See the following + patch set: https://review.opendev.org/#/c/138745/ + +2. **Done:** OSC switches its networking support for the + `network `_ + command object to use the OpenStack Python SDK instead of the neutron + client's Python library. See the following patch set: + https://review.opendev.org/#/c/253348/ + +3. **Done:** OSC removes its python-neutronclient dependency. + See the following patch set: https://review.opendev.org/#/c/255545/ + +4. **In Progress:** OpenStack Python SDK releases version 1.0 to guarantee + backwards compatibility of its networking support and OSC updates + its dependencies to include OpenStack Python SDK version 1.0 or later. + See the following blueprint: https://blueprints.launchpad.net/python-openstackclient/+spec/network-command-sdk-support + +5. **Done:** OSC switches its networking support for the + `ip floating `_, + `ip floating pool `_, + `ip fixed `_, + `security group `_, and + `security group rule `_ + command objects to use the OpenStack Python SDK instead of the nova + client's Python library when neutron is enabled. When nova network + is enabled, then the nova client's Python library will continue to + be used. See the following OSC bugs: + + * **Done** `Floating IP CRUD `_ + + * **Done** `Port CRUD `_ + + * **Done** `Security Group CRUD `_ + + * **Done** `Security Group Rule CRUD `_ + +6. **Done** OSC continues enhancing its networking support. + At this point and when applicable, enhancements to the ``neutron`` + CLI must also be made to the ``openstack`` CLI and possibly the + OpenStack Python SDK. Users of the neutron client's command extensions + should start their transition to the OSC plugin system. See the + developer guide section below for more information on this step. + +7. **Done** Deprecate the ``neutron`` CLI. Running the CLI after + it has been `deprecated `_ + will issue a warning message: + ``neutron CLI is deprecated and will be removed in the Z cycle. Use openstack CLI instead.`` + In addition, no new features will be added to the CLI, though fixes to + the CLI will be assessed on a case by case basis. + +8. **Done** Remove the ``neutron`` CLI after two deprecation cycles + once the criteria below have been met. + + * The networking support provide by the ``openstack`` CLI is functionally + equivalent to the ``neutron`` CLI and it contains sufficient functional + and unit test coverage. + + * `Neutron Stadium `_ + projects, Neutron documentation and `DevStack `_ + use ``openstack`` CLI instead of ``neutron`` CLI. + + * Most users of the neutron client's command extensions have transitioned + to the OSC plugin system and use the ``openstack`` CLI instead of the + ``neutron`` CLI. + +Developer Guide +--------------- +The ``neutron`` CLI tool is now removed and all new CLI changes should be done +in the ``OpenStackClient (OSC)`` and, if needed, also in the ``OpenStack SDK``. + +**Where does my CLI belong?** + +If you are developing an API in any of the `neutron repos `_ +the client-side support must be generally located in either the openstackclient or neutronclient +repos. Whether the actual code goes into one or the other repo it depends on the nature of the +feature, its maturity level, and/or the depth of feedback required during the development. + +The table below provides an idea of what goes where. Generally speaking, we consider Core anything +that is L2 and L3 related or that it has been located in the neutron repo for quite sometime, e.g. +QoS or Metering, or that it is available in each neutron deployment irrespective of its configuration +(e.g. auto-allocated-topology). Any client feature that falls into this categorization will need to +be contributed in OSC. Any other that does not, will need to go into neutronclient, assuming that +its server-side is located in a neutron controlled repo. This is a general guideline, when in doubt, +please reach out to a member of the neutron core team for clarifications. + ++---------------------------+-------------------+-------------------------------------------------+ +| Networking Commands | OSC Plugin | OpenStack Project for ``openstack`` Commands | ++===========================+===================+=================================================+ +| Core | No | python-openstackclient | ++---------------------------+-------------------+-------------------------------------------------+ +| Extension | Yes | python-neutronclient | +| (i.e. neutron stadium) | | (``neutronclient/osc/v2/``) | ++---------------------------+-------------------+-------------------------------------------------+ +| Other | Yes | Applicable project owning networking resource | ++---------------------------+-------------------+-------------------------------------------------+ + +When a repo stops being under neutron governance, its client-side counterpart will have to go through +deprecation. Bear in mind that for grandfathered extensions like FWaaS v1, VPNaaS, and LBaaS v1, this +is not required as the neutronclient is already deprecated on its own. + +**Which Python library do I change?** + ++-------------------------------------------------+-----------------------------------------------+ +| OpenStack Project for ``openstack`` Commands | Python Library to Change | ++=================================================+===============================================+ +| python-openstackclient | openstacksdk | ++-------------------------------------------------+-----------------------------------------------+ +| Other | Applicable project owning network resource | ++-------------------------------------------------+-----------------------------------------------+ + + +**Important:** The actual name of the command object and/or action in OSC may differ +from those used by neutron in order to follow the OSC command structure and to avoid +name conflicts. The `network` prefix must be used to avoid name conflicts if the +command object name is highly likely to have an ambiguous meaning. Developers should +get new command objects and actions approved by the OSC team before proceeding with the +implementation. + +The "Core" group includes network resources that provide core ``neutron`` project +features (e.g. network, subnet, port, etc.) and not advanced features in the +``neutron`` project (e.g. trunk, etc.) or advanced services in separate projects +(FWaaS, LBaaS, VPNaaS, dynamic routing, etc.). +The "Other" group applies projects other than the core ``neutron`` project. +Contact the neutron PTL or core team with questions on network resource classification. + +When adding or updating an ``openstack`` networking command to +python-openstackclient, changes may first be required to the +OpenStack Python SDK to support the underlying networking resource object, +properties and/or actions. Once the OpenStack Python SDK changes are merged, +the related OSC changes can be merged. The OSC changes may require an update +to the OSC openstacksdk version in the +`requirements.txt `_ +file. + +When adding an ``openstack`` networking command to python-openstackclient, +you can optionally propose an +`OSC command spec `_ +which documents the new command interface before proceeding with the implementation. + +Users of the neutron client's command extensions must adopt the +`OSC plugin `_ +system for this transition. Such users will maintain their OSC plugin within their +own project and should follow the guidance in the table above to determine +which command to change. + +Developer References +-------------------- + +* See `OSC neutron support etherpad `_ + to determine if an ``openstack`` command is in progress. +* See `OSC command list `_ + to determine if an ``openstack`` command exists. +* See `OSC command spec list `_ + to determine if an ``openstack`` command spec exists. +* See `OSC plugin command list `_ + to determine if an ``openstack`` plugin command exists. +* See `OSC command structure `_ + to determine the current ``openstack`` command objects, plugin objects and actions. +* See `OSC human interface guide `_ + for guidance on creating new OSC command interfaces. +* See `OSC plugin `_ + for information on the OSC plugin system to be used for ``neutron`` CLI extensions. +* Create an OSC blueprint: https://blueprints.launchpad.net/python-openstackclient/ +* Report an OSC bug: https://bugs.launchpad.net/python-openstackclient/+filebug +* Report an OpenStack Python SDK bug: https://bugs.launchpad.net/python-openstacksdk/+filebug diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 000000000..c36d482ed --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,65 @@ +.. + 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. + + 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.) + +================================== +python-neutronclient documentation +================================== + +This is a client for OpenStack Networking API. It provides +:doc:`Python API bindings ` (the neutronclient module). + +There is +`OpenStack Client (OSC) `__. +CLI which support the Networking API. + +User Documentation +------------------ + +.. toctree:: + :maxdepth: 2 + + cli/index + reference/index + +Contributor Guide +----------------- + +In the :doc:`Contributor Guide `, you will find +information on neutronclient's lower level programming details or APIs +as well as the transition to OpenStack client. + +.. toctree:: + :maxdepth: 2 + + contributor/index + +.. note:: + + neutron CLI has been deprecated from Ocata release. + We do not add, change and drop any existing commands any more. + We only accept changes on OSC plugin, neutronclient python bindings + and bug fixes on the deprecated CLI (``neutron`` command). + +History +------- + +Release notes is available at +http://docs.openstack.org/releasenotes/python-neutronclient/. diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst new file mode 100644 index 000000000..5678d6578 --- /dev/null +++ b/doc/source/reference/index.rst @@ -0,0 +1,95 @@ +.. + 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. + + + 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.) + +neutronclient Python API +======================== + +Basic Usage +----------- + +First create a client instance using a keystoneauth Session. For more +information on this keystoneauth API, see `Using Sessions`_. + +.. _Using Sessions: https://docs.openstack.org/keystoneauth/latest/using-sessions.html + +.. code-block:: python + + from keystoneauth1 import identity + from keystoneauth1 import session + from neutronclient.v2_0 import client + username='username' + password='password' + project_name='demo' + project_domain_id='default' + user_domain_id='default' + auth_url='http://auth.example.com:5000/v3' + auth = identity.Password(auth_url=auth_url, + username=username, + password=password, + project_name=project_name, + project_domain_id=project_domain_id, + user_domain_id=user_domain_id) + sess = session.Session(auth=auth) + neutron = client.Client(session=sess) + +If you are using Identity v2.0 API (DEPRECATED), create an auth plugin using +the appropriate parameters and `keystoneauth1.identity` will handle Identity +API version discovery. Then you can create a Session and a Neutronclient just +like the previous example. + +.. code-block:: python + + auth = identity.Password(auth_url=auth_url, + username=username, + password=password, + project_name=project_name) + # create a Session and a Neutronclient + +Now you can call various methods on the client instance. + +.. code-block:: python + + network = {'name': 'mynetwork', 'admin_state_up': True} + neutron.create_network({'network':network}) + networks = neutron.list_networks(name='mynetwork') + print networks + network_id = networks['networks'][0]['id'] + neutron.delete_network(network_id) + +Alternatively, you can create a client instance using an auth token +and a service endpoint URL directly. + +.. code-block:: python + + from neutronclient.v2_0 import client + neutron = client.Client(endpoint_url='http://192.168.206.130:9696/', + token='d3f9226f27774f338019aa2611112ef6') + +You can get ``X-Openstack-Request-Id`` as ``request_ids`` from the result. + +.. code-block:: python + + network = {'name': 'mynetwork', 'admin_state_up': True} + neutron.create_network({'network':network}) + networks = neutron.list_networks(name='mynetwork') + print networks.request_ids + # -> ['req-978a0160-7ab0-44f0-8a93-08e9a4e785fa'] diff --git a/quantumclient/tests/__init__.py b/neutronclient/__init__.py similarity index 100% rename from quantumclient/tests/__init__.py rename to neutronclient/__init__.py diff --git a/neutronclient/_i18n.py b/neutronclient/_i18n.py new file mode 100644 index 000000000..50ac3c5bf --- /dev/null +++ b/neutronclient/_i18n.py @@ -0,0 +1,31 @@ +# 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 + + +DOMAIN = 'neutronclient' + +_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN) + +# The translation function using the well-known name "_" +_ = _translators.primary + +# The contextual translation function using the name "_C" +_C = _translators.contextual_form + +# The plural translation function using the name "_P" +_P = _translators.plural_form + + +def get_available_languages(): + return oslo_i18n.get_available_languages(DOMAIN) diff --git a/neutronclient/client.py b/neutronclient/client.py new file mode 100644 index 000000000..e25f575f3 --- /dev/null +++ b/neutronclient/client.py @@ -0,0 +1,439 @@ +# 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 logging +import os + +import debtcollector.renames +from keystoneauth1 import access +from keystoneauth1 import adapter +from oslo_serialization import jsonutils +from oslo_utils import importutils +import requests + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils + +osprofiler_web = importutils.try_import("osprofiler.web") + +_logger = logging.getLogger(__name__) + +if os.environ.get('NEUTRONCLIENT_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 +USER_AGENT = 'python-neutronclient' +REQ_ID_HEADER = 'X-OpenStack-Request-ID' + + +class HTTPClient(object): + """Handles the REST calls and responses, include authn.""" + + CONTENT_TYPE = 'application/json' + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + @debtcollector.renames.renamed_kwarg( + 'tenant_name', 'project_name', replace=True) + def __init__(self, username=None, user_id=None, + project_name=None, project_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, cert=None, + log_credentials=False, service_type='network', + global_request_id=None, **kwargs): + + self.username = username + self.user_id = user_id + self.project_name = project_name + self.project_id = project_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 + self.global_request_id = global_request_id + self.cert = cert + 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'] = 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=str(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=str(e)) + utils.http_log_resp(_logger, resp, body) + + # log request-id for each api call + request_id = resp.headers.get('x-openstack-request-id') + if request_id: + _logger.debug('%(method)s call to neutron for ' + '%(url)s used request id ' + '%(response_request_id)s', + {'method': resp.request.method, + 'url': resp.url, + 'response_request_id': request_id}) + + 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) + + if self.global_request_id: + headers.setdefault(REQ_ID_HEADER, self.global_request_id) + + headers['User-Agent'] = USER_AGENT + # NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any + # headers in case if osprofiler is not initialized. + if osprofiler_web: + headers.update(osprofiler_web.get_trace_id_headers()) + + resp = requests.request( + method, + url, + data=body, + headers=headers, + verify=self.verify_cert, + cert=self.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['headers'] = kwargs.get('headers') or {} + 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() + kwargs['headers'] = kwargs.get('headers') or {} + kwargs['headers']['X-Auth-Token'] = self.auth_token + 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.create(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, + interface=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.project_id: + body = {'auth': {'passwordCredentials': creds, + 'tenantId': self.project_id, }, } + else: + body = {'auth': {'passwordCredentials': creds, + 'tenantName': self.project_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=jsonutils.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 = jsonutils.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 neutron client + # is initialized just before the token is expired + self.authenticate() + return self.endpoint_url + + body = jsonutils.loads(body) + for endpoint in body.get('endpoints', []): + if (endpoint['type'] == 'network' 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} + + def get_auth_ref(self): + return getattr(self, 'auth_ref', None) + + +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.get('headers') or {} + headers.setdefault('Accept', content_type) + + # NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any + # headers in case if osprofiler is not initialized. + if osprofiler_web: + headers.update(osprofiler_web.get_trace_id_headers()) + + try: + kwargs.setdefault('data', kwargs.pop('body')) + except KeyError: + pass + + if kwargs.get('data'): + headers.setdefault('Content-Type', content_type) + + kwargs['headers'] = headers + 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 + + def get_auth_ref(self): + return self.session.auth.get_auth_ref(self.session) + + +# FIXME(bklei): Should refactor this to use kwargs and only +# explicitly list arguments that are not None. +@debtcollector.renames.renamed_kwarg('tenant_id', 'project_id', replace=True) +@debtcollector.renames.renamed_kwarg( + 'tenant_name', 'project_name', replace=True) +def construct_http_client(username=None, + user_id=None, + project_name=None, + project_id=None, + password=None, + auth_url=None, + token=None, + region_name=None, + timeout=None, + endpoint_url=None, + insecure=False, + endpoint_type='public', + log_credentials=None, + auth_strategy='keystone', + ca_cert=None, + cert=None, + service_type='network', + session=None, + global_request_id=None, + **kwargs): + + if session: + kwargs.setdefault('user_agent', USER_AGENT) + kwargs.setdefault('interface', endpoint_type) + return SessionClient(session=session, + service_type=service_type, + region_name=region_name, + global_request_id=global_request_id, + **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, + project_id=project_id, + project_name=project_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, + cert=cert, + log_credentials=log_credentials, + auth_strategy=auth_strategy, + global_request_id=global_request_id) diff --git a/quantumclient/tests/unit/__init__.py b/neutronclient/common/__init__.py similarity index 100% rename from quantumclient/tests/unit/__init__.py rename to neutronclient/common/__init__.py diff --git a/neutronclient/common/clientmanager.py b/neutronclient/common/clientmanager.py new file mode 100644 index 000000000..56d3d3c12 --- /dev/null +++ b/neutronclient/common/clientmanager.py @@ -0,0 +1,120 @@ +# 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. +""" + +import debtcollector.renames + +from neutronclient import client +from neutronclient.neutron import client as neutron_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.""" + neutron = ClientCache(neutron_client.make_client) + # Provide support for old quantum commands (for example + # in stable versions) + quantum = neutron + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + @debtcollector.renames.renamed_kwarg( + 'tenant_name', 'project_name', replace=True) + def __init__(self, token=None, url=None, + auth_url=None, + endpoint_type=None, + project_name=None, + project_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, + service_name=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._service_name = service_name + self._endpoint_type = endpoint_type + self._project_name = project_name + self._project_id = project_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, + project_name=self._project_name, + project_id=self._project_id, + password=self._password, + region_name=self._region_name, + auth_url=self._auth_url, + service_type=self._service_type, + service_name=self._service_name, + 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 diff --git a/neutronclient/common/constants.py b/neutronclient/common/constants.py new file mode 100644 index 000000000..ccb22cd80 --- /dev/null +++ b/neutronclient/common/constants.py @@ -0,0 +1,32 @@ +# 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. + + +TYPE_BOOL = "bool" +TYPE_INT = "int" +TYPE_LONG = "long" +TYPE_FLOAT = "float" +TYPE_LIST = "list" +TYPE_DICT = "dict" + +PLURALS = {'networks': 'network', + 'ports': 'port', + 'subnets': 'subnet', + 'subnetpools': 'subnetpool', + 'dns_nameservers': 'dns_nameserver', + 'host_routes': 'host_route', + 'allocation_pools': 'allocation_pool', + 'fixed_ips': 'fixed_ip', + 'extensions': 'extension'} diff --git a/neutronclient/common/exceptions.py b/neutronclient/common/exceptions.py new file mode 100644 index 000000000..443f78155 --- /dev/null +++ b/neutronclient/common/exceptions.py @@ -0,0 +1,260 @@ +# 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 oslo_utils import encodeutils + +from neutronclient._i18n import _ + +""" +Neutron base exception handling. + +Exceptions are classified into three categories: +* Exceptions corresponding to exceptions from neutron server: + This type of exceptions should inherit one of exceptions + in HTTP_EXCEPTION_MAP. +* Exceptions from client library: + This type of exceptions should inherit NeutronClientException. +* Exceptions from CLI code: + This type of exceptions should inherit NeutronCLIError. +""" + + +# NOTE: This method is defined here to avoid +# an import loop between common.utils and this module. +def _safe_decode_dict(kwargs): + for k, v in kwargs.items(): + kwargs[k] = encodeutils.safe_decode(v) + return kwargs + + +class NeutronException(Exception): + """Base Neutron 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 % _safe_decode_dict(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 NeutronClientException(NeutronException): + """Base exception which exceptions from Neutron 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 + req_ids_msg = _("Neutron server returns request_ids: %s") + request_ids = [] + + def __init__(self, message=None, **kwargs): + self.request_ids = kwargs.get('request_ids') + if 'status_code' in kwargs: + self.status_code = kwargs['status_code'] + if self.request_ids: + req_ids_msg = self.req_ids_msg % self.request_ids + if message: + message = _('%(msg)s\n%(id)s') % {'msg': message, + 'id': req_ids_msg} + else: + message = req_ids_msg + super(NeutronClientException, self).__init__(message, **kwargs) + + +# Base exceptions from Neutron + +class BadRequest(NeutronClientException): + status_code = 400 + + +class Unauthorized(NeutronClientException): + status_code = 401 + message = _("Unauthorized: bad credentials.") + + +class Forbidden(NeutronClientException): + status_code = 403 + message = _("Forbidden: your credentials don't give you access to this " + "resource.") + + +class NotFound(NeutronClientException): + status_code = 404 + + +class Conflict(NeutronClientException): + status_code = 409 + + +class InternalServerError(NeutronClientException): + status_code = 500 + + +class ServiceUnavailable(NeutronClientException): + status_code = 503 + + +HTTP_EXCEPTION_MAP = { + 400: BadRequest, + 401: Unauthorized, + 403: Forbidden, + 404: NotFound, + 409: Conflict, + 500: InternalServerError, + 503: ServiceUnavailable, +} + + +# Exceptions mapped to Neutron server exceptions +# These are defined if a user of client library needs specific exception. +# Exception name should be + '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 IpAddressAlreadyAllocatedClient(Conflict): + pass + + +class InvalidIpForNetworkClient(BadRequest): + pass + + +class InvalidIpForSubnetClient(BadRequest): + pass + + +class OverQuotaClient(Conflict): + pass + + +class IpAddressGenerationFailureClient(Conflict): + pass + + +class MacAddressInUseClient(Conflict): + pass + + +class HostNotCompatibleWithFixedIpsClient(Conflict): + pass + + +class ExternalIpAddressExhaustedClient(BadRequest): + pass + + +# Exceptions from client library + +class NoAuthURLProvided(Unauthorized): + message = _("auth_url was not provided to the Neutron client") + + +class EndpointNotFound(NeutronClientException): + message = _("Could not find Service or Region in Service Catalog.") + + +class EndpointTypeNotFound(NeutronClientException): + message = _("Could not find endpoint type %(type_)s in Service Catalog.") + + +class AmbiguousEndpoints(NeutronClientException): + message = _("Found more than one matching endpoint in Service Catalog: " + "%(matching_endpoints)") + + +class RequestURITooLong(NeutronClientException): + """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(NeutronClientException): + message = _("Connection to neutron failed: %(reason)s") + + +class SslCertificateValidationError(NeutronClientException): + message = _("SSL certificate validation has failed: %(reason)s") + + +class MalformedResponseBody(NeutronClientException): + message = _("Malformed response body: %(reason)s") + + +class InvalidContentType(NeutronClientException): + message = _("Invalid content type %(content_type)s.") + + +# Command line exceptions + +class NeutronCLIError(NeutronException): + """Exception raised when command line parsing fails.""" + pass + + +class CommandError(NeutronCLIError): + pass + + +class UnsupportedVersion(NeutronCLIError): + """Indicates usage of an unsupported API version + + Indicates that the user is trying to use an unsupported version of + the API. + """ + pass + + +class NeutronClientNoUniqueMatch(NeutronCLIError): + message = _("Multiple %(resource)s matches found for name '%(name)s'," + " use an ID to be more specific.") diff --git a/neutronclient/common/extension.py b/neutronclient/common/extension.py new file mode 100644 index 000000000..00a035d6c --- /dev/null +++ b/neutronclient/common/extension.py @@ -0,0 +1,85 @@ +# 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 neutronclient.neutron import v2_0 as neutronV20 + + +def _discover_via_entry_points(): + emgr = extension.ExtensionManager('neutronclient.extension', + invoke_on_load=False) + return ((ext.name, ext.plugin) for ext in emgr) + + +class NeutronClientExtension(neutronV20.NeutronCommand): + pagination_support = False + _formatters = {} + sorting_support = False + + +class ClientExtensionShow(NeutronClientExtension, neutronV20.ShowCommand): + def take_action(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).take_action(parsed_args) + + +class ClientExtensionList(NeutronClientExtension, neutronV20.ListCommand): + def take_action(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).take_action(parsed_args) + + +class ClientExtensionDelete(NeutronClientExtension, neutronV20.DeleteCommand): + def take_action(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).take_action(parsed_args) + + +class ClientExtensionCreate(NeutronClientExtension, neutronV20.CreateCommand): + def take_action(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).take_action(parsed_args) + + +class ClientExtensionUpdate(NeutronClientExtension, neutronV20.UpdateCommand): + def take_action(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).take_action(parsed_args) diff --git a/neutronclient/common/serializer.py b/neutronclient/common/serializer.py new file mode 100644 index 000000000..d4c8bbbb6 --- /dev/null +++ b/neutronclient/common/serializer.py @@ -0,0 +1,120 @@ +# 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. + +from oslo_serialization import jsonutils + +from neutronclient._i18n import _ +from neutronclient.common import exceptions as exception + + +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 str(obj) + return jsonutils.dumps(data, default=sanitizer) + + +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)} + + +# NOTE(maru): this class is duplicated from neutron.wsgi +class Serializer(object): + """Serializes and deserializes dictionaries to certain MIME types.""" + + def __init__(self, metadata=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 {} + + def _get_serialize_handler(self, content_type): + handlers = { + 'application/json': JSONDictSerializer(), + } + + try: + return handlers[content_type] + except Exception: + raise exception.InvalidContentType(content_type=content_type) + + def serialize(self, data): + """Serialize a dictionary into the specified content type.""" + return self._get_serialize_handler("application/json").serialize(data) + + def deserialize(self, datastring): + """Deserialize a string to a dictionary. + + The string must be in the format of a supported MIME type. + """ + return self.get_deserialize_handler("application/json").deserialize( + datastring) + + def get_deserialize_handler(self, content_type): + handlers = { + 'application/json': JSONDeserializer(), + } + + try: + return handlers[content_type] + except Exception: + raise exception.InvalidContentType(content_type=content_type) diff --git a/neutronclient/common/utils.py b/neutronclient/common/utils.py new file mode 100644 index 000000000..93f92fd19 --- /dev/null +++ b/neutronclient/common/utils.py @@ -0,0 +1,237 @@ +# 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 functools +import hashlib +import logging +import os + +from oslo_utils import encodeutils +from oslo_utils import importutils + +from neutronclient._i18n import _ +from neutronclient.common import exceptions + +SENSITIVE_HEADERS = ('X-Auth-Token',) + + +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 convert_to_uppercase(string): + return string.upper() + + +def convert_to_lowercase(string): + return string.lower() + + +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, required_keys=None, optional_keys=None): + """Convert key1=value1,key2=value2,... string into dictionary. + + :param strdict: string in the form of key1=value1,key2=value2 + :param required_keys: list of required keys. All keys in this list must be + specified. Otherwise ArgumentTypeError will be raised. + If this parameter is unspecified, no required key check + will be done. + :param optional_keys: list of optional keys. + This parameter is used for valid key check. + When at least one of required_keys and optional_keys, + a key must be a member of either of required_keys or + optional_keys. Otherwise, ArgumentTypeError will be + raised. When both required_keys and optional_keys are + unspecified, no valid key check will be done. + """ + result = {} + if strdict: + i = 0 + kvlist = [] + for kv in strdict.split(','): + if '=' in kv: + kvlist.append(kv) + i += 1 + elif i == 0: + msg = _("missing value for key '%s'") + raise argparse.ArgumentTypeError(msg % kv) + else: + kvlist[i - 1] = "%s,%s" % (kvlist[i - 1], kv) + for kv in kvlist: + key, sep, value = kv.partition('=') + if not sep: + msg = _("invalid key-value '%s', expected format: key=value") + raise argparse.ArgumentTypeError(msg % kv) + result[key] = value + valid_keys = set(required_keys or []) | set(optional_keys or []) + if valid_keys: + invalid_keys = [k for k in result if k not in valid_keys] + if invalid_keys: + msg = _("Invalid key(s) '%(invalid_keys)s' specified. " + "Valid key(s): '%(valid_keys)s'.") + raise argparse.ArgumentTypeError( + msg % {'invalid_keys': ', '.join(sorted(invalid_keys)), + 'valid_keys': ', '.join(sorted(valid_keys))}) + if required_keys: + not_found_keys = [k for k in required_keys if k not in result] + if not_found_keys: + msg = _("Required key(s) '%s' not specified.") + raise argparse.ArgumentTypeError(msg % ', '.join(not_found_keys)) + return result + + +def str2dict_type(optional_keys=None, required_keys=None): + return functools.partial(str2dict, + optional_keys=optional_keys, + required_keys=required_keys) + + +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 (key, value) in kwargs['headers'].items(): + if key in SENSITIVE_HEADERS: + v = value.encode('utf-8') + h = hashlib.sha256(v) + d = h.hexdigest() + value = "{SHA256}%s" % d + header = ' -H "%s: %s"' % (key, value) + 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("REQ: %s", req) + + +def http_log_resp(_logger, resp, body): + if not _logger.isEnabledFor(logging.DEBUG): + return + _logger.debug("RESP: %(code)s %(headers)s %(body)s", + {'code': resp.status_code, + 'headers': resp.headers, + 'body': body}) + + +def _safe_encode_without_obj(data): + if isinstance(data, str): + 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) diff --git a/neutronclient/common/validators.py b/neutronclient/common/validators.py new file mode 100644 index 000000000..831d68e8f --- /dev/null +++ b/neutronclient/common/validators.py @@ -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 neutronclient._i18n import _ +from neutronclient.common import exceptions + + +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})) diff --git a/neutronclient/neutron/__init__.py b/neutronclient/neutron/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/neutron/client.py b/neutronclient/neutron/client.py new file mode 100644 index 000000000..d859d815d --- /dev/null +++ b/neutronclient/neutron/client.py @@ -0,0 +1,65 @@ +# 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 neutronclient.common import utils + + +API_NAME = 'network' +API_VERSIONS = { + '2.0': 'neutronclient.v2_0.client.Client', + '2': 'neutronclient.v2_0.client.Client', +} + + +def make_client(instance): + """Returns an neutron client.""" + neutron_client = utils.get_client_class( + API_NAME, + instance._api_version, + API_VERSIONS, + ) + instance.initialize() + url = instance._url + url = url.rstrip("/") + client = neutron_client(username=instance._username, + project_name=instance._project_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 + + +def Client(api_version, *args, **kwargs): + """Return an neutron client. + + @param api_version: only 2.0 is supported now + """ + neutron_client = utils.get_client_class( + API_NAME, + api_version, + API_VERSIONS, + ) + return neutron_client(*args, **kwargs) diff --git a/neutronclient/neutron/v2_0/__init__.py b/neutronclient/neutron/v2_0/__init__.py new file mode 100644 index 000000000..849a93d9e --- /dev/null +++ b/neutronclient/neutron/v2_0/__init__.py @@ -0,0 +1,826 @@ +# 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 abc +import argparse +import functools +import logging + +from cliff import command +from cliff import lister +from cliff import show +from oslo_serialization import jsonutils + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils + +HYPHEN_OPTS = ['tags_any', 'not_tags', 'not_tags_any'] + + +def find_resource_by_id(client, resource, resource_id, cmd_resource=None, + parent_id=None, fields=None): + return client.find_resource_by_id(resource, resource_id, cmd_resource, + parent_id, fields) + + +def find_resourceid_by_id(client, resource, resource_id, cmd_resource=None, + parent_id=None): + return find_resource_by_id(client, resource, resource_id, cmd_resource, + parent_id, fields='id')['id'] + + +def find_resource_by_name_or_id(client, resource, name_or_id, + project_id=None, cmd_resource=None, + parent_id=None, fields=None): + return client.find_resource(resource, name_or_id, project_id, + cmd_resource, parent_id, fields) + + +def find_resourceid_by_name_or_id(client, resource, name_or_id, + project_id=None, cmd_resource=None, + parent_id=None): + return find_resource_by_name_or_id(client, resource, name_or_id, + project_id, cmd_resource, + parent_id, fields='id')['id'] + + +def add_show_list_common_argument(parser): + parser.add_argument( + '-D', '--show-details', + help=_('Show detailed information.'), + 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 V2.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 + # dict of allowed types + allowed_type_dict = { + 'bool': utils.str2bool, + 'dict': utils.str2dict, + 'int': int, + 'str': str, + } + + 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] + if current_type_str in allowed_type_dict: + current_arg['type'] = allowed_type_dict[current_type_str] + continue + else: + raise exceptions.CommandError( + _("Invalid value_specs {valspec}: type {curtypestr}" + " is not supported").format( + valspec=' '.join(values_specs), + curtypestr=current_type_str)) + + 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 + + if _item.startswith('---'): + raise exceptions.CommandError( + _("Invalid values_specs %s") % ' '.join(values_specs)) + _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) + + +# cliff.command.Command is abstract class so that metaclass of +# subclass must be subclass of metaclass of all its base. +# otherwise metaclass conflict exception is raised. +class NeutronCommandMeta(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(NeutronCommandMeta, cls).__new__(cls, + name, bases, cls_dict) + + +class NeutronCommand(command.Command, metaclass=NeutronCommandMeta): + + values_specs = [] + json_indent = None + resource = None + shadow_resource = None + parent_id = None + + def run(self, parsed_args): + self.log.debug('run(%s)', parsed_args) + return super(NeutronCommand, self).run(parsed_args) + + @property + def cmd_resource(self): + if self.shadow_resource: + return self.shadow_resource + return self.resource + + def get_client(self): + return self.app.client_manager.neutron + + def get_parser(self, prog_name): + parser = super(NeutronCommand, self).get_parser(prog_name) + parser.add_argument( + '--request-format', + help=argparse.SUPPRESS, + default='json', + choices=['json', ], ) + parser.add_argument( + '--request_format', + choices=['json', ], + help=argparse.SUPPRESS) + + return parser + + def cleanup_output_data(self, data): + pass + + 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 set_extra_attrs(self, parsed_args): + pass + + def args2body(self, parsed_args): + return {} + + +class CreateCommand(NeutronCommand, show.ShowOne): + """Create a resource for a given tenant.""" + + log = None + + 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 take_action(self, parsed_args): + self.set_extra_attrs(parsed_args) + neutron_client = self.get_client() + _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(neutron_client, + "create_%s" % self.cmd_resource) + if self.parent_id: + data = obj_creator(self.parent_id, body) + else: + data = obj_creator(body) + self.cleanup_output_data(data) + if parsed_args.formatter == 'table': + self.format_output_data(data) + info = self.resource in data and data[self.resource] or None + if info: + if parsed_args.formatter == 'table': + print(_('Created a new %s:') % self.resource, + file=self.app.stdout) + else: + info = {'': ''} + return zip(*sorted(info.items())) + + +class UpdateCommand(NeutronCommand): + """Update resource's information.""" + + log = None + allow_names = True + help_resource = None + + def get_parser(self, prog_name): + parser = super(UpdateCommand, self).get_parser(prog_name) + if self.allow_names: + help_str = _('ID or name of %s to update.') + else: + help_str = _('ID of %s to update.') + if not self.help_resource: + self.help_resource = self.resource + parser.add_argument( + 'id', metavar=self.resource.upper(), + help=help_str % self.help_resource) + self.add_known_arguments(parser) + return parser + + def take_action(self, parsed_args): + self.set_extra_attrs(parsed_args) + neutron_client = self.get_client() + _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.cmd_resource) + if self.allow_names: + _id = find_resourceid_by_name_or_id( + neutron_client, self.resource, parsed_args.id, + cmd_resource=self.cmd_resource, parent_id=self.parent_id) + else: + _id = find_resourceid_by_id( + neutron_client, self.resource, parsed_args.id, + self.cmd_resource, self.parent_id) + obj_updater = getattr(neutron_client, + "update_%s" % self.cmd_resource) + if self.parent_id: + obj_updater(_id, self.parent_id, body) + else: + obj_updater(_id, body) + print((_('Updated %(resource)s: %(id)s') % + {'id': parsed_args.id, 'resource': self.resource}), + file=self.app.stdout) + return + + +class DeleteCommand(NeutronCommand): + """Delete a given resource.""" + + log = None + allow_names = True + help_resource = None + bulk_delete = True + + def get_parser(self, prog_name): + parser = super(DeleteCommand, self).get_parser(prog_name) + if not self.help_resource: + self.help_resource = self.resource + if self.allow_names: + help_str = _('ID(s) or name(s) of %s to delete.') + else: + help_str = _('ID(s) of %s to delete.') + parser.add_argument( + 'id', metavar=self.resource.upper(), + nargs='+' if self.bulk_delete else 1, + help=help_str % self.help_resource) + self.add_known_arguments(parser) + return parser + + def take_action(self, parsed_args): + self.set_extra_attrs(parsed_args) + neutron_client = self.get_client() + obj_deleter = getattr(neutron_client, + "delete_%s" % self.cmd_resource) + + if self.bulk_delete: + self._bulk_delete(obj_deleter, neutron_client, parsed_args.id) + else: + self.delete_item(obj_deleter, neutron_client, parsed_args.id) + print((_('Deleted %(resource)s: %(id)s') + % {'id': parsed_args.id, + 'resource': self.resource}), + file=self.app.stdout) + return + + def _bulk_delete(self, obj_deleter, neutron_client, parsed_args_ids): + successful_delete = [] + non_existent = [] + multiple_ids = [] + for item_id in parsed_args_ids: + try: + self.delete_item(obj_deleter, neutron_client, item_id) + successful_delete.append(item_id) + except exceptions.NotFound: + non_existent.append(item_id) + except exceptions.NeutronClientNoUniqueMatch: + multiple_ids.append(item_id) + if successful_delete: + print((_('Deleted %(resource)s(s): %(id)s')) + % {'id': ", ".join(successful_delete), + 'resource': self.cmd_resource}, + file=self.app.stdout) + if non_existent or multiple_ids: + err_msgs = [] + if non_existent: + err_msgs.append((_("Unable to find %(resource)s(s) with id(s) " + "'%(id)s'.") % + {'resource': self.cmd_resource, + 'id': ", ".join(non_existent)})) + if multiple_ids: + err_msgs.append((_("Multiple %(resource)s(s) matches found " + "for name(s) '%(id)s'. Please use an ID " + "to be more specific.") % + {'resource': self.cmd_resource, + 'id': ", ".join(multiple_ids)})) + raise exceptions.NeutronCLIError(message='\n'.join(err_msgs)) + + def delete_item(self, obj_deleter, neutron_client, item_id): + if self.allow_names: + params = {'cmd_resource': self.cmd_resource, + 'parent_id': self.parent_id} + _id = find_resourceid_by_name_or_id(neutron_client, + self.resource, + item_id, + **params) + else: + _id = item_id + + if self.parent_id: + obj_deleter(_id, self.parent_id) + else: + obj_deleter(_id) + return + + +class ListCommand(NeutronCommand, lister.Lister): + """List resources that belong to a given tenant.""" + + log = None + _formatters = {} + list_columns = [] + unknown_parts_flag = True + pagination_support = False + sorting_support = False + resource_plural = None + + # A list to define arguments for filtering by attribute value + # CLI arguments are shown in the order of this list. + # Each element must be either of a string of an attribute name + # or a dict of a full attribute definitions whose format is: + # {'name': attribute name, (mandatory) + # 'help': help message for CLI (mandatory) + # 'boolean': boolean parameter or not. (Default: False) (optional) + # 'argparse_kwargs': a dict of parameters passed to + # argparse add_argument() + # (Default: {}) (optional) + # } + # For more details, see ListNetworks.filter_attrs. + filter_attrs = [] + + default_attr_defs = { + 'name': { + 'help': _("Filter %s according to their name."), + 'boolean': False, + }, + 'tenant_id': { + 'help': _('Filter %s belonging to the given tenant.'), + 'boolean': False, + }, + 'admin_state_up': { + 'help': _('Filter and list the %s whose administrative ' + 'state is active'), + 'boolean': True, + }, + } + + 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) + self.add_known_arguments(parser) + self.add_filtering_arguments(parser) + return parser + + def add_filtering_arguments(self, parser): + if not self.filter_attrs: + return + + group_parser = parser.add_argument_group('filtering arguments') + collection = self.resource_plural or '%ss' % self.resource + for attr in self.filter_attrs: + if isinstance(attr, str): + # Use detail defined in default_attr_defs + attr_name = attr + attr_defs = self.default_attr_defs[attr] + else: + attr_name = attr['name'] + attr_defs = attr + option_name = '--%s' % attr_name.replace('_', '-') + params = attr_defs.get('argparse_kwargs', {}) + try: + help_msg = attr_defs['help'] % collection + except TypeError: + help_msg = attr_defs['help'] + if attr_defs.get('boolean', False): + add_arg_func = functools.partial(utils.add_boolean_argument, + group_parser) + else: + add_arg_func = group_parser.add_argument + add_arg_func(option_name, help=help_msg, **params) + + 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'}) + filter_attrs = [field if isinstance(field, str) else field['name'] + for field in self.filter_attrs] + for attr in filter_attrs: + val = getattr(parsed_args, attr, None) + if attr in HYPHEN_OPTS: + attr = attr.replace('_', '-') + if val: + search_opts[attr] = val + return search_opts + + def call_server(self, neutron_client, search_opts, parsed_args): + resource_plural = neutron_client.get_resource_plural(self.cmd_resource) + obj_lister = getattr(neutron_client, "list_%s" % resource_plural) + if self.parent_id: + data = obj_lister(self.parent_id, **search_opts) + else: + data = obj_lister(**search_opts) + return data + + def retrieve_list(self, parsed_args): + """Retrieve a list of resources from Neutron server.""" + neutron_client = self.get_client() + _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(neutron_client, search_opts, parsed_args) + collection = neutron_client.get_resource_plural(self.resource) + return data.get(collection, []) + + def extend_list(self, data, parsed_args): + """Update a retrieved list. + + This method provides a way to modify an original list returned from + the neutron server. For example, you can add subnet cidr information + to a network list. + """ + 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 = self._setup_columns_with_tenant_id(self.list_columns, + _columns) + + if parsed_args.formatter == 'table': + formatters = self._formatters + elif (parsed_args.formatter == 'csv' and + hasattr(self, '_formatters_csv')): + formatters = self._formatters_csv + else: + # For other formatters, we use raw value returned from neutron + formatters = {} + + return (_columns, (utils.get_item_properties( + s, _columns, formatters=formatters, ) + for s in info), ) + + def _setup_columns_with_tenant_id(self, display_columns, avail_columns): + _columns = [x for x in display_columns if x in avail_columns] + if 'tenant_id' in display_columns: + return _columns + if 'tenant_id' not in avail_columns: + return _columns + if not self.is_admin_role(): + return _columns + try: + pos_id = _columns.index('id') + except ValueError: + pos_id = 0 + try: + pos_name = _columns.index('name') + except ValueError: + pos_name = 0 + _columns.insert(max(pos_id, pos_name) + 1, 'tenant_id') + return _columns + + def is_admin_role(self): + client = self.get_client() + auth_ref = client.httpclient.get_auth_ref() + if not auth_ref: + return False + return 'admin' in auth_ref.role_names + + def take_action(self, parsed_args): + self.set_extra_attrs(parsed_args) + data = self.retrieve_list(parsed_args) + self.extend_list(data, parsed_args) + return self.setup_columns(data, parsed_args) + + +class ShowCommand(NeutronCommand, show.ShowOne): + """Show information of a given resource.""" + + log = None + allow_names = True + help_resource = None + + 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.') + if not self.help_resource: + self.help_resource = self.resource + parser.add_argument( + 'id', metavar=self.resource.upper(), + help=help_str % self.help_resource) + self.add_known_arguments(parser) + return parser + + def take_action(self, parsed_args): + self.set_extra_attrs(parsed_args) + neutron_client = self.get_client() + + 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(neutron_client, + self.resource, + parsed_args.id, + cmd_resource=self.cmd_resource, + parent_id=self.parent_id) + else: + _id = parsed_args.id + + obj_shower = getattr(neutron_client, "show_%s" % self.cmd_resource) + if self.parent_id: + data = obj_shower(_id, self.parent_id, **params) + else: + data = obj_shower(_id, **params) + self.cleanup_output_data(data) + if parsed_args.formatter == 'table': + self.format_output_data(data) + resource = data[self.resource] + if self.resource in data: + return zip(*sorted(resource.items())) + else: + return None diff --git a/neutronclient/neutron/v2_0/address_scope.py b/neutronclient/neutron/v2_0/address_scope.py new file mode 100644 index 000000000..73eba80f5 --- /dev/null +++ b/neutronclient/neutron/v2_0/address_scope.py @@ -0,0 +1,89 @@ +# Copyright 2015 Huawei Technologies India Pvt. Ltd.. +# 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 neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListAddressScope(neutronV20.ListCommand): + """List address scopes that belong to a given tenant.""" + + resource = 'address_scope' + list_columns = ['id', 'name', 'ip_version'] + pagination_support = True + sorting_support = True + + +class ShowAddressScope(neutronV20.ShowCommand): + """Show information about an address scope.""" + + resource = 'address_scope' + + +class CreateAddressScope(neutronV20.CreateCommand): + """Create an address scope for a given tenant.""" + + resource = 'address_scope' + + def add_known_arguments(self, parser): + parser.add_argument( + '--shared', + action='store_true', + help=_('Set the address scope as shared.')) + parser.add_argument( + 'name', + metavar='NAME', + help=_('Specify the name of the address scope.')) + parser.add_argument( + 'ip_version', + metavar='IP_VERSION', + type=int, + choices=[4, 6], + help=_('Specify the address family of the address scope.')) + + def args2body(self, parsed_args): + body = {'name': parsed_args.name, + 'ip_version': parsed_args.ip_version} + if parsed_args.shared: + body['shared'] = True + neutronV20.update_dict(parsed_args, body, ['tenant_id']) + return {self.resource: body} + + +class DeleteAddressScope(neutronV20.DeleteCommand): + """Delete an address scope.""" + + resource = 'address_scope' + + +class UpdateAddressScope(neutronV20.UpdateCommand): + """Update an address scope.""" + + resource = 'address_scope' + + def add_known_arguments(self, parser): + parser.add_argument('--name', + help=_('Updated name of the address scope.')) + utils.add_boolean_argument( + parser, '--shared', + help=_('Set sharing of address scope. ' + '(True means shared)')) + + def args2body(self, parsed_args): + body = {} + neutronV20.update_dict(parsed_args, body, ['name', 'shared']) + return {self.resource: body} diff --git a/neutronclient/neutron/v2_0/agent.py b/neutronclient/neutron/v2_0/agent.py new file mode 100644 index 000000000..36167ff66 --- /dev/null +++ b/neutronclient/neutron/v2_0/agent.py @@ -0,0 +1,78 @@ +# 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. +# + +from neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 + + +def _format_timestamp(component): + try: + return component['heartbeat_timestamp'].split(".", 2)[0] + except (TypeError, KeyError): + return '' + + +class ListAgent(neutronV20.ListCommand): + """List agents.""" + + resource = 'agent' + list_columns = ['id', 'agent_type', 'host', 'availability_zone', 'alive', + 'admin_state_up', 'binary'] + _formatters = {'heartbeat_timestamp': _format_timestamp} + sorting_support = True + + def extend_list(self, data, parsed_args): + for agent in data: + if 'alive' in agent: + agent['alive'] = ":-)" if agent['alive'] else 'xxx' + + +class ShowAgent(neutronV20.ShowCommand): + """Show information of a given agent.""" + + resource = 'agent' + allow_names = False + json_indent = 5 + + +class DeleteAgent(neutronV20.DeleteCommand): + """Delete a given agent.""" + + resource = 'agent' + allow_names = False + + +class UpdateAgent(neutronV20.UpdateCommand): + """Updates the admin status and description for a specified agent.""" + + resource = 'agent' + allow_names = False + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', + action='store_false', + help=_('Set admin state up of the agent to false.')) + parser.add_argument( + '--description', + help=_('Description for the agent.')) + + def args2body(self, parsed_args): + body = {'admin_state_up': parsed_args.admin_state} + neutronV20.update_dict(parsed_args, body, + ['description']) + return {self.resource: body} diff --git a/neutronclient/neutron/v2_0/agentscheduler.py b/neutronclient/neutron/v2_0/agentscheduler.py new file mode 100644 index 000000000..e92b10be1 --- /dev/null +++ b/neutronclient/neutron/v2_0/agentscheduler.py @@ -0,0 +1,341 @@ +# 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. +# + +from neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.neutron.v2_0 import network +from neutronclient.neutron.v2_0 import router + + +PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" + + +class AddNetworkToDhcpAgent(neutronV20.NeutronCommand): + """Add a network to a DHCP agent.""" + + def get_parser(self, prog_name): + parser = super(AddNetworkToDhcpAgent, self).get_parser(prog_name) + parser.add_argument( + 'dhcp_agent', + metavar='DHCP_AGENT', + help=_('ID of the DHCP agent.')) + parser.add_argument( + 'network', + metavar='NETWORK', + help=_('Network to add.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _net_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'network', parsed_args.network) + neutron_client.add_network_to_dhcp_agent(parsed_args.dhcp_agent, + {'network_id': _net_id}) + print(_('Added network %s to DHCP agent') % parsed_args.network, + file=self.app.stdout) + + +class RemoveNetworkFromDhcpAgent(neutronV20.NeutronCommand): + """Remove a network from a DHCP agent.""" + + def get_parser(self, prog_name): + parser = super(RemoveNetworkFromDhcpAgent, self).get_parser(prog_name) + parser.add_argument( + 'dhcp_agent', + metavar='DHCP_AGENT', + help=_('ID of the DHCP agent.')) + parser.add_argument( + 'network', + metavar='NETWORK', + help=_('Network to remove.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _net_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'network', parsed_args.network) + neutron_client.remove_network_from_dhcp_agent( + parsed_args.dhcp_agent, _net_id) + print(_('Removed network %s from DHCP agent') % parsed_args.network, + file=self.app.stdout) + + +class ListNetworksOnDhcpAgent(network.ListNetwork): + """List the networks on a DHCP agent.""" + + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListNetworksOnDhcpAgent, + self).get_parser(prog_name) + parser.add_argument( + 'dhcp_agent', + metavar='DHCP_AGENT', + help=_('ID of the DHCP agent.')) + return parser + + def call_server(self, neutron_client, search_opts, parsed_args): + data = neutron_client.list_networks_on_dhcp_agent( + parsed_args.dhcp_agent, **search_opts) + return data + + +class ListDhcpAgentsHostingNetwork(neutronV20.ListCommand): + """List DHCP agents hosting a network.""" + + resource = 'agent' + _formatters = {} + list_columns = ['id', 'host', 'admin_state_up', 'alive'] + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListDhcpAgentsHostingNetwork, + self).get_parser(prog_name) + parser.add_argument( + 'network', + metavar='NETWORK', + help=_('Network to query.')) + return parser + + def extend_list(self, data, parsed_args): + for agent in data: + agent['alive'] = ":-)" if agent['alive'] else 'xxx' + + def call_server(self, neutron_client, search_opts, parsed_args): + _id = neutronV20.find_resourceid_by_name_or_id(neutron_client, + 'network', + parsed_args.network) + search_opts['network'] = _id + data = neutron_client.list_dhcp_agent_hosting_networks(**search_opts) + return data + + +class AddRouterToL3Agent(neutronV20.NeutronCommand): + """Add a router to a L3 agent.""" + + def get_parser(self, prog_name): + parser = super(AddRouterToL3Agent, self).get_parser(prog_name) + parser.add_argument( + 'l3_agent', + metavar='L3_AGENT', + help=_('ID of the L3 agent.')) + parser.add_argument( + 'router', + metavar='ROUTER', + help=_('Router to add.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'router', parsed_args.router) + neutron_client.add_router_to_l3_agent(parsed_args.l3_agent, + {'router_id': _id}) + print(_('Added router %s to L3 agent') % parsed_args.router, + file=self.app.stdout) + + +class RemoveRouterFromL3Agent(neutronV20.NeutronCommand): + """Remove a router from a L3 agent.""" + + def get_parser(self, prog_name): + parser = super(RemoveRouterFromL3Agent, self).get_parser(prog_name) + parser.add_argument( + 'l3_agent', + metavar='L3_AGENT', + help=_('ID of the L3 agent.')) + parser.add_argument( + 'router', + metavar='ROUTER', + help=_('Router to remove.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'router', parsed_args.router) + neutron_client.remove_router_from_l3_agent( + parsed_args.l3_agent, _id) + print(_('Removed router %s from L3 agent') % parsed_args.router, + file=self.app.stdout) + + +class ListRoutersOnL3Agent(neutronV20.ListCommand): + """List the routers on a L3 agent.""" + + _formatters = {'external_gateway_info': + router._format_external_gateway_info} + list_columns = ['id', 'name', 'external_gateway_info'] + resource = 'router' + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListRoutersOnL3Agent, + self).get_parser(prog_name) + parser.add_argument( + 'l3_agent', + metavar='L3_AGENT', + help=_('ID of the L3 agent to query.')) + return parser + + def call_server(self, neutron_client, search_opts, parsed_args): + data = neutron_client.list_routers_on_l3_agent( + parsed_args.l3_agent, **search_opts) + return data + + +class ListL3AgentsHostingRouter(neutronV20.ListCommand): + """List L3 agents hosting a router.""" + + resource = 'agent' + _formatters = {} + list_columns = ['id', 'host', 'admin_state_up', 'alive'] + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListL3AgentsHostingRouter, + self).get_parser(prog_name) + parser.add_argument('router', + metavar='ROUTER', + help=_('Router to query.')) + return parser + + def extend_list(self, data, parsed_args): + # Show the ha_state column only if the server responds with it, + # as some plugins do not support HA routers. + if any('ha_state' in agent for agent in data): + if 'ha_state' not in self.list_columns: + self.list_columns.append('ha_state') + for agent in data: + agent['alive'] = ":-)" if agent['alive'] else 'xxx' + + def call_server(self, neutron_client, search_opts, parsed_args): + _id = neutronV20.find_resourceid_by_name_or_id(neutron_client, + 'router', + parsed_args.router) + search_opts['router'] = _id + data = neutron_client.list_l3_agent_hosting_routers(**search_opts) + return data + + +class ListPoolsOnLbaasAgent(neutronV20.ListCommand): + """List the pools on a loadbalancer agent.""" + + list_columns = ['id', 'name', 'lb_method', 'protocol', + 'admin_state_up', 'status'] + resource = 'pool' + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListPoolsOnLbaasAgent, self).get_parser(prog_name) + parser.add_argument( + 'lbaas_agent', + metavar='LBAAS_AGENT', + help=_('ID of the loadbalancer agent to query.')) + return parser + + def call_server(self, neutron_client, search_opts, parsed_args): + data = neutron_client.list_pools_on_lbaas_agent( + parsed_args.lbaas_agent, **search_opts) + return data + + +class GetLbaasAgentHostingPool(neutronV20.ListCommand): + """Get loadbalancer agent hosting a pool. + + Deriving from ListCommand though server will return only one agent + to keep common output format for all agent schedulers + """ + + resource = 'agent' + list_columns = ['id', 'host', 'admin_state_up', 'alive'] + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(GetLbaasAgentHostingPool, + self).get_parser(prog_name) + parser.add_argument('pool', + metavar='POOL', + help=_('Pool to query.')) + return parser + + def extend_list(self, data, parsed_args): + for agent in data: + agent['alive'] = ":-)" if agent['alive'] else 'xxx' + + def call_server(self, neutron_client, search_opts, parsed_args): + _id = neutronV20.find_resourceid_by_name_or_id(neutron_client, + 'pool', + parsed_args.pool) + search_opts['pool'] = _id + agent = neutron_client.get_lbaas_agent_hosting_pool(**search_opts) + data = {'agents': [agent['agent']]} + return data + + +class ListLoadBalancersOnLbaasAgent(neutronV20.ListCommand): + """List the loadbalancers on a loadbalancer v2 agent.""" + + list_columns = ['id', 'name', 'admin_state_up', 'provisioning_status'] + resource = 'loadbalancer' + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListLoadBalancersOnLbaasAgent, self).get_parser( + prog_name) + parser.add_argument( + 'lbaas_agent', + metavar='LBAAS_AGENT', + help=_('ID of the loadbalancer agent to query.')) + return parser + + def call_server(self, neutron_client, search_opts, parsed_args): + data = neutron_client.list_loadbalancers_on_lbaas_agent( + parsed_args.lbaas_agent, **search_opts) + return data + + +class GetLbaasAgentHostingLoadBalancer(neutronV20.ListCommand): + """Get lbaas v2 agent hosting a loadbalancer. + + Deriving from ListCommand though server will return only one agent + to keep common output format for all agent schedulers + """ + + resource = 'agent' + list_columns = ['id', 'host', 'admin_state_up', 'alive'] + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(GetLbaasAgentHostingLoadBalancer, + self).get_parser(prog_name) + parser.add_argument('loadbalancer', + metavar='LOADBALANCER', + help=_('LoadBalancer to query.')) + return parser + + def extend_list(self, data, parsed_args): + for agent in data: + agent['alive'] = ":-)" if agent['alive'] else 'xxx' + + def call_server(self, neutron_client, search_opts, parsed_args): + _id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'loadbalancer', parsed_args.loadbalancer) + search_opts['loadbalancer'] = _id + agent = neutron_client.get_lbaas_agent_hosting_loadbalancer( + **search_opts) + data = {'agents': [agent['agent']]} + return data diff --git a/neutronclient/neutron/v2_0/auto_allocated_topology.py b/neutronclient/neutron/v2_0/auto_allocated_topology.py new file mode 100644 index 000000000..a24959e16 --- /dev/null +++ b/neutronclient/neutron/v2_0/auto_allocated_topology.py @@ -0,0 +1,112 @@ +# Copyright 2016 IBM +# 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 + +from cliff import show +from oslo_serialization import jsonutils + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.neutron import v2_0 + + +class ShowAutoAllocatedTopology(v2_0.NeutronCommand, show.ShowOne): + """Show the auto-allocated topology of a given tenant.""" + + resource = 'auto_allocated_topology' + + def get_parser(self, prog_name): + parser = super(ShowAutoAllocatedTopology, self).get_parser(prog_name) + parser.add_argument( + '--dry-run', + help=_('Validate the requirements for auto-allocated-topology. ' + '(Does not return a topology.)'), + action='store_true') + parser.add_argument( + '--tenant-id', metavar='tenant-id', + help=_('The owner tenant ID.')) + # Allow people to do + # neutron auto-allocated-topology-show + # (Only useful to users who can look at other tenants' topologies.) + # We use a different name for this arg because the default will + # override whatever is in the named arg otherwise. + parser.add_argument( + 'pos_tenant_id', + help=argparse.SUPPRESS, nargs='?') + return parser + + def take_action(self, parsed_args): + client = self.get_client() + extra_values = v2_0.parse_args_to_dict(self.values_specs) + if extra_values: + raise exceptions.CommandError( + _("Invalid argument(s): --%s") % ', --'.join(extra_values)) + tenant_id = parsed_args.tenant_id or parsed_args.pos_tenant_id + if parsed_args.dry_run: + data = client.validate_auto_allocated_topology_requirements( + tenant_id) + else: + data = client.get_auto_allocated_topology(tenant_id) + if self.resource in data: + for k, v in data[self.resource].items(): + if isinstance(v, list): + value = "" + for _item in v: + if value: + value += "\n" + if isinstance(_item, dict): + value += jsonutils.dumps(_item) + else: + value += str(_item) + data[self.resource][k] = value + elif v == "dry-run=pass": + return ("dry-run",), ("pass",) + elif v is None: + data[self.resource][k] = '' + return zip(*sorted(data[self.resource].items())) + else: + return None + + +class DeleteAutoAllocatedTopology(v2_0.NeutronCommand): + """Delete the auto-allocated topology of a given tenant.""" + + resource = 'auto_allocated_topology' + + def get_parser(self, prog_name): + parser = super(DeleteAutoAllocatedTopology, self).get_parser(prog_name) + parser.add_argument( + '--tenant-id', metavar='tenant-id', + help=_('The owner tenant ID.')) + # Allow people to do + # neutron auto-allocated-topology-delete + # (Only useful to users who can look at other tenants' topologies.) + # We use a different name for this arg because the default will + # override whatever is in the named arg otherwise. + parser.add_argument( + 'pos_tenant_id', + help=argparse.SUPPRESS, nargs='?') + return parser + + def take_action(self, parsed_args): + client = self.get_client() + tenant_id = parsed_args.tenant_id or parsed_args.pos_tenant_id + client.delete_auto_allocated_topology(tenant_id) + # It tenant is None, let's be clear on what it means. + tenant_id = tenant_id or 'None (i.e. yours)' + print(_('Deleted topology for tenant %s.') % tenant_id, + file=self.app.stdout) diff --git a/neutronclient/neutron/v2_0/availability_zone.py b/neutronclient/neutron/v2_0/availability_zone.py new file mode 100644 index 000000000..2081d0990 --- /dev/null +++ b/neutronclient/neutron/v2_0/availability_zone.py @@ -0,0 +1,38 @@ +# 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 neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronv20 + + +def add_az_hint_argument(parser, resource): + parser.add_argument( + '--availability-zone-hint', metavar='AVAILABILITY_ZONE', + action='append', dest='availability_zone_hints', + help=_('Availability Zone for the %s ' + '(requires availability zone extension, ' + 'this option can be repeated).') % resource) + + +def args2body_az_hint(parsed_args, resource): + if parsed_args.availability_zone_hints: + resource['availability_zone_hints'] = ( + parsed_args.availability_zone_hints) + + +class ListAvailabilityZone(neutronv20.ListCommand): + """List availability zones.""" + + resource = 'availability_zone' + list_columns = ['name', 'resource', 'state'] + pagination_support = True + sorting_support = True diff --git a/neutronclient/neutron/v2_0/bgp/__init__.py b/neutronclient/neutron/v2_0/bgp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/neutron/v2_0/bgp/dragentscheduler.py b/neutronclient/neutron/v2_0/bgp/dragentscheduler.py new file mode 100644 index 000000000..d1e1de63f --- /dev/null +++ b/neutronclient/neutron/v2_0/bgp/dragentscheduler.py @@ -0,0 +1,115 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.neutron.v2_0.bgp import speaker as bgp_speaker + + +def add_common_args(parser): + parser.add_argument('dragent_id', + metavar='BGP_DRAGENT_ID', + help=_('ID of the Dynamic Routing agent.')) + parser.add_argument('bgp_speaker', + metavar='BGP_SPEAKER', + help=_('ID or name of the BGP speaker.')) + + +class AddBGPSpeakerToDRAgent(neutronV20.NeutronCommand): + """Add a BGP speaker to a Dynamic Routing agent.""" + + def get_parser(self, prog_name): + parser = super(AddBGPSpeakerToDRAgent, self).get_parser(prog_name) + add_common_args(parser) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _speaker_id = bgp_speaker.get_bgp_speaker_id(neutron_client, + parsed_args.bgp_speaker) + neutron_client.add_bgp_speaker_to_dragent( + parsed_args.dragent_id, {'bgp_speaker_id': _speaker_id}) + print(_('Associated BGP speaker %s to the Dynamic Routing agent.') + % parsed_args.bgp_speaker, file=self.app.stdout) + + +class RemoveBGPSpeakerFromDRAgent(neutronV20.NeutronCommand): + """Removes a BGP speaker from a Dynamic Routing agent.""" + + def get_parser(self, prog_name): + parser = super(RemoveBGPSpeakerFromDRAgent, self).get_parser( + prog_name) + add_common_args(parser) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _speaker_id = bgp_speaker.get_bgp_speaker_id(neutron_client, + parsed_args.bgp_speaker) + neutron_client.remove_bgp_speaker_from_dragent(parsed_args.dragent_id, + _speaker_id) + print(_('Disassociated BGP speaker %s from the ' + 'Dynamic Routing agent.') + % parsed_args.bgp_speaker, file=self.app.stdout) + + +class ListBGPSpeakersOnDRAgent(neutronV20.ListCommand): + """List BGP speakers hosted by a Dynamic Routing agent.""" + + list_columns = ['id', 'name', 'local_as', 'ip_version'] + resource = 'bgp_speaker' + + def get_parser(self, prog_name): + parser = super(ListBGPSpeakersOnDRAgent, + self).get_parser(prog_name) + parser.add_argument( + 'dragent_id', + metavar='BGP_DRAGENT_ID', + help=_('ID of the Dynamic Routing agent.')) + return parser + + def call_server(self, neutron_client, search_opts, parsed_args): + data = neutron_client.list_bgp_speaker_on_dragent( + parsed_args.dragent_id, **search_opts) + return data + + +class ListDRAgentsHostingBGPSpeaker(neutronV20.ListCommand): + """List Dynamic Routing agents hosting a BGP speaker.""" + + resource = 'agent' + _formatters = {} + list_columns = ['id', 'host', 'admin_state_up', 'alive'] + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListDRAgentsHostingBGPSpeaker, + self).get_parser(prog_name) + parser.add_argument('bgp_speaker', + metavar='BGP_SPEAKER', + help=_('ID or name of the BGP speaker.')) + return parser + + def extend_list(self, data, parsed_args): + for agent in data: + agent['alive'] = ":-)" if agent['alive'] else 'xxx' + + def call_server(self, neutron_client, search_opts, parsed_args): + _speaker_id = bgp_speaker.get_bgp_speaker_id(neutron_client, + parsed_args.bgp_speaker) + search_opts['bgp_speaker'] = _speaker_id + data = neutron_client.list_dragents_hosting_bgp_speaker(**search_opts) + return data diff --git a/neutronclient/neutron/v2_0/bgp/peer.py b/neutronclient/neutron/v2_0/bgp/peer.py new file mode 100644 index 000000000..8fefb660a --- /dev/null +++ b/neutronclient/neutron/v2_0/bgp/peer.py @@ -0,0 +1,127 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.common import validators +from neutronclient.neutron import v2_0 as neutronv20 + + +def get_bgp_peer_id(client, id_or_name): + return neutronv20.find_resourceid_by_name_or_id(client, + 'bgp_peer', + id_or_name) + + +def validate_peer_attributes(parsed_args): + # Validate AS number + validators.validate_int_range(parsed_args, 'remote_as', + neutronv20.bgp.speaker.MIN_AS_NUM, + neutronv20.bgp.speaker.MAX_AS_NUM) + # Validate password + if parsed_args.auth_type != 'none' and parsed_args.password is None: + raise exceptions.CommandError(_('Must provide password if auth-type ' + 'is specified.')) + if parsed_args.auth_type == 'none' and parsed_args.password: + raise exceptions.CommandError(_('Must provide auth-type if password ' + 'is specified.')) + + +class ListPeers(neutronv20.ListCommand): + """List BGP peers.""" + + resource = 'bgp_peer' + list_columns = ['id', 'name', 'peer_ip', 'remote_as'] + pagination_support = True + sorting_support = True + + +class ShowPeer(neutronv20.ShowCommand): + """Show information of a given BGP peer.""" + + resource = 'bgp_peer' + + +class CreatePeer(neutronv20.CreateCommand): + """Create a BGP Peer.""" + + resource = 'bgp_peer' + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', + metavar='NAME', + help=_('Name of the BGP peer to create.')) + parser.add_argument( + '--peer-ip', + metavar='PEER_IP_ADDRESS', + required=True, + help=_('Peer IP address.')) + parser.add_argument( + '--remote-as', + required=True, + metavar='PEER_REMOTE_AS', + help=_('Peer AS number. (Integer in [%(min_val)s, %(max_val)s] ' + 'is allowed.)') % + {'min_val': neutronv20.bgp.speaker.MIN_AS_NUM, + 'max_val': neutronv20.bgp.speaker.MAX_AS_NUM}) + parser.add_argument( + '--auth-type', + metavar='PEER_AUTH_TYPE', + choices=['none', 'md5'], + default='none', + type=utils.convert_to_lowercase, + help=_('Authentication algorithm. Supported algorithms: ' + 'none(default), md5')) + parser.add_argument( + '--password', + metavar='AUTH_PASSWORD', + help=_('Authentication password.')) + + def args2body(self, parsed_args): + body = {} + validate_peer_attributes(parsed_args) + neutronv20.update_dict(parsed_args, body, + ['name', 'peer_ip', + 'remote_as', 'auth_type', 'password']) + return {self.resource: body} + + +class UpdatePeer(neutronv20.UpdateCommand): + """Update BGP Peer's information.""" + + resource = 'bgp_peer' + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help=_('Updated name of the BGP peer.')) + parser.add_argument( + '--password', + metavar='AUTH_PASSWORD', + help=_('Updated authentication password.')) + + def args2body(self, parsed_args): + body = {} + neutronv20.update_dict(parsed_args, body, ['name', 'password']) + return {self.resource: body} + + +class DeletePeer(neutronv20.DeleteCommand): + """Delete a BGP peer.""" + + resource = 'bgp_peer' diff --git a/neutronclient/neutron/v2_0/bgp/speaker.py b/neutronclient/neutron/v2_0/bgp/speaker.py new file mode 100644 index 000000000..34ce22eb8 --- /dev/null +++ b/neutronclient/neutron/v2_0/bgp/speaker.py @@ -0,0 +1,275 @@ +# Copyright 2016 Huawei Technologies India Pvt. Ltd. +# 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 neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.common import validators +from neutronclient.neutron import v2_0 as neutronv20 +from neutronclient.neutron.v2_0.bgp import peer as bgp_peer + +# Allowed BGP Autonomous number range +MIN_AS_NUM = 1 +MAX_AS_NUM = 4294967295 + + +def get_network_id(client, id_or_name): + return neutronv20.find_resourceid_by_name_or_id(client, + 'network', + id_or_name) + + +def get_bgp_speaker_id(client, id_or_name): + return neutronv20.find_resourceid_by_name_or_id(client, + 'bgp_speaker', + id_or_name) + + +def validate_speaker_attributes(parsed_args): + # Validate AS number + validators.validate_int_range(parsed_args, 'local_as', + MIN_AS_NUM, MAX_AS_NUM) + + +def add_common_arguments(parser): + utils.add_boolean_argument( + parser, '--advertise-floating-ip-host-routes', + help=_('Whether to enable or disable the advertisement ' + 'of floating-ip host routes by the BGP speaker. ' + 'By default floating ip host routes will be ' + 'advertised by the BGP speaker.')) + utils.add_boolean_argument( + parser, '--advertise-tenant-networks', + help=_('Whether to enable or disable the advertisement ' + 'of tenant network routes by the BGP speaker. ' + 'By default tenant network routes will be ' + 'advertised by the BGP speaker.')) + + +def args2body_common_arguments(body, parsed_args): + neutronv20.update_dict(parsed_args, body, + ['name', + 'advertise_floating_ip_host_routes', + 'advertise_tenant_networks']) + + +class ListSpeakers(neutronv20.ListCommand): + """List BGP speakers.""" + + resource = 'bgp_speaker' + list_columns = ['id', 'name', 'local_as', 'ip_version'] + pagination_support = True + sorting_support = True + + +class ShowSpeaker(neutronv20.ShowCommand): + """Show information of a given BGP speaker.""" + + resource = 'bgp_speaker' + + +class CreateSpeaker(neutronv20.CreateCommand): + """Create a BGP Speaker.""" + + resource = 'bgp_speaker' + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', + metavar='NAME', + help=_('Name of the BGP speaker to create.')) + parser.add_argument( + '--local-as', + metavar='LOCAL_AS', + required=True, + help=_('Local AS number. (Integer in [%(min_val)s, %(max_val)s] ' + 'is allowed.)') % {'min_val': MIN_AS_NUM, + 'max_val': MAX_AS_NUM}) + parser.add_argument( + '--ip-version', + type=int, choices=[4, 6], + default=4, + help=_('IP version for the BGP speaker (default is 4).')) + add_common_arguments(parser) + + def args2body(self, parsed_args): + body = {} + validate_speaker_attributes(parsed_args) + body['local_as'] = parsed_args.local_as + body['ip_version'] = parsed_args.ip_version + args2body_common_arguments(body, parsed_args) + return {self.resource: body} + + +class UpdateSpeaker(neutronv20.UpdateCommand): + """Update BGP Speaker's information.""" + + resource = 'bgp_speaker' + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help=_('Name of the BGP speaker to update.')) + add_common_arguments(parser) + + def args2body(self, parsed_args): + body = {} + args2body_common_arguments(body, parsed_args) + return {self.resource: body} + + +class DeleteSpeaker(neutronv20.DeleteCommand): + """Delete a BGP speaker.""" + + resource = 'bgp_speaker' + + +class AddPeerToSpeaker(neutronv20.NeutronCommand): + """Add a peer to the BGP speaker.""" + + def get_parser(self, prog_name): + parser = super(AddPeerToSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='BGP_SPEAKER', + help=_('ID or name of the BGP speaker.')) + parser.add_argument( + 'bgp_peer', + metavar='BGP_PEER', + help=_('ID or name of the BGP peer to add.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _speaker_id = get_bgp_speaker_id(neutron_client, + parsed_args.bgp_speaker) + _peer_id = bgp_peer.get_bgp_peer_id(neutron_client, + parsed_args.bgp_peer) + neutron_client.add_peer_to_bgp_speaker(_speaker_id, + {'bgp_peer_id': _peer_id}) + print(_('Added BGP peer %(peer)s to BGP speaker %(speaker)s.') % + {'peer': parsed_args.bgp_peer, + 'speaker': parsed_args.bgp_speaker}, + file=self.app.stdout) + + +class RemovePeerFromSpeaker(neutronv20.NeutronCommand): + """Remove a peer from the BGP speaker.""" + + def get_parser(self, prog_name): + parser = super(RemovePeerFromSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='BGP_SPEAKER', + help=_('ID or name of the BGP speaker.')) + parser.add_argument( + 'bgp_peer', + metavar='BGP_PEER', + help=_('ID or name of the BGP peer to remove.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _speaker_id = get_bgp_speaker_id(neutron_client, + parsed_args.bgp_speaker) + _peer_id = bgp_peer.get_bgp_peer_id(neutron_client, + parsed_args.bgp_peer) + neutron_client.remove_peer_from_bgp_speaker(_speaker_id, + {'bgp_peer_id': _peer_id}) + print(_('Removed BGP peer %(peer)s from BGP speaker %(speaker)s.') % + {'peer': parsed_args.bgp_peer, + 'speaker': parsed_args.bgp_speaker}, + file=self.app.stdout) + + +class AddNetworkToSpeaker(neutronv20.NeutronCommand): + """Add a network to the BGP speaker.""" + + def get_parser(self, prog_name): + parser = super(AddNetworkToSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='BGP_SPEAKER', + help=_('ID or name of the BGP speaker.')) + parser.add_argument( + 'network', + metavar='NETWORK', + help=_('ID or name of the network to add.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _speaker_id = get_bgp_speaker_id(neutron_client, + parsed_args.bgp_speaker) + _net_id = get_network_id(neutron_client, + parsed_args.network) + neutron_client.add_network_to_bgp_speaker(_speaker_id, + {'network_id': _net_id}) + print(_('Added network %(net)s to BGP speaker %(speaker)s.') % + {'net': parsed_args.network, 'speaker': parsed_args.bgp_speaker}, + file=self.app.stdout) + + +class RemoveNetworkFromSpeaker(neutronv20.NeutronCommand): + """Remove a network from the BGP speaker.""" + + def get_parser(self, prog_name): + parser = super(RemoveNetworkFromSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='BGP_SPEAKER', + help=_('ID or name of the BGP speaker.')) + parser.add_argument( + 'network', + metavar='NETWORK', + help=_('ID or name of the network to remove.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _speaker_id = get_bgp_speaker_id(neutron_client, + parsed_args.bgp_speaker) + _net_id = get_network_id(neutron_client, + parsed_args.network) + neutron_client.remove_network_from_bgp_speaker(_speaker_id, + {'network_id': _net_id}) + print(_('Removed network %(net)s from BGP speaker %(speaker)s.') % + {'net': parsed_args.network, 'speaker': parsed_args.bgp_speaker}, + file=self.app.stdout) + + +class ListRoutesAdvertisedBySpeaker(neutronv20.ListCommand): + """List routes advertised by a given BGP speaker.""" + + list_columns = ['id', 'destination', 'next_hop'] + resource = 'advertised_route' + pagination_support = True + sorting_support = True + + def get_parser(self, prog_name): + parser = super(ListRoutesAdvertisedBySpeaker, + self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='BGP_SPEAKER', + help=_('ID or name of the BGP speaker.')) + return parser + + def call_server(self, neutron_client, search_opts, parsed_args): + _speaker_id = get_bgp_speaker_id(neutron_client, + parsed_args.bgp_speaker) + data = neutron_client.list_route_advertised_from_bgp_speaker( + _speaker_id, **search_opts) + return data diff --git a/neutronclient/neutron/v2_0/contrib/__init__.py b/neutronclient/neutron/v2_0/contrib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/neutron/v2_0/contrib/_fox_sockets.py b/neutronclient/neutron/v2_0/contrib/_fox_sockets.py new file mode 100644 index 000000000..1ff5ffd2b --- /dev/null +++ b/neutronclient/neutron/v2_0/contrib/_fox_sockets.py @@ -0,0 +1,93 @@ +# 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 neutronclient._i18n import _ +from neutronclient.common import extension + + +def _add_updatable_args(parser): + parser.add_argument( + 'name', + help=_('Name of this fox socket.')) + + +def _updatable_args2body(parsed_args, body, client): + if parsed_args.name: + body['name'] = parsed_args.name + + +class FoxInSocket(extension.NeutronClientExtension): + """Define required variables for resource operations.""" + + resource = 'fox_socket' + resource_plural = '%ss' % resource + object_path = '/%s' % resource_plural + resource_path = '/%s/%%s' % resource_plural + versions = ['2.0'] + + +class FoxInSocketsList(extension.ClientExtensionList, FoxInSocket): + """List fox sockets.""" + + shell_command = 'fox-sockets-list' + list_columns = ['id', 'name'] + pagination_support = True + sorting_support = True + + +class FoxInSocketsCreate(extension.ClientExtensionCreate, FoxInSocket): + """Create a fox socket.""" + + shell_command = 'fox-sockets-create' + list_columns = ['id', 'name'] + + def add_known_arguments(self, parser): + _add_updatable_args(parser) + + def args2body(self, parsed_args): + body = {} + client = self.get_client() + _updatable_args2body(parsed_args, body, client) + return {'fox_socket': body} + + +class FoxInSocketsUpdate(extension.ClientExtensionUpdate, FoxInSocket): + """Update a fox socket.""" + + shell_command = 'fox-sockets-update' + list_columns = ['id', 'name'] + + def add_known_arguments(self, parser): + # _add_updatable_args(parser) + parser.add_argument( + '--name', + help=_('Name of this fox socket.')) + + def args2body(self, parsed_args): + body = {'name': parsed_args.name} + return {'fox_socket': body} + + +class FoxInSocketsDelete(extension.ClientExtensionDelete, FoxInSocket): + """Delete a fox socket.""" + + shell_command = 'fox-sockets-delete' + + +class FoxInSocketsShow(extension.ClientExtensionShow, FoxInSocket): + """Show a fox socket.""" + + shell_command = 'fox-sockets-show' diff --git a/neutronclient/neutron/v2_0/dns.py b/neutronclient/neutron/v2_0/dns.py new file mode 100644 index 000000000..0cf7ec0b4 --- /dev/null +++ b/neutronclient/neutron/v2_0/dns.py @@ -0,0 +1,67 @@ +# Copyright (c) 2016 IBM +# 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 neutronclient._i18n import _ + + +def add_dns_argument_create(parser, resource, attribute): + # Add dns_name and dns_domain support to network, port and floatingip + # create + argument = '--dns-%s' % attribute + parser.add_argument( + argument, + help=_('Assign DNS %(attribute)s to the %(resource)s ' + '(requires DNS integration ' + 'extension)') % {'attribute': attribute, 'resource': resource}) + + +def args2body_dns_create(parsed_args, resource, attribute): + # Add dns_name and dns_domain support to network, port and floatingip + # create + destination = 'dns_%s' % attribute + argument = getattr(parsed_args, destination) + if argument: + resource[destination] = argument + + +def add_dns_argument_update(parser, resource, attribute): + # Add dns_name and dns_domain support to network, port and floatingip + # update + argument = '--dns-%s' % attribute + no_argument = '--no-dns-%s' % attribute + dns_args = parser.add_mutually_exclusive_group() + dns_args.add_argument( + argument, + help=_('Assign DNS %(attribute)s to the %(resource)s ' + '(requires DNS integration ' + 'extension.)') % {'attribute': attribute, 'resource': resource}) + dns_args.add_argument( + no_argument, action='store_true', + help=_('Unassign DNS %(attribute)s from the %(resource)s ' + '(requires DNS integration ' + 'extension.)') % {'attribute': attribute, 'resource': resource}) + + +def args2body_dns_update(parsed_args, resource, attribute): + # Add dns_name and dns_domain support to network, port and floatingip + # update + destination = 'dns_%s' % attribute + no_destination = 'no_dns_%s' % attribute + argument = getattr(parsed_args, destination) + no_argument = getattr(parsed_args, no_destination) + if argument: + resource[destination] = argument + if no_argument: + resource[destination] = "" diff --git a/neutronclient/neutron/v2_0/extension.py b/neutronclient/neutron/v2_0/extension.py new file mode 100644 index 000000000..24905d59d --- /dev/null +++ b/neutronclient/neutron/v2_0/extension.py @@ -0,0 +1,31 @@ +# 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 neutronclient.neutron import v2_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 diff --git a/neutronclient/neutron/v2_0/flavor/__init__.py b/neutronclient/neutron/v2_0/flavor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/neutron/v2_0/flavor/flavor.py b/neutronclient/neutron/v2_0/flavor/flavor.py new file mode 100644 index 000000000..36a052ef2 --- /dev/null +++ b/neutronclient/neutron/v2_0/flavor/flavor.py @@ -0,0 +1,163 @@ +# 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 argparse + +from neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListFlavor(neutronV20.ListCommand): + """List Neutron service flavors.""" + + resource = 'flavor' + list_columns = ['id', 'name', 'service_type', 'enabled'] + pagination_support = True + sorting_support = True + + +class ShowFlavor(neutronV20.ShowCommand): + """Show information about a given Neutron service flavor.""" + + resource = 'flavor' + + +class CreateFlavor(neutronV20.CreateCommand): + """Create a Neutron service flavor.""" + + resource = 'flavor' + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', + metavar='NAME', + help=_('Name for the flavor.')) + parser.add_argument( + 'service_type', + metavar='SERVICE_TYPE', + help=_('Service type to which the flavor applies to: e.g. VPN. ' + '(See service-provider-list for loaded examples.)')) + parser.add_argument( + '--description', + help=_('Description for the flavor.')) + utils.add_boolean_argument( + parser, + '--enabled', + default=argparse.SUPPRESS, + help=_('Sets enabled flag.')) + + def args2body(self, parsed_args): + body = {} + neutronV20.update_dict(parsed_args, body, + ['name', 'description', 'service_type', + 'enabled']) + return {self.resource: body} + + +class DeleteFlavor(neutronV20.DeleteCommand): + """Delete a given Neutron service flavor.""" + + resource = 'flavor' + + +class UpdateFlavor(neutronV20.UpdateCommand): + """Update a Neutron service flavor.""" + + resource = 'flavor' + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help=_('Name for the flavor.')) + parser.add_argument( + '--description', + help=_('Description for the flavor.')) + utils.add_boolean_argument( + parser, + '--enabled', + default=argparse.SUPPRESS, + help=_('Sets enabled flag.')) + + def args2body(self, parsed_args): + body = {} + neutronV20.update_dict(parsed_args, body, + ['name', 'description', 'enabled']) + return {self.resource: body} + + +class AssociateFlavor(neutronV20.NeutronCommand): + """Associate a Neutron service flavor with a flavor profile.""" + + resource = 'flavor' + + def get_parser(self, prog_name): + parser = super(AssociateFlavor, self).get_parser(prog_name) + parser.add_argument( + 'flavor', + metavar='FLAVOR', + help=_('ID or name of the flavor to associate.')) + parser.add_argument( + 'flavor_profile', + metavar='FLAVOR_PROFILE', + help=_('ID of the flavor profile to be associated with the ' + 'flavor.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + flavor_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'flavor', parsed_args.flavor) + service_profile_id = neutronV20.find_resourceid_by_id( + neutron_client, 'service_profile', parsed_args.flavor_profile) + body = {'service_profile': {'id': service_profile_id}} + neutron_client.associate_flavor(flavor_id, body) + print((_('Associated flavor %(flavor)s with ' + 'flavor_profile %(profile)s') % + {'flavor': parsed_args.flavor, + 'profile': parsed_args.flavor_profile}), + file=self.app.stdout) + + +class DisassociateFlavor(neutronV20.NeutronCommand): + """Disassociate a Neutron service flavor from a flavor profile.""" + + resource = 'flavor' + + def get_parser(self, prog_name): + parser = super(DisassociateFlavor, self).get_parser(prog_name) + parser.add_argument( + 'flavor', + metavar='FLAVOR', + help=_('ID or name of the flavor to be disassociated.')) + parser.add_argument( + 'flavor_profile', + metavar='FLAVOR_PROFILE', + help=_('ID of the flavor profile to be disassociated from the ' + 'flavor.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + flavor_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'flavor', parsed_args.flavor) + service_profile_id = neutronV20.find_resourceid_by_id( + neutron_client, 'service_profile', parsed_args.flavor_profile) + neutron_client.disassociate_flavor(flavor_id, service_profile_id) + print((_('Disassociated flavor %(flavor)s from ' + 'flavor_profile %(profile)s') % + {'flavor': parsed_args.flavor, + 'profile': parsed_args.flavor_profile}), + file=self.app.stdout) diff --git a/neutronclient/neutron/v2_0/flavor/flavor_profile.py b/neutronclient/neutron/v2_0/flavor/flavor_profile.py new file mode 100644 index 000000000..894a8a67c --- /dev/null +++ b/neutronclient/neutron/v2_0/flavor/flavor_profile.py @@ -0,0 +1,99 @@ +# 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 argparse + +from neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListFlavorProfile(neutronV20.ListCommand): + """List Neutron service flavor profiles.""" + + resource = 'service_profile' + list_columns = ['id', 'description', 'enabled', 'metainfo'] + pagination_support = True + sorting_support = True + + +class ShowFlavorProfile(neutronV20.ShowCommand): + """Show information about a given Neutron service flavor profile.""" + + resource = 'service_profile' + + +class CreateFlavorProfile(neutronV20.CreateCommand): + """Create a Neutron service flavor profile.""" + + resource = 'service_profile' + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description for the flavor profile.')) + parser.add_argument( + '--driver', + help=_('Python module path to driver.')) + parser.add_argument( + '--metainfo', + help=_('Metainfo for the flavor profile.')) + utils.add_boolean_argument( + parser, + '--enabled', + default=argparse.SUPPRESS, + help=_('Sets enabled flag.')) + + def args2body(self, parsed_args): + body = {} + neutronV20.update_dict(parsed_args, body, + ['description', 'driver', 'enabled', + 'metainfo']) + return {self.resource: body} + + +class DeleteFlavorProfile(neutronV20.DeleteCommand): + """Delete a given Neutron service flavor profile.""" + + resource = 'service_profile' + + +class UpdateFlavorProfile(neutronV20.UpdateCommand): + """Update a given Neutron service flavor profile.""" + + resource = 'service_profile' + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description for the flavor profile.')) + parser.add_argument( + '--driver', + help=_('Python module path to driver.')) + parser.add_argument( + '--metainfo', + help=_('Metainfo for the flavor profile.')) + utils.add_boolean_argument( + parser, + '--enabled', + default=argparse.SUPPRESS, + help=_('Sets enabled flag.')) + + def args2body(self, parsed_args): + body = {} + neutronV20.update_dict(parsed_args, body, + ['description', 'driver', 'enabled', + 'metainfo']) + return {self.resource: body} diff --git a/neutronclient/neutron/v2_0/floatingip.py b/neutronclient/neutron/v2_0/floatingip.py new file mode 100644 index 000000000..5f4ced40a --- /dev/null +++ b/neutronclient/neutron/v2_0/floatingip.py @@ -0,0 +1,148 @@ +# 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 argparse + +from neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.neutron.v2_0 import dns + + +class ListFloatingIP(neutronV20.ListCommand): + """List floating IPs that belong to a given tenant.""" + + resource = 'floatingip' + list_columns = ['id', 'fixed_ip_address', 'floating_ip_address', + 'port_id'] + pagination_support = True + sorting_support = True + + +class ShowFloatingIP(neutronV20.ShowCommand): + """Show information of a given floating IP.""" + + resource = 'floatingip' + allow_names = False + + +class CreateFloatingIP(neutronV20.CreateCommand): + """Create a floating IP for a given tenant.""" + + resource = 'floatingip' + + def add_known_arguments(self, parser): + parser.add_argument( + 'floating_network_id', metavar='FLOATING_NETWORK', + help=_('ID or name of the network from which ' + 'the floating IP is allocated.')) + parser.add_argument( + '--description', + help=_('Description of the floating IP.')) + parser.add_argument( + '--port-id', + help=_('ID of the port to be associated with the floating IP.')) + parser.add_argument( + '--port_id', + help=argparse.SUPPRESS) + parser.add_argument( + '--fixed-ip-address', + help=_('IP address on the port (only required if port has ' + 'multiple IPs).')) + parser.add_argument( + '--fixed_ip_address', + help=argparse.SUPPRESS) + parser.add_argument( + '--floating-ip-address', + help=_('IP address of the floating IP')) + parser.add_argument( + '--subnet', + dest='subnet_id', + help=_('Subnet ID on which you want to create the floating IP.')) + dns.add_dns_argument_create(parser, self.resource, 'domain') + dns.add_dns_argument_create(parser, self.resource, 'name') + + def args2body(self, parsed_args): + _network_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'network', parsed_args.floating_network_id) + body = {'floating_network_id': _network_id} + neutronV20.update_dict(parsed_args, body, + ['port_id', 'tenant_id', + 'fixed_ip_address', 'description', + 'floating_ip_address', 'subnet_id']) + dns.args2body_dns_create(parsed_args, body, 'domain') + dns.args2body_dns_create(parsed_args, body, 'name') + return {self.resource: body} + + +class DeleteFloatingIP(neutronV20.DeleteCommand): + """Delete a given floating IP.""" + + resource = 'floatingip' + allow_names = False + + +class AssociateFloatingIP(neutronV20.NeutronCommand): + """Create a mapping between a floating IP and a fixed IP.""" + + resource = 'floatingip' + + def get_parser(self, prog_name): + parser = super(AssociateFloatingIP, self).get_parser(prog_name) + parser.add_argument( + 'floatingip_id', metavar='FLOATINGIP_ID', + help=_('ID of the floating IP to associate.')) + parser.add_argument( + 'port_id', metavar='PORT', + help=_('ID or name of the port to be associated with the ' + 'floating IP.')) + parser.add_argument( + '--fixed-ip-address', + help=_('IP address on the port (only required if port has ' + 'multiple IPs).')) + parser.add_argument( + '--fixed_ip_address', + help=argparse.SUPPRESS) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + update_dict = {} + neutronV20.update_dict(parsed_args, update_dict, + ['port_id', 'fixed_ip_address']) + neutron_client.update_floatingip(parsed_args.floatingip_id, + {'floatingip': update_dict}) + print(_('Associated floating IP %s') % parsed_args.floatingip_id, + file=self.app.stdout) + + +class DisassociateFloatingIP(neutronV20.NeutronCommand): + """Remove a mapping from a floating IP to a fixed IP.""" + + resource = 'floatingip' + + def get_parser(self, prog_name): + parser = super(DisassociateFloatingIP, self).get_parser(prog_name) + parser.add_argument( + 'floatingip_id', metavar='FLOATINGIP_ID', + help=_('ID of the floating IP to disassociate.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + neutron_client.update_floatingip(parsed_args.floatingip_id, + {'floatingip': {'port_id': None}}) + print(_('Disassociated floating IP %s') % parsed_args.floatingip_id, + file=self.app.stdout) diff --git a/neutronclient/neutron/v2_0/fw/__init__.py b/neutronclient/neutron/v2_0/fw/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/neutron/v2_0/fw/firewall.py b/neutronclient/neutron/v2_0/fw/firewall.py new file mode 100644 index 000000000..c04660a3b --- /dev/null +++ b/neutronclient/neutron/v2_0/fw/firewall.py @@ -0,0 +1,128 @@ +# Copyright 2013 Big Switch Networks +# 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 neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronv20 + + +def add_common_args(parser): + parser.add_argument( + '--name', + help=_('Name for the firewall.')) + parser.add_argument( + '--description', + help=_('Description for the firewall.')) + router = parser.add_mutually_exclusive_group() + router.add_argument( + '--router', + dest='routers', + metavar='ROUTER', + action='append', + help=_('ID or name of the router associated with the firewall ' + '(requires FWaaS router insertion extension to be enabled). ' + 'This option can be repeated.')) + router.add_argument( + '--no-routers', + action='store_true', + help=_('Associate no routers with the firewall (requires FWaaS ' + 'router insertion extension).')) + + +def parse_common_args(client, parsed_args): + body = {} + if parsed_args.policy: + body['firewall_policy_id'] = neutronv20.find_resourceid_by_name_or_id( + client, 'firewall_policy', + parsed_args.policy) + + if parsed_args.routers: + body['router_ids'] = [ + neutronv20.find_resourceid_by_name_or_id(client, 'router', r) + for r in parsed_args.routers] + elif parsed_args.no_routers: + body['router_ids'] = [] + + neutronv20.update_dict(parsed_args, body, + ['name', 'description']) + return body + + +class ListFirewall(neutronv20.ListCommand): + """List firewalls that belong to a given tenant.""" + + resource = 'firewall' + list_columns = ['id', 'name', 'firewall_policy_id'] + _formatters = {} + pagination_support = True + sorting_support = True + + +class ShowFirewall(neutronv20.ShowCommand): + """Show information of a given firewall.""" + + resource = 'firewall' + + +class CreateFirewall(neutronv20.CreateCommand): + """Create a firewall.""" + + resource = 'firewall' + + def add_known_arguments(self, parser): + add_common_args(parser) + parser.add_argument( + 'policy', metavar='POLICY', + help=_('ID or name of the firewall policy ' + 'associated to this firewall.')) + parser.add_argument( + '--admin-state-down', + dest='admin_state', + action='store_false', + help=_('Set admin state up to false.')) + + def args2body(self, parsed_args): + body = parse_common_args(self.get_client(), parsed_args) + neutronv20.update_dict(parsed_args, body, ['tenant_id']) + body['admin_state_up'] = parsed_args.admin_state + return {self.resource: body} + + +class UpdateFirewall(neutronv20.UpdateCommand): + """Update a given firewall.""" + + resource = 'firewall' + + def add_known_arguments(self, parser): + add_common_args(parser) + parser.add_argument( + '--policy', metavar='POLICY', + help=_('ID or name of the firewall policy ' + 'associated to this firewall.')) + utils.add_boolean_argument( + parser, '--admin-state-up', dest='admin_state_up', + help=_('Update the admin state for the firewall ' + '(True means UP).')) + + def args2body(self, parsed_args): + body = parse_common_args(self.get_client(), parsed_args) + neutronv20.update_dict(parsed_args, body, ['admin_state_up']) + return {self.resource: body} + + +class DeleteFirewall(neutronv20.DeleteCommand): + """Delete a given firewall.""" + + resource = 'firewall' diff --git a/neutronclient/neutron/v2_0/fw/firewallpolicy.py b/neutronclient/neutron/v2_0/fw/firewallpolicy.py new file mode 100644 index 000000000..3d9331cd6 --- /dev/null +++ b/neutronclient/neutron/v2_0/fw/firewallpolicy.py @@ -0,0 +1,227 @@ +# Copyright 2013 Big Switch Networks +# 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 + +from neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronv20 + + +def _format_firewall_rules(firewall_policy): + try: + output = '[' + ',\n '.join([rule for rule in + firewall_policy['firewall_rules']]) + ']' + return output + except (TypeError, KeyError): + return '' + + +def add_common_args(parser): + parser.add_argument( + '--description', + help=_('Description for the firewall policy.')) + parser.add_argument( + '--firewall-rules', type=lambda x: x.split(), + help=_('Ordered list of whitespace-delimited firewall rule ' + 'names or IDs; e.g., --firewall-rules \"rule1 rule2\"')) + + +def parse_common_args(client, parsed_args): + if parsed_args.firewall_rules: + _firewall_rules = [] + for f in parsed_args.firewall_rules: + _firewall_rules.append( + neutronv20.find_resourceid_by_name_or_id( + client, 'firewall_rule', f)) + body = {'firewall_rules': _firewall_rules} + else: + body = {} + neutronv20.update_dict(parsed_args, body, + ['name', 'description', 'shared', + 'audited', 'tenant_id']) + return {'firewall_policy': body} + + +class ListFirewallPolicy(neutronv20.ListCommand): + """List firewall policies that belong to a given tenant.""" + + resource = 'firewall_policy' + list_columns = ['id', 'name', 'firewall_rules'] + _formatters = {'firewall_rules': _format_firewall_rules, + } + pagination_support = True + sorting_support = True + + +class ShowFirewallPolicy(neutronv20.ShowCommand): + """Show information of a given firewall policy.""" + + resource = 'firewall_policy' + + +class CreateFirewallPolicy(neutronv20.CreateCommand): + """Create a firewall policy.""" + + resource = 'firewall_policy' + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', + metavar='NAME', + help=_('Name for the firewall policy.')) + parser.add_argument( + '--shared', + action='store_true', + help=_('Create a shared policy.'), + default=argparse.SUPPRESS) + parser.add_argument( + '--audited', + action='store_true', + help=_('Sets audited to True.'), + default=argparse.SUPPRESS) + add_common_args(parser) + + def args2body(self, parsed_args): + return parse_common_args(self.get_client(), parsed_args) + + +class UpdateFirewallPolicy(neutronv20.UpdateCommand): + """Update a given firewall policy.""" + + resource = 'firewall_policy' + + def add_known_arguments(self, parser): + add_common_args(parser) + parser.add_argument( + '--name', + help=_('Name for the firewall policy.')) + utils.add_boolean_argument( + parser, '--shared', + help=_('Update the sharing status of the policy. ' + '(True means shared).')) + utils.add_boolean_argument( + parser, '--audited', + help=_('Update the audit status of the policy. ' + '(True means auditing is enabled).')) + + def args2body(self, parsed_args): + return parse_common_args(self.get_client(), parsed_args) + + +class DeleteFirewallPolicy(neutronv20.DeleteCommand): + """Delete a given firewall policy.""" + + resource = 'firewall_policy' + + +class FirewallPolicyInsertRule(neutronv20.UpdateCommand): + """Insert a rule into a given firewall policy.""" + + resource = 'firewall_policy' + + def call_api(self, neutron_client, firewall_policy_id, body): + return neutron_client.firewall_policy_insert_rule(firewall_policy_id, + body) + + def args2body(self, parsed_args): + _rule = '' + if parsed_args.firewall_rule_id: + _rule = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'firewall_rule', + parsed_args.firewall_rule_id) + _insert_before = '' + if 'insert_before' in parsed_args: + if parsed_args.insert_before: + _insert_before = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'firewall_rule', + parsed_args.insert_before) + _insert_after = '' + if 'insert_after' in parsed_args: + if parsed_args.insert_after: + _insert_after = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'firewall_rule', + parsed_args.insert_after) + body = {'firewall_rule_id': _rule, + 'insert_before': _insert_before, + 'insert_after': _insert_after} + return body + + def get_parser(self, prog_name): + parser = super(FirewallPolicyInsertRule, self).get_parser(prog_name) + parser.add_argument( + '--insert-before', + metavar='FIREWALL_RULE', + help=_('Insert before this rule.')) + parser.add_argument( + '--insert-after', + metavar='FIREWALL_RULE', + help=_('Insert after this rule.')) + parser.add_argument( + 'firewall_rule_id', + metavar='FIREWALL_RULE', + help=_('New rule to insert.')) + self.add_known_arguments(parser) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + body = self.args2body(parsed_args) + _id = neutronv20.find_resourceid_by_name_or_id(neutron_client, + self.resource, + parsed_args.id) + self.call_api(neutron_client, _id, body) + print((_('Inserted firewall rule in firewall policy %(id)s') % + {'id': parsed_args.id}), file=self.app.stdout) + + +class FirewallPolicyRemoveRule(neutronv20.UpdateCommand): + """Remove a rule from a given firewall policy.""" + + resource = 'firewall_policy' + + def call_api(self, neutron_client, firewall_policy_id, body): + return neutron_client.firewall_policy_remove_rule(firewall_policy_id, + body) + + def args2body(self, parsed_args): + _rule = '' + if parsed_args.firewall_rule_id: + _rule = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'firewall_rule', + parsed_args.firewall_rule_id) + body = {'firewall_rule_id': _rule} + return body + + def get_parser(self, prog_name): + parser = super(FirewallPolicyRemoveRule, self).get_parser(prog_name) + parser.add_argument( + 'firewall_rule_id', + metavar='FIREWALL_RULE', + help=_('ID or name of the firewall rule to be removed ' + 'from the policy.')) + self.add_known_arguments(parser) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + body = self.args2body(parsed_args) + _id = neutronv20.find_resourceid_by_name_or_id(neutron_client, + self.resource, + parsed_args.id) + self.call_api(neutron_client, _id, body) + print((_('Removed firewall rule from firewall policy %(id)s') % + {'id': parsed_args.id}), file=self.app.stdout) diff --git a/neutronclient/neutron/v2_0/fw/firewallrule.py b/neutronclient/neutron/v2_0/fw/firewallrule.py new file mode 100644 index 000000000..df5db95d6 --- /dev/null +++ b/neutronclient/neutron/v2_0/fw/firewallrule.py @@ -0,0 +1,168 @@ +# Copyright 2013 Big Switch Networks +# 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 + +from neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronv20 + + +def _add_common_args(parser, is_create=True): + """If is_create is True, protocol and action become mandatory arguments. + + CreateCommand = is_create : True + UpdateCommand = is_create : False + """ + parser.add_argument( + '--name', + help=_('Name for the firewall rule.')) + parser.add_argument( + '--description', + help=_('Description for the firewall rule.')) + parser.add_argument( + '--source-ip-address', + help=_('Source IP address or subnet.')) + parser.add_argument( + '--destination-ip-address', + help=_('Destination IP address or subnet.')) + parser.add_argument( + '--source-port', + help=_('Source port (integer in [1, 65535] or range in a:b).')) + parser.add_argument( + '--destination-port', + help=_('Destination port (integer in [1, 65535] or range in ' + 'a:b).')) + utils.add_boolean_argument( + parser, '--enabled', dest='enabled', + help=_('Whether to enable or disable this rule.')) + parser.add_argument( + '--protocol', choices=['tcp', 'udp', 'icmp', 'any'], + required=is_create, + type=utils.convert_to_lowercase, + help=_('Protocol for the firewall rule.')) + parser.add_argument( + '--action', + required=is_create, + type=utils.convert_to_lowercase, + choices=['allow', 'deny', 'reject'], + help=_('Action for the firewall rule.')) + + +def common_args2body(parsed_args): + body = {} + neutronv20.update_dict(parsed_args, body, + ['name', 'description', 'shared', 'tenant_id', + 'source_ip_address', 'destination_ip_address', + 'source_port', 'destination_port', 'action', + 'enabled', 'ip_version']) + protocol = parsed_args.protocol + if protocol: + if protocol == 'any': + protocol = None + body['protocol'] = protocol + return body + + +class ListFirewallRule(neutronv20.ListCommand): + """List firewall rules that belong to a given tenant.""" + + resource = 'firewall_rule' + list_columns = ['id', 'name', 'firewall_policy_id', 'summary', 'enabled'] + pagination_support = True + sorting_support = True + + def extend_list(self, data, parsed_args): + for d in data: + val = [] + if d.get('protocol'): + protocol = d['protocol'].upper() + else: + protocol = 'no-protocol' + val.append(protocol) + if 'source_ip_address' in d and 'source_port' in d: + src = 'source: ' + str(d['source_ip_address']).lower() + src = src + '(' + str(d['source_port']).lower() + ')' + else: + src = 'source: none specified' + val.append(src) + if 'destination_ip_address' in d and 'destination_port' in d: + dst = 'dest: ' + str(d['destination_ip_address']).lower() + dst = dst + '(' + str(d['destination_port']).lower() + ')' + else: + dst = 'dest: none specified' + val.append(dst) + if 'action' in d: + action = d['action'] + else: + action = 'no-action' + val.append(action) + d['summary'] = ',\n '.join(val) + + +class ShowFirewallRule(neutronv20.ShowCommand): + """Show information of a given firewall rule.""" + + resource = 'firewall_rule' + + +class CreateFirewallRule(neutronv20.CreateCommand): + """Create a firewall rule.""" + + resource = 'firewall_rule' + + def add_known_arguments(self, parser): + parser.add_argument( + '--shared', + action='store_true', + help=_('Set shared flag for the firewall rule.'), + default=argparse.SUPPRESS) + _add_common_args(parser) + parser.add_argument( + '--ip-version', + type=int, choices=[4, 6], default=4, + help=_('IP version for the firewall rule (default is 4).')) + + def args2body(self, parsed_args): + return {self.resource: common_args2body(parsed_args)} + + +class UpdateFirewallRule(neutronv20.UpdateCommand): + """Update a given firewall rule.""" + + resource = 'firewall_rule' + + def add_known_arguments(self, parser): + utils.add_boolean_argument( + parser, + '--shared', + dest='shared', + help=_('Update the shared flag for the firewall rule.'), + default=argparse.SUPPRESS) + parser.add_argument( + '--ip-version', + type=int, choices=[4, 6], + help=_('Update IP version for the firewall rule.')) + _add_common_args(parser, is_create=False) + + def args2body(self, parsed_args): + return {self.resource: common_args2body(parsed_args)} + + +class DeleteFirewallRule(neutronv20.DeleteCommand): + """Delete a given firewall rule.""" + + resource = 'firewall_rule' diff --git a/neutronclient/neutron/v2_0/lb/__init__.py b/neutronclient/neutron/v2_0/lb/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/neutron/v2_0/lb/healthmonitor.py b/neutronclient/neutron/v2_0/lb/healthmonitor.py new file mode 100644 index 000000000..33dfbf0bc --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/healthmonitor.py @@ -0,0 +1,161 @@ +# Copyright 2013 Mirantis 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 neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListHealthMonitor(neutronV20.ListCommand): + """List health monitors that belong to a given tenant.""" + + resource = 'health_monitor' + list_columns = ['id', 'type', 'admin_state_up'] + pagination_support = True + sorting_support = True + + +class ShowHealthMonitor(neutronV20.ShowCommand): + """Show information of a given health monitor.""" + + resource = 'health_monitor' + allow_names = False + + +class CreateHealthMonitor(neutronV20.CreateCommand): + """Create a health monitor.""" + + resource = 'health_monitor' + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--expected-codes', + help=_('The list of HTTP status codes expected in ' + 'response from the member to declare it healthy. This ' + 'attribute can contain one value, ' + 'or a list of values separated by comma, ' + 'or a range of values (e.g. "200-299"). If this attribute ' + 'is not specified, it defaults to "200".')) + parser.add_argument( + '--http-method', + help=_('The HTTP method used for requests by the monitor of type ' + 'HTTP.')) + parser.add_argument( + '--url-path', + help=_('The HTTP path used in the HTTP request used by the monitor' + ' to test a member health. This must be a string ' + 'beginning with a / (forward slash).')) + parser.add_argument( + '--delay', + required=True, + help=_('The time in seconds between sending probes to members.')) + parser.add_argument( + '--max-retries', + required=True, + help=_('Number of permissible connection failures before changing ' + 'the member status to INACTIVE. [1..10]')) + parser.add_argument( + '--timeout', + required=True, + help=_('Maximum number of seconds for a monitor to wait for a ' + 'connection to be established before it times out. The ' + 'value must be less than the delay value.')) + parser.add_argument( + '--type', + required=True, choices=['PING', 'TCP', 'HTTP', 'HTTPS'], + help=_('One of the predefined health monitor types.')) + + def args2body(self, parsed_args): + body = {'admin_state_up': parsed_args.admin_state, + 'delay': parsed_args.delay, + 'max_retries': parsed_args.max_retries, + 'timeout': parsed_args.timeout, + 'type': parsed_args.type} + neutronV20.update_dict(parsed_args, body, + ['expected_codes', 'http_method', 'url_path', + 'tenant_id']) + return {self.resource: body} + + +class UpdateHealthMonitor(neutronV20.UpdateCommand): + """Update a given health monitor.""" + + resource = 'health_monitor' + allow_names = False + + +class DeleteHealthMonitor(neutronV20.DeleteCommand): + """Delete a given health monitor.""" + + resource = 'health_monitor' + allow_names = False + + +class AssociateHealthMonitor(neutronV20.NeutronCommand): + """Create a mapping between a health monitor and a pool.""" + + resource = 'health_monitor' + + def get_parser(self, prog_name): + parser = super(AssociateHealthMonitor, self).get_parser(prog_name) + parser.add_argument( + 'health_monitor_id', metavar='HEALTH_MONITOR_ID', + help=_('Health monitor to associate.')) + parser.add_argument( + 'pool_id', metavar='POOL', + help=_('ID of the pool to be associated with the health monitor.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + body = {'health_monitor': {'id': parsed_args.health_monitor_id}} + pool_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'pool', parsed_args.pool_id) + neutron_client.associate_health_monitor(pool_id, body) + print((_('Associated health monitor ' + '%s') % parsed_args.health_monitor_id), + file=self.app.stdout) + + +class DisassociateHealthMonitor(neutronV20.NeutronCommand): + """Remove a mapping from a health monitor to a pool.""" + + resource = 'health_monitor' + + def get_parser(self, prog_name): + parser = super(DisassociateHealthMonitor, self).get_parser(prog_name) + parser.add_argument( + 'health_monitor_id', metavar='HEALTH_MONITOR_ID', + help=_('Health monitor to disassociate.')) + parser.add_argument( + 'pool_id', metavar='POOL', + help=_('ID of the pool to be disassociated with the health ' + 'monitor.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + pool_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'pool', parsed_args.pool_id) + neutron_client.disassociate_health_monitor(pool_id, + parsed_args + .health_monitor_id) + print((_('Disassociated health monitor ' + '%s') % parsed_args.health_monitor_id), + file=self.app.stdout) diff --git a/neutronclient/neutron/v2_0/lb/member.py b/neutronclient/neutron/v2_0/lb/member.py new file mode 100644 index 000000000..c90840cd1 --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/member.py @@ -0,0 +1,87 @@ +# Copyright 2013 Mirantis 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 neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListMember(neutronV20.ListCommand): + """List members that belong to a given tenant.""" + + resource = 'member' + list_columns = [ + 'id', 'address', 'protocol_port', 'weight', 'admin_state_up', 'status' + ] + pagination_support = True + sorting_support = True + + +class ShowMember(neutronV20.ShowCommand): + """Show information of a given member.""" + + resource = 'member' + allow_names = False + + +class CreateMember(neutronV20.CreateCommand): + """Create a member.""" + + resource = 'member' + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--weight', + help=_('Weight of pool member in the pool (default:1, [0..256]).')) + parser.add_argument( + '--address', + required=True, + help=_('IP address of the pool member on the pool network.')) + parser.add_argument( + '--protocol-port', + required=True, + help=_('Port on which the pool member listens for requests or ' + 'connections.')) + parser.add_argument( + 'pool_id', metavar='POOL', + help=_('ID or name of the pool this vip belongs to.')) + + def args2body(self, parsed_args): + _pool_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'pool', parsed_args.pool_id) + body = {'pool_id': _pool_id, + 'admin_state_up': parsed_args.admin_state} + neutronV20.update_dict( + parsed_args, + body, + ['address', 'protocol_port', 'weight', 'tenant_id'] + ) + return {self.resource: body} + + +class UpdateMember(neutronV20.UpdateCommand): + """Update a given member.""" + + resource = 'member' + + +class DeleteMember(neutronV20.DeleteCommand): + """Delete a given member.""" + + resource = 'member' diff --git a/neutronclient/neutron/v2_0/lb/pool.py b/neutronclient/neutron/v2_0/lb/pool.py new file mode 100644 index 000000000..5684e8e6a --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/pool.py @@ -0,0 +1,121 @@ +# Copyright 2013 Mirantis 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 neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 + + +def _format_provider(pool): + return pool.get('provider') or 'N/A' + + +class ListPool(neutronV20.ListCommand): + """List pools that belong to a given tenant.""" + + resource = 'pool' + list_columns = ['id', 'name', 'provider', 'lb_method', 'protocol', + 'admin_state_up', 'status'] + _formatters = {'provider': _format_provider} + pagination_support = True + sorting_support = True + + +class ShowPool(neutronV20.ShowCommand): + """Show information of a given pool.""" + + resource = 'pool' + + +class CreatePool(neutronV20.CreateCommand): + """Create a pool.""" + + resource = 'pool' + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--description', + help=_('Description of the pool.')) + parser.add_argument( + '--lb-method', + required=True, + choices=['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'], + help=_('The algorithm used to distribute load between the members ' + 'of the pool.')) + parser.add_argument( + '--name', + required=True, + help=_('The name of the pool.')) + parser.add_argument( + '--protocol', + required=True, + choices=['HTTP', 'HTTPS', 'TCP'], + help=_('Protocol for balancing.')) + parser.add_argument( + '--subnet-id', metavar='SUBNET', + required=True, + help=_('The subnet on which the members of the pool will be ' + 'located.')) + parser.add_argument( + '--provider', + help=_('Provider name of the loadbalancer service.')) + + def args2body(self, parsed_args): + _subnet_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'subnet', parsed_args.subnet_id) + body = {'admin_state_up': parsed_args.admin_state, + 'subnet_id': _subnet_id} + neutronV20.update_dict(parsed_args, body, + ['description', 'lb_method', 'name', + 'protocol', 'tenant_id', 'provider']) + return {self.resource: body} + + +class UpdatePool(neutronV20.UpdateCommand): + """Update a given pool.""" + + resource = 'pool' + + +class DeletePool(neutronV20.DeleteCommand): + """Delete a given pool.""" + + resource = 'pool' + + +class RetrievePoolStats(neutronV20.ShowCommand): + """Retrieve stats for a given pool.""" + + resource = 'pool' + + def take_action(self, parsed_args): + neutron_client = self.get_client() + pool_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'pool', parsed_args.id) + params = {} + if parsed_args.fields: + params = {'fields': parsed_args.fields} + + data = neutron_client.retrieve_pool_stats(pool_id, **params) + self.format_output_data(data) + stats = data['stats'] + if 'stats' in data: + return zip(*sorted(stats.items())) + else: + return None diff --git a/neutronclient/neutron/v2_0/lb/v2/__init__.py b/neutronclient/neutron/v2_0/lb/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/neutron/v2_0/lb/v2/healthmonitor.py b/neutronclient/neutron/v2_0/lb/v2/healthmonitor.py new file mode 100644 index 000000000..34e72c842 --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/v2/healthmonitor.py @@ -0,0 +1,143 @@ +# Copyright 2013 Mirantis Inc. +# Copyright 2014 Blue Box Group, 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 neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _add_common_args(parser, is_create=True): + parser.add_argument( + '--delay', + required=is_create, + help=_('The time in seconds between sending probes to members.')) + parser.add_argument( + '--name', + help=_('Name of the health monitor.')) + parser.add_argument( + '--timeout', + required=is_create, + help=_('Maximum number of seconds for a monitor to wait for a ' + 'connection to be established before it times out. The ' + 'value must be less than the delay value.')) + parser.add_argument( + '--http-method', + type=utils.convert_to_uppercase, + help=_('The HTTP method used for requests by the monitor of type ' + 'HTTP.')) + parser.add_argument( + '--url-path', + help=_('The HTTP path used in the HTTP request used by the monitor ' + 'to test a member health. This must be a string ' + 'beginning with a / (forward slash).')) + parser.add_argument( + '--max-retries', + required=is_create, + help=_('Number of permissible connection failures before changing ' + 'the member status to INACTIVE. [1..10].')) + parser.add_argument( + '--expected-codes', + help=_('The list of HTTP status codes expected in ' + 'response from the member to declare it healthy. This ' + 'attribute can contain one value, ' + 'or a list of values separated by comma, ' + 'or a range of values (e.g. "200-299"). If this attribute ' + 'is not specified, it defaults to "200".')) + + +def _parse_common_args(body, parsed_args): + neutronV20.update_dict(parsed_args, body, + ['expected_codes', 'http_method', 'url_path', + 'timeout', 'name', 'delay', 'max_retries']) + + +class ListHealthMonitor(neutronV20.ListCommand): + """LBaaS v2 List healthmonitors that belong to a given tenant.""" + + resource = 'healthmonitor' + shadow_resource = 'lbaas_healthmonitor' + list_columns = ['id', 'name', 'type', 'admin_state_up'] + pagination_support = True + sorting_support = True + + +class ShowHealthMonitor(neutronV20.ShowCommand): + """LBaaS v2 Show information of a given healthmonitor.""" + + resource = 'healthmonitor' + shadow_resource = 'lbaas_healthmonitor' + + +class CreateHealthMonitor(neutronV20.CreateCommand): + """LBaaS v2 Create a healthmonitor.""" + + resource = 'healthmonitor' + shadow_resource = 'lbaas_healthmonitor' + + def add_known_arguments(self, parser): + _add_common_args(parser) + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--type', + required=True, choices=['PING', 'TCP', 'HTTP', 'HTTPS'], + help=_('One of the predefined health monitor types.')) + parser.add_argument( + '--pool', required=True, + help=_('ID or name of the pool that this healthmonitor will ' + 'monitor.')) + + def args2body(self, parsed_args): + pool_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'pool', parsed_args.pool, + cmd_resource='lbaas_pool') + body = {'admin_state_up': parsed_args.admin_state, + 'type': parsed_args.type, + 'pool_id': pool_id} + neutronV20.update_dict(parsed_args, body, + ['tenant_id']) + _parse_common_args(body, parsed_args) + return {self.resource: body} + + +class UpdateHealthMonitor(neutronV20.UpdateCommand): + """LBaaS v2 Update a given healthmonitor.""" + + resource = 'healthmonitor' + shadow_resource = 'lbaas_healthmonitor' + + def add_known_arguments(self, parser): + _add_common_args(parser, is_create=False) + utils.add_boolean_argument( + parser, '--admin-state-up', + help=_('Update the administrative state of ' + 'the health monitor (True meaning "Up").')) + + def args2body(self, parsed_args): + body = {} + _parse_common_args(body, parsed_args) + neutronV20.update_dict(parsed_args, body, + ['admin_state_up']) + return {self.resource: body} + + +class DeleteHealthMonitor(neutronV20.DeleteCommand): + """LBaaS v2 Delete a given healthmonitor.""" + + resource = 'healthmonitor' + shadow_resource = 'lbaas_healthmonitor' diff --git a/neutronclient/neutron/v2_0/lb/v2/l7policy.py b/neutronclient/neutron/v2_0/lb/v2/l7policy.py new file mode 100644 index 000000000..26c331554 --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/v2/l7policy.py @@ -0,0 +1,155 @@ +# Copyright 2016 Radware LTD. +# 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 neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _get_listener_id(client, listener_id_or_name): + return neutronV20.find_resourceid_by_name_or_id( + client, 'listener', listener_id_or_name) + + +def _get_pool_id(client, pool_id_or_name): + return neutronV20.find_resourceid_by_name_or_id( + client, 'pool', pool_id_or_name, cmd_resource='lbaas_pool') + + +def _add_common_args(parser, is_create=True): + parser.add_argument( + '--name', + help=_('Name of the policy.')) + parser.add_argument( + '--description', + help=_('Description of the policy.')) + parser.add_argument( + '--action', + required=is_create, + metavar='ACTION', + type=utils.convert_to_uppercase, + choices=['REJECT', 'REDIRECT_TO_POOL', 'REDIRECT_TO_URL'], + help=_('Action type of the policy.')) + parser.add_argument( + '--redirect-pool', + help=_('ID or name of the pool for REDIRECT_TO_POOL action type.')) + parser.add_argument( + '--redirect-url', + help=_('URL for REDIRECT_TO_URL action type. ' + 'This should be a valid URL string.')) + parser.add_argument( + '--position', + type=int, + help=_('L7 policy position in ordered policies list. ' + 'This must be an integer starting from 1. ' + 'Not specifying the position will place the policy ' + 'at the tail of existing policies list.')) + + +def _common_args2body(client, parsed_args, is_create=True): + if parsed_args.redirect_url: + if parsed_args.action != 'REDIRECT_TO_URL': + raise exceptions.CommandError(_('Action must be REDIRECT_TO_URL')) + if parsed_args.redirect_pool: + if parsed_args.action != 'REDIRECT_TO_POOL': + raise exceptions.CommandError(_('Action must be REDIRECT_TO_POOL')) + parsed_args.redirect_pool_id = _get_pool_id( + client, parsed_args.redirect_pool) + if (parsed_args.action == 'REDIRECT_TO_URL' and + not parsed_args.redirect_url): + raise exceptions.CommandError(_('Redirect URL must be specified')) + if (parsed_args.action == 'REDIRECT_TO_POOL' and + not parsed_args.redirect_pool): + raise exceptions.CommandError(_('Redirect pool must be specified')) + + attributes = ['name', 'description', + 'action', 'redirect_pool_id', 'redirect_url', + 'position', 'admin_state_up'] + if is_create: + parsed_args.listener_id = _get_listener_id( + client, parsed_args.listener) + attributes.extend(['listener_id', 'tenant_id']) + body = {} + neutronV20.update_dict(parsed_args, body, attributes) + return {'l7policy': body} + + +class ListL7Policy(neutronV20.ListCommand): + """LBaaS v2 List L7 policies that belong to a given listener.""" + + resource = 'l7policy' + shadow_resource = 'lbaas_l7policy' + pagination_support = True + sorting_support = True + list_columns = [ + 'id', 'name', 'action', 'redirect_pool_id', 'redirect_url', + 'position', 'admin_state_up', 'status' + ] + + +class ShowL7Policy(neutronV20.ShowCommand): + """LBaaS v2 Show information of a given L7 policy.""" + + resource = 'l7policy' + shadow_resource = 'lbaas_l7policy' + + +class CreateL7Policy(neutronV20.CreateCommand): + """LBaaS v2 Create L7 policy.""" + + resource = 'l7policy' + shadow_resource = 'lbaas_l7policy' + + def add_known_arguments(self, parser): + _add_common_args(parser) + parser.add_argument( + '--admin-state-down', + dest='admin_state_up', + action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--listener', + required=True, + metavar='LISTENER', + help=_('ID or name of the listener this policy belongs to.')) + + def args2body(self, parsed_args): + return _common_args2body(self.get_client(), parsed_args) + + +class UpdateL7Policy(neutronV20.UpdateCommand): + """LBaaS v2 Update a given L7 policy.""" + + resource = 'l7policy' + shadow_resource = 'lbaas_l7policy' + + def add_known_arguments(self, parser): + _add_common_args(parser, is_create=False) + utils.add_boolean_argument( + parser, '--admin-state-up', + help=_('Specify the administrative state of the policy' + ' (True meaning "Up").')) + + def args2body(self, parsed_args): + return _common_args2body(self.get_client(), parsed_args, False) + + +class DeleteL7Policy(neutronV20.DeleteCommand): + """LBaaS v2 Delete a given L7 policy.""" + + resource = 'l7policy' + shadow_resource = 'lbaas_l7policy' diff --git a/neutronclient/neutron/v2_0/lb/v2/l7rule.py b/neutronclient/neutron/v2_0/lb/v2/l7rule.py new file mode 100644 index 000000000..3c468cabf --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/v2/l7rule.py @@ -0,0 +1,148 @@ +# Copyright 2016 Radware LTD. +# 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 neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _get_policy_id(client, policy_id_or_name): + return neutronV20.find_resourceid_by_name_or_id( + client, 'l7policy', policy_id_or_name, + cmd_resource='lbaas_l7policy') + + +class LbaasL7RuleMixin(object): + + def set_extra_attrs(self, parsed_args): + self.parent_id = _get_policy_id(self.get_client(), + parsed_args.l7policy) + + def add_known_arguments(self, parser): + parser.add_argument( + 'l7policy', metavar='L7POLICY', + help=_('ID or name of L7 policy this rule belongs to.')) + + +def _add_common_args(parser, is_create=True): + parser.add_argument( + '--type', + required=is_create, + type=utils.convert_to_uppercase, + choices=['HOST_NAME', 'PATH', 'FILE_TYPE', 'HEADER', 'COOKIE'], + help=_('Rule type.')) + parser.add_argument( + '--compare-type', + required=is_create, + type=utils.convert_to_uppercase, + choices=['REGEX', 'STARTS_WITH', 'ENDS_WITH', + 'CONTAINS', 'EQUAL_TO'], + help=_('Rule compare type.')) + parser.add_argument( + '--invert-compare', + dest='invert', + action='store_true', + help=_('Invert the compare type.')) + parser.add_argument( + '--key', + help=_('Key to compare.' + ' Relevant for HEADER and COOKIE types only.')) + parser.add_argument( + '--value', + required=is_create, + help=_('Value to compare.')) + + +def _common_args2body(client, parsed_args, is_create=True): + attributes = ['type', 'compare_type', + 'invert', 'key', 'value', 'admin_state_up'] + if is_create: + attributes.append('tenant_id') + body = {} + neutronV20.update_dict(parsed_args, body, attributes) + return {'rule': body} + + +class ListL7Rule(LbaasL7RuleMixin, neutronV20.ListCommand): + """LBaaS v2 List L7 rules that belong to a given L7 policy.""" + + resource = 'rule' + shadow_resource = 'lbaas_l7rule' + pagination_support = True + sorting_support = True + + list_columns = [ + 'id', 'type', 'compare_type', 'invert', 'key', 'value', + 'admin_state_up', 'status' + ] + + def take_action(self, parsed_args): + self.parent_id = _get_policy_id(self.get_client(), + parsed_args.l7policy) + self.values_specs.append('--l7policy_id=%s' % self.parent_id) + return super(ListL7Rule, self).take_action(parsed_args) + + +class ShowL7Rule(LbaasL7RuleMixin, neutronV20.ShowCommand): + """LBaaS v2 Show information of a given rule.""" + + resource = 'rule' + shadow_resource = 'lbaas_l7rule' + + +class CreateL7Rule(LbaasL7RuleMixin, neutronV20.CreateCommand): + """LBaaS v2 Create L7 rule.""" + + resource = 'rule' + shadow_resource = 'lbaas_l7rule' + + def add_known_arguments(self, parser): + super(CreateL7Rule, self).add_known_arguments(parser) + _add_common_args(parser) + parser.add_argument( + '--admin-state-down', + dest='admin_state_up', + action='store_false', + help=_('Set admin state up to false')) + + def args2body(self, parsed_args): + return _common_args2body(self.get_client(), parsed_args) + + +class UpdateL7Rule(LbaasL7RuleMixin, neutronV20.UpdateCommand): + """LBaaS v2 Update a given L7 rule.""" + + resource = 'rule' + shadow_resource = 'lbaas_l7rule' + + def add_known_arguments(self, parser): + super(UpdateL7Rule, self).add_known_arguments(parser) + _add_common_args(parser, False) + utils.add_boolean_argument( + parser, '--admin-state-up', + help=_('Specify the administrative state of the rule' + ' (True meaning "Up").')) + + def args2body(self, parsed_args): + return _common_args2body(self.get_client(), parsed_args, False) + + +class DeleteL7Rule(LbaasL7RuleMixin, neutronV20.DeleteCommand): + """LBaaS v2 Delete a given L7 rule.""" + + resource = 'rule' + shadow_resource = 'lbaas_l7rule' diff --git a/neutronclient/neutron/v2_0/lb/v2/listener.py b/neutronclient/neutron/v2_0/lb/v2/listener.py new file mode 100644 index 000000000..481943e6b --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/v2/listener.py @@ -0,0 +1,169 @@ +# Copyright 2014 Blue Box Group, Inc. +# 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. +# + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _get_loadbalancer_id(client, lb_id_or_name): + return neutronV20.find_resourceid_by_name_or_id( + client, 'loadbalancer', lb_id_or_name, + cmd_resource='lbaas_loadbalancer') + + +def _get_pool(client, pool_id_or_name): + return neutronV20.find_resource_by_name_or_id( + client, 'pool', pool_id_or_name, cmd_resource='lbaas_pool') + + +def _get_pool_id(client, pool_id_or_name): + return neutronV20.find_resourceid_by_name_or_id( + client, 'pool', pool_id_or_name, cmd_resource='lbaas_pool') + + +def _add_common_args(parser): + parser.add_argument( + '--description', + help=_('Description of the listener.')) + parser.add_argument( + '--connection-limit', + type=int, + help=_('The maximum number of connections per second allowed for ' + 'the listener. Positive integer or -1 ' + 'for unlimited (default).')) + parser.add_argument( + '--default-pool', + help=_('Default pool for the listener.')) + + +def _parse_common_args(body, parsed_args, client): + neutronV20.update_dict(parsed_args, body, + ['name', 'description', 'connection_limit']) + if parsed_args.default_pool: + default_pool_id = _get_pool_id( + client, parsed_args.default_pool) + body['default_pool_id'] = default_pool_id + + +class ListListener(neutronV20.ListCommand): + """LBaaS v2 List listeners that belong to a given tenant.""" + + resource = 'listener' + list_columns = ['id', 'default_pool_id', 'name', 'protocol', + 'protocol_port', 'admin_state_up', 'status'] + pagination_support = True + sorting_support = True + + +class ShowListener(neutronV20.ShowCommand): + """LBaaS v2 Show information of a given listener.""" + + resource = 'listener' + + +class CreateListener(neutronV20.CreateCommand): + """LBaaS v2 Create a listener.""" + + resource = 'listener' + + def add_known_arguments(self, parser): + _add_common_args(parser) + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--name', + help=_('The name of the listener. At least one of --default-pool ' + 'or --loadbalancer must be specified.')) + parser.add_argument( + '--default-tls-container-ref', + dest='default_tls_container_ref', + help=_('Default TLS container reference' + ' to retrieve TLS information.')) + parser.add_argument( + '--sni-container-refs', + dest='sni_container_refs', + nargs='+', + help=_('List of TLS container references for SNI.')) + parser.add_argument( + '--loadbalancer', + metavar='LOADBALANCER', + help=_('ID or name of the load balancer.')) + parser.add_argument( + '--protocol', + required=True, + choices=['TCP', 'HTTP', 'HTTPS', 'TERMINATED_HTTPS'], + type=utils.convert_to_uppercase, + help=_('Protocol for the listener.')) + parser.add_argument( + '--protocol-port', + dest='protocol_port', required=True, + metavar='PORT', + help=_('Protocol port for the listener.')) + + def args2body(self, parsed_args): + if not parsed_args.loadbalancer and not parsed_args.default_pool: + message = _('Either --default-pool or --loadbalancer must be ' + 'specified.') + raise exceptions.CommandError(message) + body = { + 'protocol': parsed_args.protocol, + 'protocol_port': parsed_args.protocol_port, + 'admin_state_up': parsed_args.admin_state + } + if parsed_args.loadbalancer: + loadbalancer_id = _get_loadbalancer_id( + self.get_client(), parsed_args.loadbalancer) + body['loadbalancer_id'] = loadbalancer_id + + neutronV20.update_dict(parsed_args, body, + ['default_tls_container_ref', + 'sni_container_refs', 'tenant_id']) + _parse_common_args(body, parsed_args, self.get_client()) + return {self.resource: body} + + +class UpdateListener(neutronV20.UpdateCommand): + """LBaaS v2 Update a given listener.""" + + resource = 'listener' + + def add_known_arguments(self, parser): + _add_common_args(parser) + parser.add_argument( + '--name', + help=_('Name of the listener.')) + utils.add_boolean_argument( + parser, '--admin-state-up', dest='admin_state_up', + help=_('Specify the administrative state of the listener. ' + '(True meaning "Up")')) + + def args2body(self, parsed_args): + body = {} + neutronV20.update_dict(parsed_args, body, + ['admin_state_up']) + _parse_common_args(body, parsed_args, self.get_client()) + return {self.resource: body} + + +class DeleteListener(neutronV20.DeleteCommand): + """LBaaS v2 Delete a given listener.""" + + resource = 'listener' diff --git a/neutronclient/neutron/v2_0/lb/v2/loadbalancer.py b/neutronclient/neutron/v2_0/lb/v2/loadbalancer.py new file mode 100644 index 000000000..c40809fad --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/v2/loadbalancer.py @@ -0,0 +1,178 @@ +# Copyright 2014 Blue Box Group, Inc. +# 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. +# +from oslo_serialization import jsonutils + +from neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _add_common_args(parser): + parser.add_argument( + '--description', + help=_('Description of the load balancer.')) + parser.add_argument( + '--name', metavar='NAME', + help=_('Name of the load balancer.')) + + +def _parse_common_args(body, parsed_args): + neutronV20.update_dict(parsed_args, body, + ['name', 'description']) + + +class ListLoadBalancer(neutronV20.ListCommand): + """LBaaS v2 List loadbalancers that belong to a given tenant.""" + + resource = 'loadbalancer' + list_columns = ['id', 'name', 'vip_address', + 'provisioning_status', 'provider'] + pagination_support = True + sorting_support = True + + +class ShowLoadBalancer(neutronV20.ShowCommand): + """LBaaS v2 Show information of a given loadbalancer.""" + + resource = 'loadbalancer' + + +class CreateLoadBalancer(neutronV20.CreateCommand): + """LBaaS v2 Create a loadbalancer.""" + + resource = 'loadbalancer' + + def add_known_arguments(self, parser): + _add_common_args(parser) + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--provider', + help=_('Provider name of the load balancer service.')) + parser.add_argument( + '--flavor', + help=_('ID or name of the flavor.')) + parser.add_argument( + '--vip-address', + help=_('VIP address for the load balancer.')) + parser.add_argument( + 'vip_subnet', metavar='VIP_SUBNET', + help=_('Load balancer VIP subnet.')) + + def args2body(self, parsed_args): + _subnet_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'subnet', parsed_args.vip_subnet) + body = {'vip_subnet_id': _subnet_id, + 'admin_state_up': parsed_args.admin_state} + if parsed_args.flavor: + _flavor_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'flavor', parsed_args.flavor) + body['flavor_id'] = _flavor_id + + neutronV20.update_dict(parsed_args, body, + ['provider', 'vip_address', 'tenant_id']) + _parse_common_args(body, parsed_args) + return {self.resource: body} + + +class UpdateLoadBalancer(neutronV20.UpdateCommand): + """LBaaS v2 Update a given loadbalancer.""" + + resource = 'loadbalancer' + + def add_known_arguments(self, parser): + utils.add_boolean_argument( + parser, '--admin-state-up', + help=_('Update the administrative state of ' + 'the load balancer (True meaning "Up").')) + _add_common_args(parser) + + def args2body(self, parsed_args): + body = {} + _parse_common_args(body, parsed_args) + neutronV20.update_dict(parsed_args, body, + ['admin_state_up']) + return {self.resource: body} + + +class DeleteLoadBalancer(neutronV20.DeleteCommand): + """LBaaS v2 Delete a given loadbalancer.""" + + resource = 'loadbalancer' + + +class RetrieveLoadBalancerStats(neutronV20.ShowCommand): + """Retrieve stats for a given loadbalancer.""" + + resource = 'loadbalancer' + + def take_action(self, parsed_args): + neutron_client = self.get_client() + loadbalancer_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'loadbalancer', parsed_args.id) + params = {} + if parsed_args.fields: + params = {'fields': parsed_args.fields} + + data = neutron_client.retrieve_loadbalancer_stats(loadbalancer_id, + **params) + self.format_output_data(data) + stats = data['stats'] + if 'stats' in data: + # To render the output table like: + # +--------------------+-------+ + # | Field | Value | + # +--------------------+-------+ + # | field1 | value1| + # | field2 | value2| + # | field3 | value3| + # | ... | ... | + # +--------------------+-------+ + # it has two columns and the Filed column is alphabetical, + # here convert the data dict to the 1-1 vector format below: + # [(field1, field2, field3, ...), (value1, value2, value3, ...)] + return list(zip(*sorted(stats.items()))) + + +class RetrieveLoadBalancerStatus(neutronV20.NeutronCommand): + """Retrieve status for a given loadbalancer. + + The only output is a formatted JSON tree, and the table format + does not support this type of data. + """ + resource = 'loadbalancer' + + def get_parser(self, prog_name): + parser = super(RetrieveLoadBalancerStatus, self).get_parser(prog_name) + parser.add_argument( + self.resource, metavar=self.resource.upper(), + help=_('ID or name of %s to show.') % self.resource) + + return parser + + def take_action(self, parsed_args): + self.log.debug('run(%s)', parsed_args) + neutron_client = self.get_client() + lb_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, self.resource, parsed_args.loadbalancer) + params = {} + data = neutron_client.retrieve_loadbalancer_status(lb_id, **params) + res = data['statuses'] + if 'statuses' in data: + print(jsonutils.dumps(res, indent=4)) diff --git a/neutronclient/neutron/v2_0/lb/v2/member.py b/neutronclient/neutron/v2_0/lb/v2/member.py new file mode 100644 index 000000000..ee3c090a9 --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/v2/member.py @@ -0,0 +1,150 @@ +# Copyright 2013 Mirantis Inc. +# Copyright 2014 Blue Box Group, Inc. +# 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. +# +from neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _get_pool_id(client, pool_id_or_name): + return neutronV20.find_resourceid_by_name_or_id(client, 'pool', + pool_id_or_name, + cmd_resource='lbaas_pool') + + +class LbaasMemberMixin(object): + + def set_extra_attrs(self, parsed_args): + self.parent_id = _get_pool_id(self.get_client(), parsed_args.pool) + + def add_known_arguments(self, parser): + parser.add_argument( + 'pool', metavar='POOL', + help=_('ID or name of the pool that this member belongs to.')) + + +def _add_common_args(parser): + parser.add_argument( + '--name', + help=_('Name of the member.')) + parser.add_argument( + '--weight', + help=_('Weight of the member in the pool (default:1, [0..256]).')) + + +def _parse_common_args(body, parsed_args): + neutronV20.update_dict(parsed_args, body, ['weight', 'name']) + + +class ListMember(LbaasMemberMixin, neutronV20.ListCommand): + """LBaaS v2 List members that belong to a given pool.""" + + resource = 'member' + shadow_resource = 'lbaas_member' + list_columns = [ + 'id', 'name', 'address', 'protocol_port', 'weight', + 'subnet_id', 'admin_state_up', 'status' + ] + pagination_support = True + sorting_support = True + + def take_action(self, parsed_args): + self.parent_id = _get_pool_id(self.get_client(), parsed_args.pool) + self.values_specs.append('--pool_id=%s' % self.parent_id) + return super(ListMember, self).take_action(parsed_args) + + +class ShowMember(LbaasMemberMixin, neutronV20.ShowCommand): + """LBaaS v2 Show information of a given member.""" + + resource = 'member' + shadow_resource = 'lbaas_member' + + +class CreateMember(neutronV20.CreateCommand): + """LBaaS v2 Create a member.""" + + resource = 'member' + shadow_resource = 'lbaas_member' + + def add_known_arguments(self, parser): + _add_common_args(parser) + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--subnet', + required=True, + help=_('Subnet ID or name for the member.')) + parser.add_argument( + '--address', + required=True, + help=_('IP address of the pool member in the pool.')) + parser.add_argument( + '--protocol-port', + required=True, + help=_('Port on which the pool member listens for requests or ' + 'connections.')) + parser.add_argument( + 'pool', metavar='POOL', + help=_('ID or name of the pool that this member belongs to.')) + + def args2body(self, parsed_args): + self.parent_id = _get_pool_id(self.get_client(), parsed_args.pool) + _subnet_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'subnet', parsed_args.subnet) + body = {'subnet_id': _subnet_id, + 'admin_state_up': parsed_args.admin_state, + 'protocol_port': parsed_args.protocol_port, + 'address': parsed_args.address} + neutronV20.update_dict(parsed_args, body, + ['subnet_id', 'tenant_id']) + _parse_common_args(body, parsed_args) + return {self.resource: body} + + +class UpdateMember(neutronV20.UpdateCommand): + """LBaaS v2 Update a given member.""" + + resource = 'member' + shadow_resource = 'lbaas_member' + + def add_known_arguments(self, parser): + parser.add_argument( + 'pool', metavar='POOL', + help=_('ID or name of the pool that this member belongs to.')) + utils.add_boolean_argument( + parser, '--admin-state-up', + help=_('Update the administrative state of ' + 'the member (True meaning "Up").')) + _add_common_args(parser) + + def args2body(self, parsed_args): + self.parent_id = _get_pool_id(self.get_client(), parsed_args.pool) + body = {} + if hasattr(parsed_args, "admin_state_up"): + body['admin_state_up'] = parsed_args.admin_state_up + _parse_common_args(body, parsed_args) + return {self.resource: body} + + +class DeleteMember(LbaasMemberMixin, neutronV20.DeleteCommand): + """LBaaS v2 Delete a given member.""" + + resource = 'member' + shadow_resource = 'lbaas_member' diff --git a/neutronclient/neutron/v2_0/lb/v2/pool.py b/neutronclient/neutron/v2_0/lb/v2/pool.py new file mode 100644 index 000000000..0af383970 --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/v2/pool.py @@ -0,0 +1,189 @@ +# Copyright 2013 Mirantis Inc. +# Copyright 2014 Blue Box Group, Inc. +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# Copyright 2015 Blue Box, an IBM Company +# 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 neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _get_loadbalancer_id(client, lb_id_or_name): + return neutronV20.find_resourceid_by_name_or_id( + client, 'loadbalancer', lb_id_or_name, + cmd_resource='lbaas_loadbalancer') + + +def _get_listener(client, listener_id_or_name): + return neutronV20.find_resource_by_name_or_id( + client, 'listener', listener_id_or_name) + + +def _get_listener_id(client, listener_id_or_name): + return neutronV20.find_resourceid_by_name_or_id( + client, 'listener', listener_id_or_name) + + +def _add_common_args(parser, is_create=True): + parser.add_argument( + '--description', + help=_('Description of the pool.')) + parser.add_argument( + '--name', help=_('The name of the pool.')) + parser.add_argument( + '--lb-algorithm', + required=is_create, + type=utils.convert_to_uppercase, + choices=['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'], + help=_('The algorithm used to distribute load between the members ' + 'of the pool.')) + + +def _parse_common_args(parsed_args): + body = {} + neutronV20.update_dict(parsed_args, + body, ['description', 'lb_algorithm', 'name']) + return body + + +class ListPool(neutronV20.ListCommand): + """LBaaS v2 List pools that belong to a given tenant.""" + + resource = 'pool' + shadow_resource = 'lbaas_pool' + list_columns = ['id', 'name', 'lb_algorithm', 'protocol', + 'admin_state_up'] + pagination_support = True + sorting_support = True + + +class ShowPool(neutronV20.ShowCommand): + """LBaaS v2 Show information of a given pool.""" + + resource = 'pool' + shadow_resource = 'lbaas_pool' + + def cleanup_output_data(self, data): + if 'members' not in data['pool']: + return [] + member_info = [] + for member in data['pool']['members']: + member_info.append(member['id']) + data['pool']['members'] = member_info + + +class CreatePool(neutronV20.CreateCommand): + """LBaaS v2 Create a pool.""" + + resource = 'pool' + shadow_resource = 'lbaas_pool' + + def add_known_arguments(self, parser): + _add_common_args(parser) + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--listener', + help=_('Listener whose default-pool should be set to this pool. ' + 'At least one of --listener or --loadbalancer must be ' + 'specified.')) + parser.add_argument( + '--loadbalancer', + help=_('Loadbalancer with which this pool should be associated. ' + 'At least one of --listener or --loadbalancer must be ' + 'specified.')) + parser.add_argument( + '--protocol', + type=utils.convert_to_uppercase, + required=True, + choices=['HTTP', 'HTTPS', 'TCP'], + help=_('Protocol for balancing.')) + parser.add_argument( + '--session-persistence', + metavar='type=TYPE[,cookie_name=COOKIE_NAME]', + type=utils.str2dict_type(required_keys=['type'], + optional_keys=['cookie_name']), + help=_('The type of session persistence to use and associated ' + 'cookie name.')) + + def args2body(self, parsed_args): + if not parsed_args.listener and not parsed_args.loadbalancer: + message = _('At least one of --listener or --loadbalancer must be ' + 'specified.') + raise exceptions.CommandError(message) + body = _parse_common_args(parsed_args) + if parsed_args.listener: + listener_id = _get_listener_id( + self.get_client(), + parsed_args.listener) + body['listener_id'] = listener_id + if parsed_args.loadbalancer: + loadbalancer_id = _get_loadbalancer_id( + self.get_client(), + parsed_args.loadbalancer) + body['loadbalancer_id'] = loadbalancer_id + body['admin_state_up'] = parsed_args.admin_state + neutronV20.update_dict(parsed_args, body, + ['tenant_id', 'protocol', + 'session_persistence']) + return {self.resource: body} + + +class UpdatePool(neutronV20.UpdateCommand): + """LBaaS v2 Update a given pool.""" + + resource = 'pool' + shadow_resource = 'lbaas_pool' + + def add_known_arguments(self, parser): + utils.add_boolean_argument( + parser, '--admin-state-up', + help=_('Update the administrative state of ' + 'the pool (True meaning "Up").')) + session_group = parser.add_mutually_exclusive_group() + session_group.add_argument( + '--session-persistence', + metavar='type=TYPE[,cookie_name=COOKIE_NAME]', + type=utils.str2dict_type(required_keys=['type'], + optional_keys=['cookie_name']), + help=_('The type of session persistence to use and associated ' + 'cookie name.')) + session_group.add_argument( + '--no-session-persistence', + action='store_true', + help=_('Clear session persistence for the pool.')) + _add_common_args(parser, False) + + def args2body(self, parsed_args): + body = _parse_common_args(parsed_args) + if parsed_args.no_session_persistence: + body['session_persistence'] = None + elif parsed_args.session_persistence: + body['session_persistence'] = parsed_args.session_persistence + neutronV20.update_dict(parsed_args, body, + ['admin_state_up']) + return {self.resource: body} + + +class DeletePool(neutronV20.DeleteCommand): + """LBaaS v2 Delete a given pool.""" + + resource = 'pool' + shadow_resource = 'lbaas_pool' diff --git a/neutronclient/neutron/v2_0/lb/vip.py b/neutronclient/neutron/v2_0/lb/vip.py new file mode 100644 index 000000000..e3ac8ee9d --- /dev/null +++ b/neutronclient/neutron/v2_0/lb/vip.py @@ -0,0 +1,104 @@ +# Copyright 2013 Mirantis 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 neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListVip(neutronV20.ListCommand): + """List vips that belong to a given tenant.""" + + resource = 'vip' + list_columns = ['id', 'name', 'algorithm', 'address', 'protocol', + 'admin_state_up', 'status'] + pagination_support = True + sorting_support = True + + +class ShowVip(neutronV20.ShowCommand): + """Show information of a given vip.""" + + resource = 'vip' + + +class CreateVip(neutronV20.CreateCommand): + """Create a vip.""" + + resource = 'vip' + + def add_known_arguments(self, parser): + parser.add_argument( + 'pool_id', metavar='POOL', + help=_('ID or name of the pool to which this vip belongs.')) + parser.add_argument( + '--address', + help=_('IP address of the vip.')) + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--connection-limit', + help=_('The maximum number of connections per second allowed for ' + 'the vip. Valid values: a positive integer or -1 ' + 'for unlimited (default).')) + parser.add_argument( + '--description', + help=_('Description of the vip to be created.')) + parser.add_argument( + '--name', + required=True, + help=_('Name of the vip to be created.')) + parser.add_argument( + '--protocol-port', + required=True, + help=_('TCP port on which to listen for client traffic that is ' + 'associated with the vip address.')) + parser.add_argument( + '--protocol', + required=True, choices=['TCP', 'HTTP', 'HTTPS'], + help=_('Protocol for balancing.')) + parser.add_argument( + '--subnet-id', metavar='SUBNET', + required=True, + help=_('The subnet on which to allocate the vip address.')) + + def args2body(self, parsed_args): + _pool_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'pool', parsed_args.pool_id) + _subnet_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'subnet', parsed_args.subnet_id) + + body = {'pool_id': _pool_id, + 'admin_state_up': parsed_args.admin_state, + 'subnet_id': _subnet_id} + neutronV20.update_dict(parsed_args, body, + ['address', 'connection_limit', 'description', + 'name', 'protocol_port', 'protocol', + 'tenant_id']) + return {self.resource: body} + + +class UpdateVip(neutronV20.UpdateCommand): + """Update a given vip.""" + + resource = 'vip' + + +class DeleteVip(neutronV20.DeleteCommand): + """Delete a given vip.""" + + resource = 'vip' diff --git a/neutronclient/neutron/v2_0/metering.py b/neutronclient/neutron/v2_0/metering.py new file mode 100644 index 000000000..8f6d2174e --- /dev/null +++ b/neutronclient/neutron/v2_0/metering.py @@ -0,0 +1,121 @@ +# Copyright (C) 2013 eNovance SAS +# +# 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 neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronv20 + + +class ListMeteringLabel(neutronv20.ListCommand): + """List metering labels that belong to a given tenant.""" + + resource = 'metering_label' + list_columns = ['id', 'name', 'description', 'shared'] + pagination_support = True + sorting_support = True + + +class ShowMeteringLabel(neutronv20.ShowCommand): + """Show information of a given metering label.""" + + resource = 'metering_label' + allow_names = True + + +class CreateMeteringLabel(neutronv20.CreateCommand): + """Create a metering label for a given tenant.""" + + resource = 'metering_label' + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of the metering label to be created.')) + parser.add_argument( + '--description', + help=_('Description of the metering label to be created.')) + parser.add_argument( + '--shared', + action='store_true', + help=_('Set the label as shared.')) + + def args2body(self, parsed_args): + body = {'name': parsed_args.name} + neutronv20.update_dict(parsed_args, body, + ['tenant_id', 'description', 'shared']) + return {'metering_label': body} + + +class DeleteMeteringLabel(neutronv20.DeleteCommand): + """Delete a given metering label.""" + + resource = 'metering_label' + allow_names = True + + +class ListMeteringLabelRule(neutronv20.ListCommand): + """List metering labels that belong to a given label.""" + + resource = 'metering_label_rule' + list_columns = ['id', 'excluded', 'direction', 'remote_ip_prefix'] + pagination_support = True + sorting_support = True + + +class ShowMeteringLabelRule(neutronv20.ShowCommand): + """Show information of a given metering label rule.""" + + resource = 'metering_label_rule' + allow_names = False + + +class CreateMeteringLabelRule(neutronv20.CreateCommand): + """Create a metering label rule for a given label.""" + + resource = 'metering_label_rule' + + def add_known_arguments(self, parser): + parser.add_argument( + 'label_id', metavar='LABEL', + help=_('ID or name of the label.')) + parser.add_argument( + 'remote_ip_prefix', metavar='REMOTE_IP_PREFIX', + help=_('CIDR to match on.')) + parser.add_argument( + '--direction', + default='ingress', choices=['ingress', 'egress'], + type=utils.convert_to_lowercase, + help=_('Direction of traffic, default: ingress.')) + parser.add_argument( + '--excluded', + action='store_true', + help=_('Exclude this CIDR from the label, default: not excluded.')) + + def args2body(self, parsed_args): + neutron_client = self.get_client() + label_id = neutronv20.find_resourceid_by_name_or_id( + neutron_client, 'metering_label', parsed_args.label_id) + + body = {'metering_label_id': label_id, + 'remote_ip_prefix': parsed_args.remote_ip_prefix} + neutronv20.update_dict(parsed_args, body, + ['direction', 'excluded']) + return {'metering_label_rule': body} + + +class DeleteMeteringLabelRule(neutronv20.DeleteCommand): + """Delete a given metering label.""" + + resource = 'metering_label_rule' + allow_names = False diff --git a/neutronclient/neutron/v2_0/network.py b/neutronclient/neutron/v2_0/network.py new file mode 100644 index 000000000..6c68b623a --- /dev/null +++ b/neutronclient/neutron/v2_0/network.py @@ -0,0 +1,247 @@ +# 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 argparse + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.neutron.v2_0 import availability_zone +from neutronclient.neutron.v2_0 import dns +from neutronclient.neutron.v2_0.qos import policy as qos_policy + + +def _format_subnets(network): + try: + return '\n'.join([' '.join([s['id'], s.get('cidr', '')]) + for s in network['subnets']]) + except (TypeError, KeyError): + return '' + + +def args2body_common(body, parsed_args): + neutronV20.update_dict(parsed_args, body, + ['name', 'description']) + + +class ListNetwork(neutronV20.ListCommand): + """List networks that belong to a given tenant.""" + + # Length of a query filter on subnet id + # id=& (with len(uuid)=36) + subnet_id_filter_len = 40 + # Length of a marker in pagination + # &marker= (with len(uuid)=36) + marker_len = 44 + resource = 'network' + _formatters = {'subnets': _format_subnets, } + list_columns = ['id', 'name', 'subnets'] + pagination_support = True + sorting_support = True + + filter_attrs = [ + 'tenant_id', + 'name', + 'admin_state_up', + {'name': 'status', + 'help': _("Filter %s according to their operation status." + "(For example: ACTIVE, ERROR etc)"), + 'boolean': False, + 'argparse_kwargs': {'type': utils.convert_to_uppercase}}, + {'name': 'shared', + 'help': _('Filter and list the networks which are shared.'), + 'boolean': True}, + {'name': 'router:external', + 'help': _('Filter and list the networks which are external.'), + 'boolean': True}, + {'name': 'tags', + 'help': _("Filter and list %s which has all given tags. " + "Multiple tags can be set like --tags "), + 'boolean': False, + 'argparse_kwargs': {'metavar': 'TAG'}}, + {'name': 'tags_any', + 'help': _("Filter and list %s which has any given tags. " + "Multiple tags can be set like --tags-any "), + 'boolean': False, + 'argparse_kwargs': {'metavar': 'TAG'}}, + {'name': 'not_tags', + 'help': _("Filter and list %s which does not have all given tags. " + "Multiple tags can be set like --not-tags "), + 'boolean': False, + 'argparse_kwargs': {'metavar': 'TAG'}}, + {'name': 'not_tags_any', + 'help': _("Filter and list %s which does not have any given tags. " + "Multiple tags can be set like --not-tags-any " + ""), + 'boolean': False, + 'argparse_kwargs': {'metavar': 'TAG'}}, + ] + + def extend_list(self, data, parsed_args): + """Add subnet information to a network list.""" + neutron_client = self.get_client() + search_opts = {'fields': ['id', 'cidr']} + if self.pagination_support: + page_size = parsed_args.page_size + if page_size: + search_opts.update({'limit': page_size}) + subnet_ids = [] + for n in data: + if 'subnets' in n: + subnet_ids.extend(n['subnets']) + + def _get_subnet_list(sub_ids): + search_opts['id'] = sub_ids + return neutron_client.list_subnets( + **search_opts).get('subnets', []) + + try: + subnets = _get_subnet_list(subnet_ids) + except exceptions.RequestURITooLong as uri_len_exc: + # The URI is too long because of too many subnet_id filters + # Use the excess attribute of the exception to know how many + # subnet_id filters can be inserted into a single request + subnet_count = len(subnet_ids) + max_size = ((self.subnet_id_filter_len * subnet_count) - + uri_len_exc.excess) + if self.pagination_support: + max_size -= self.marker_len + chunk_size = max_size // self.subnet_id_filter_len + subnets = [] + for i in range(0, subnet_count, chunk_size): + subnets.extend( + _get_subnet_list(subnet_ids[i: i + chunk_size])) + + subnet_dict = dict([(s['id'], s) for s in subnets]) + for n in data: + if 'subnets' in n: + n['subnets'] = [(subnet_dict.get(s) or {"id": s}) + for s in n['subnets']] + + +class ListExternalNetwork(ListNetwork): + """List external networks that belong to a given tenant.""" + + pagination_support = True + sorting_support = True + + def retrieve_list(self, parsed_args): + external = '--router:external=True' + if external not in self.values_specs: + self.values_specs.append('--router:external=True') + return super(ListExternalNetwork, self).retrieve_list(parsed_args) + + +class ShowNetwork(neutronV20.ShowCommand): + """Show information of a given network.""" + + resource = 'network' + + +class CreateNetwork(neutronV20.CreateCommand, qos_policy.CreateQosPolicyMixin): + """Create a network for a given tenant.""" + + resource = 'network' + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--admin_state_down', + dest='admin_state', action='store_false', + help=argparse.SUPPRESS) + parser.add_argument( + '--shared', + action='store_true', + help=_('Set the network as shared.'), + default=argparse.SUPPRESS) + parser.add_argument( + '--provider:network_type', + metavar='', + help=_('The physical mechanism by which the virtual network' + ' is implemented.')) + parser.add_argument( + '--provider:physical_network', + metavar='', + help=_('Name of the physical network over which the virtual ' + 'network is implemented.')) + parser.add_argument( + '--provider:segmentation_id', + metavar='', + help=_('VLAN ID for VLAN networks or tunnel-id for GRE/VXLAN ' + 'networks.')) + utils.add_boolean_argument( + parser, + '--vlan-transparent', + default=argparse.SUPPRESS, + help=_('Create a VLAN transparent network.')) + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of the network to be created.')) + parser.add_argument( + '--description', + help=_('Description of network.')) + + self.add_arguments_qos_policy(parser) + availability_zone.add_az_hint_argument(parser, self.resource) + dns.add_dns_argument_create(parser, self.resource, 'domain') + + def args2body(self, parsed_args): + body = {'admin_state_up': parsed_args.admin_state} + args2body_common(body, parsed_args) + neutronV20.update_dict(parsed_args, body, + ['shared', 'tenant_id', + 'vlan_transparent', + 'provider:network_type', + 'provider:physical_network', + 'provider:segmentation_id', + 'description']) + self.args2body_qos_policy(parsed_args, body) + availability_zone.args2body_az_hint(parsed_args, body) + dns.args2body_dns_create(parsed_args, body, 'domain') + return {'network': body} + + +class DeleteNetwork(neutronV20.DeleteCommand): + """Delete a given network.""" + + resource = 'network' + + +class UpdateNetwork(neutronV20.UpdateCommand, qos_policy.UpdateQosPolicyMixin): + """Update network's information.""" + + resource = 'network' + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help=_('Name of the network.')) + parser.add_argument( + '--description', + help=_('Description of this network.')) + self.add_arguments_qos_policy(parser) + dns.add_dns_argument_update(parser, self.resource, 'domain') + + def args2body(self, parsed_args): + body = {} + args2body_common(body, parsed_args) + self.args2body_qos_policy(parsed_args, body) + dns.args2body_dns_update(parsed_args, body, 'domain') + return {'network': body} diff --git a/neutronclient/neutron/v2_0/network_ip_availability.py b/neutronclient/neutron/v2_0/network_ip_availability.py new file mode 100644 index 000000000..ba7af8f80 --- /dev/null +++ b/neutronclient/neutron/v2_0/network_ip_availability.py @@ -0,0 +1,72 @@ +# 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 show + +from neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListIpAvailability(neutronV20.ListCommand): + """List IP usage of networks""" + + resource = 'network_ip_availability' + resource_plural = 'network_ip_availabilities' + list_columns = ['network_id', 'network_name', 'total_ips', 'used_ips'] + paginations_support = True + sorting_support = True + + filter_attrs = [ + {'name': 'ip_version', + 'help': _('Returns IP availability for the network subnets ' + 'with a given IP version. Default: 4'), + 'argparse_kwargs': {'type': int, + 'choices': [4, 6], + 'default': 4} + }, + {'name': 'network_id', + 'help': _('Returns IP availability for the network ' + 'matching a given network ID.')}, + {'name': 'network_name', + 'help': _('Returns IP availability for the network ' + 'matching a given name.')}, + {'name': 'tenant_id', + 'help': _('Returns IP availability for the networks ' + 'with a given tenant ID.')}, + ] + + +class ShowIpAvailability(neutronV20.NeutronCommand, show.ShowOne): + """Show IP usage of specific network""" + + resource = 'network_ip_availability' + + def get_parser(self, prog_name): + parser = super(ShowIpAvailability, self).get_parser(prog_name) + parser.add_argument( + 'network_id', metavar='NETWORK', + help=_('ID or name of network to look up.')) + return parser + + def take_action(self, parsed_args): + self.log.debug('run(%s)', parsed_args) + neutron_client = self.get_client() + _id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'network', parsed_args.network_id) + data = neutron_client.show_network_ip_availability(_id) + self.format_output_data(data) + resource = data[self.resource] + if self.resource in data: + return zip(*sorted(resource.items())) + else: + return None diff --git a/neutronclient/neutron/v2_0/port.py b/neutronclient/neutron/v2_0/port.py new file mode 100644 index 000000000..3fe73544b --- /dev/null +++ b/neutronclient/neutron/v2_0/port.py @@ -0,0 +1,341 @@ +# 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 argparse + +from oslo_serialization import jsonutils + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.neutron.v2_0 import dns +from neutronclient.neutron.v2_0.qos import policy as qos_policy + + +def _format_fixed_ips(port): + try: + return '\n'.join([jsonutils.dumps(ip) for ip in port['fixed_ips']]) + except (TypeError, KeyError): + return '' + + +def _add_updatable_args(parser): + parser.add_argument( + '--name', + help=_('Name of this port.')) + parser.add_argument( + '--description', + help=_('Description of this port.')) + parser.add_argument( + '--fixed-ip', metavar='subnet_id=SUBNET,ip_address=IP_ADDR', + action='append', + type=utils.str2dict_type(optional_keys=['subnet_id', 'ip_address']), + help=_('Desired IP and/or subnet for this port: ' + 'subnet_id=,ip_address=. ' + 'You can repeat this option.')) + parser.add_argument( + '--fixed_ip', + action='append', + help=argparse.SUPPRESS) + parser.add_argument( + '--device-id', + help=_('Device ID of this port.')) + parser.add_argument( + '--device_id', + help=argparse.SUPPRESS) + parser.add_argument( + '--device-owner', + help=_('Device owner of this port.')) + parser.add_argument( + '--device_owner', + help=argparse.SUPPRESS) + + +def _updatable_args2body(parsed_args, body, client): + neutronV20.update_dict(parsed_args, body, + ['device_id', 'device_owner', 'name', + 'description']) + ips = [] + if parsed_args.fixed_ip: + for ip_spec in parsed_args.fixed_ip: + if 'subnet_id' in ip_spec: + subnet_name_id = ip_spec['subnet_id'] + _subnet_id = neutronV20.find_resourceid_by_name_or_id( + client, 'subnet', subnet_name_id) + ip_spec['subnet_id'] = _subnet_id + ips.append(ip_spec) + if ips: + body['fixed_ips'] = ips + + +class ListPort(neutronV20.ListCommand): + """List ports that belong to a given tenant.""" + + resource = 'port' + _formatters = {'fixed_ips': _format_fixed_ips, } + list_columns = ['id', 'name', 'mac_address', 'fixed_ips'] + pagination_support = True + sorting_support = True + + +class ListRouterPort(neutronV20.ListCommand): + """List ports that belong to a given tenant, with specified router.""" + + resource = 'port' + _formatters = {'fixed_ips': _format_fixed_ips, } + list_columns = ['id', 'name', 'mac_address', 'fixed_ips'] + pagination_support = True + sorting_support = True + + def get_parser(self, prog_name): + parser = super(ListRouterPort, self).get_parser(prog_name) + parser.add_argument( + 'id', metavar='ROUTER', + help=_('ID or name of the router to look up.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'router', parsed_args.id) + self.values_specs.append('--device_id=%s' % _id) + return super(ListRouterPort, self).take_action(parsed_args) + + +class ShowPort(neutronV20.ShowCommand): + """Show information of a given port.""" + + resource = 'port' + + +class UpdatePortSecGroupMixin(object): + def add_arguments_secgroup(self, parser): + group_sg = parser.add_mutually_exclusive_group() + group_sg.add_argument( + '--security-group', metavar='SECURITY_GROUP', + default=[], action='append', dest='security_groups', + help=_('Security group associated with the port. You can ' + 'repeat this option.')) + group_sg.add_argument( + '--no-security-groups', + action='store_true', + help=_('Associate no security groups with the port.')) + + def _resolv_sgid(self, secgroup): + return neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'security_group', secgroup) + + def args2body_secgroup(self, parsed_args, port): + if parsed_args.security_groups: + port['security_groups'] = [self._resolv_sgid(sg) for sg + in parsed_args.security_groups] + elif parsed_args.no_security_groups: + port['security_groups'] = [] + + +class UpdateExtraDhcpOptMixin(object): + def add_arguments_extradhcpopt(self, parser): + group_sg = parser.add_mutually_exclusive_group() + group_sg.add_argument( + '--extra-dhcp-opt', + default=[], + action='append', + dest='extra_dhcp_opts', + type=utils.str2dict_type( + required_keys=['opt_name'], + optional_keys=['opt_value', 'ip_version']), + help=_('Extra dhcp options to be assigned to this port: ' + 'opt_name=,opt_value=,' + 'ip_version={4,6}. You can repeat this option.')) + + def args2body_extradhcpopt(self, parsed_args, port): + ops = [] + if parsed_args.extra_dhcp_opts: + # the extra_dhcp_opt params (opt_name & opt_value) + # must come in pairs, if there is a parm error + # both must be thrown out. + opt_ele = {} + edo_err_msg = _("Invalid --extra-dhcp-opt option, can only be: " + "opt_name=,opt_value=," + "ip_version={4,6}. " + "You can repeat this option.") + for opt in parsed_args.extra_dhcp_opts: + opt_ele.update(opt) + if ('opt_name' in opt_ele and + ('opt_value' in opt_ele or 'ip_version' in opt_ele)): + if opt_ele.get('opt_value') == 'null': + opt_ele['opt_value'] = None + ops.append(opt_ele) + opt_ele = {} + else: + raise exceptions.CommandError(edo_err_msg) + + if ops: + port['extra_dhcp_opts'] = ops + + +class UpdatePortAllowedAddressPair(object): + """Update Port for allowed_address_pairs""" + + def add_arguments_allowedaddresspairs(self, parser): + group_aap = parser.add_mutually_exclusive_group() + group_aap.add_argument( + '--allowed-address-pair', + metavar='ip_address=IP_ADDR|CIDR[,mac_address=MAC_ADDR]', + default=[], + action='append', + dest='allowed_address_pairs', + type=utils.str2dict_type( + required_keys=['ip_address'], + optional_keys=['mac_address']), + help=_('Allowed address pair associated with the port. ' + '"ip_address" parameter is required. IP address or ' + 'CIDR can be specified for "ip_address". ' + '"mac_address" parameter is optional. ' + 'You can repeat this option.')) + group_aap.add_argument( + '--no-allowed-address-pairs', + action='store_true', + help=_('Associate no allowed address pairs with the port.')) + + def args2body_allowedaddresspairs(self, parsed_args, port): + if parsed_args.allowed_address_pairs: + port['allowed_address_pairs'] = parsed_args.allowed_address_pairs + elif parsed_args.no_allowed_address_pairs: + port['allowed_address_pairs'] = [] + + +class CreatePort(neutronV20.CreateCommand, UpdatePortSecGroupMixin, + UpdateExtraDhcpOptMixin, qos_policy.CreateQosPolicyMixin, + UpdatePortAllowedAddressPair): + """Create a port for a given tenant.""" + + resource = 'port' + + def add_known_arguments(self, parser): + _add_updatable_args(parser) + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--admin_state_down', + dest='admin_state', action='store_false', + help=argparse.SUPPRESS) + parser.add_argument( + '--mac-address', + help=_('MAC address of this port.')) + parser.add_argument( + '--mac_address', + help=argparse.SUPPRESS) + parser.add_argument( + '--vnic-type', + metavar='', + choices=['direct', 'direct-physical', 'macvtap', + 'normal', 'baremetal', 'smart-nic'], + type=utils.convert_to_lowercase, + help=_('VNIC type for this port.')) + parser.add_argument( + '--vnic_type', + choices=['direct', 'direct-physical', 'macvtap', + 'normal', 'baremetal', 'smart-nic'], + type=utils.convert_to_lowercase, + help=argparse.SUPPRESS) + parser.add_argument( + '--binding-profile', + help=_('Custom data to be passed as binding:profile.')) + parser.add_argument( + '--binding_profile', + help=argparse.SUPPRESS) + self.add_arguments_secgroup(parser) + self.add_arguments_extradhcpopt(parser) + self.add_arguments_qos_policy(parser) + self.add_arguments_allowedaddresspairs(parser) + + parser.add_argument( + 'network_id', metavar='NETWORK', + help=_('ID or name of the network this port belongs to.')) + dns.add_dns_argument_create(parser, self.resource, 'name') + + def args2body(self, parsed_args): + client = self.get_client() + _network_id = neutronV20.find_resourceid_by_name_or_id( + client, 'network', parsed_args.network_id) + body = {'admin_state_up': parsed_args.admin_state, + 'network_id': _network_id, } + _updatable_args2body(parsed_args, body, client) + neutronV20.update_dict(parsed_args, body, + ['mac_address', 'tenant_id']) + if parsed_args.vnic_type: + body['binding:vnic_type'] = parsed_args.vnic_type + if parsed_args.binding_profile: + body['binding:profile'] = jsonutils.loads( + parsed_args.binding_profile) + + self.args2body_secgroup(parsed_args, body) + self.args2body_extradhcpopt(parsed_args, body) + self.args2body_qos_policy(parsed_args, body) + self.args2body_allowedaddresspairs(parsed_args, body) + dns.args2body_dns_create(parsed_args, body, 'name') + + return {'port': body} + + +class DeletePort(neutronV20.DeleteCommand): + """Delete a given port.""" + + resource = 'port' + + +class UpdatePort(neutronV20.UpdateCommand, UpdatePortSecGroupMixin, + UpdateExtraDhcpOptMixin, qos_policy.UpdateQosPolicyMixin, + UpdatePortAllowedAddressPair): + """Update port's information.""" + + resource = 'port' + + def add_known_arguments(self, parser): + _add_updatable_args(parser) + parser.add_argument( + '--admin-state-up', + choices=['True', 'False'], + help=_('Set admin state up for the port.')) + parser.add_argument( + '--admin_state_up', + choices=['True', 'False'], + help=argparse.SUPPRESS) + self.add_arguments_secgroup(parser) + self.add_arguments_extradhcpopt(parser) + self.add_arguments_qos_policy(parser) + self.add_arguments_allowedaddresspairs(parser) + dns.add_dns_argument_update(parser, self.resource, 'name') + + def args2body(self, parsed_args): + body = {} + client = self.get_client() + _updatable_args2body(parsed_args, body, client) + if parsed_args.admin_state_up: + body['admin_state_up'] = parsed_args.admin_state_up + + self.args2body_secgroup(parsed_args, body) + self.args2body_extradhcpopt(parsed_args, body) + self.args2body_qos_policy(parsed_args, body) + self.args2body_allowedaddresspairs(parsed_args, body) + dns.args2body_dns_update(parsed_args, body, 'name') + + return {'port': body} diff --git a/neutronclient/neutron/v2_0/purge.py b/neutronclient/neutron/v2_0/purge.py new file mode 100644 index 000000000..332a20b32 --- /dev/null +++ b/neutronclient/neutron/v2_0/purge.py @@ -0,0 +1,150 @@ +# Copyright 2016 Cisco Systems +# 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 neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronV20 + + +class Purge(neutronV20.NeutronCommand): + """Delete all resources that belong to a given tenant.""" + + def _pluralize(self, string): + return string + 's' + + def _get_resources(self, neutron_client, resource_types, tenant_id): + resources = [] + for resource_type in resource_types: + resources.append([]) + resource_type_plural = self._pluralize(resource_type) + opts = {'fields': ['id', 'tenant_id']} + if resource_type_plural == 'ports': + opts['fields'].append('device_id') + opts['fields'].append('device_owner') + function = getattr(neutron_client, 'list_%s' % + resource_type_plural) + if callable(function): + returned_resources = function(**opts).get(resource_type_plural, + []) + for resource in returned_resources: + if resource['tenant_id'] == tenant_id: + index = resource_types.index(resource_type) + resources[index].append(resource) + self.total_resources += 1 + return resources + + def _delete_resource(self, neutron_client, resource_type, resource): + resource_id = resource['id'] + if resource_type == 'port': + router_interface_owners = ['network:router_interface', + 'network:router_interface_distributed'] + if resource.get('device_owner', '') in router_interface_owners: + body = {'port_id': resource_id} + neutron_client.remove_interface_router(resource['device_id'], + body) + return + function = getattr(neutron_client, 'delete_%s' % resource_type) + if callable(function): + function(resource_id) + + def _purge_resources(self, neutron_client, resource_types, + tenant_resources): + deleted = {} + failed = {} + failures = False + for resources in tenant_resources: + index = tenant_resources.index(resources) + resource_type = resource_types[index] + failed[resource_type] = 0 + deleted[resource_type] = 0 + for resource in resources: + try: + self._delete_resource(neutron_client, resource_type, + resource) + deleted[resource_type] += 1 + self.deleted_resources += 1 + except Exception: + failures = True + failed[resource_type] += 1 + self.total_resources -= 1 + percent_complete = 100 + if self.total_resources > 0: + percent_complete = (self.deleted_resources / + float(self.total_resources)) * 100 + sys.stdout.write("\rPurging resources: %d%% complete." % + percent_complete) + sys.stdout.flush() + return (deleted, failed, failures) + + def _build_message(self, deleted, failed, failures): + msg = '' + deleted_msg = [] + for resource, value in deleted.items(): + if value: + if not msg: + msg = 'Deleted' + if not value == 1: + resource = self._pluralize(resource) + deleted_msg.append(" %d %s" % (value, resource)) + if deleted_msg: + msg += ','.join(deleted_msg) + + failed_msg = [] + if failures: + if msg: + msg += '. ' + msg += 'The following resources could not be deleted:' + for resource, value in failed.items(): + if value: + if not value == 1: + resource = self._pluralize(resource) + failed_msg.append(" %d %s" % (value, resource)) + msg += ','.join(failed_msg) + + if msg: + msg += '.' + else: + msg = _('Tenant has no supported resources.') + + return msg + + def get_parser(self, prog_name): + parser = super(Purge, self).get_parser(prog_name) + parser.add_argument( + 'tenant', metavar='TENANT', + help=_('ID of Tenant owning the resources to be deleted.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + + self.any_failures = False + + # A list of the types of resources supported in the order in which + # they should be deleted. + resource_types = ['floatingip', 'port', 'router', + 'network', 'security_group'] + + deleted = {} + failed = {} + self.total_resources = 0 + self.deleted_resources = 0 + resources = self._get_resources(neutron_client, resource_types, + parsed_args.tenant) + deleted, failed, failures = self._purge_resources(neutron_client, + resource_types, + resources) + print('\n%s' % self._build_message(deleted, failed, failures)) diff --git a/neutronclient/neutron/v2_0/qos/__init__.py b/neutronclient/neutron/v2_0/qos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/neutron/v2_0/qos/bandwidth_limit_rule.py b/neutronclient/neutron/v2_0/qos/bandwidth_limit_rule.py new file mode 100644 index 000000000..27e7b626a --- /dev/null +++ b/neutronclient/neutron/v2_0/qos/bandwidth_limit_rule.py @@ -0,0 +1,101 @@ +# Copyright 2015 Huawei Technologies India Pvt Ltd, 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 neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.neutron import v2_0 as neutronv20 +from neutronclient.neutron.v2_0.qos import rule as qos_rule + + +BANDWIDTH_LIMIT_RULE_RESOURCE = 'bandwidth_limit_rule' + + +def add_bandwidth_limit_arguments(parser): + parser.add_argument( + '--max-kbps', + help=_('Maximum bandwidth in kbps.')) + parser.add_argument( + '--max-burst-kbps', + help=_('Maximum burst bandwidth in kbps.')) + + +def update_bandwidth_limit_args2body(parsed_args, body): + max_kbps = parsed_args.max_kbps + max_burst_kbps = parsed_args.max_burst_kbps + if not (max_kbps or max_burst_kbps): + raise exceptions.CommandError(_("Must provide max-kbps" + " or max-burst-kbps option.")) + neutronv20.update_dict(parsed_args, body, + ['max_kbps', 'max_burst_kbps', 'tenant_id']) + + +class CreateQoSBandwidthLimitRule(qos_rule.QosRuleMixin, + neutronv20.CreateCommand): + """Create a qos bandwidth limit rule.""" + + resource = BANDWIDTH_LIMIT_RULE_RESOURCE + + def add_known_arguments(self, parser): + super(CreateQoSBandwidthLimitRule, self).add_known_arguments(parser) + add_bandwidth_limit_arguments(parser) + + def args2body(self, parsed_args): + body = {} + update_bandwidth_limit_args2body(parsed_args, body) + return {self.resource: body} + + +class ListQoSBandwidthLimitRules(qos_rule.QosRuleMixin, + neutronv20.ListCommand): + """List all qos bandwidth limit rules belonging to the specified policy.""" + + resource = BANDWIDTH_LIMIT_RULE_RESOURCE + _formatters = {} + pagination_support = True + sorting_support = True + + +class ShowQoSBandwidthLimitRule(qos_rule.QosRuleMixin, neutronv20.ShowCommand): + """Show information about the given qos bandwidth limit rule.""" + + resource = BANDWIDTH_LIMIT_RULE_RESOURCE + allow_names = False + + +class UpdateQoSBandwidthLimitRule(qos_rule.QosRuleMixin, + neutronv20.UpdateCommand): + """Update the given qos bandwidth limit rule.""" + + resource = BANDWIDTH_LIMIT_RULE_RESOURCE + allow_names = False + + def add_known_arguments(self, parser): + super(UpdateQoSBandwidthLimitRule, self).add_known_arguments(parser) + add_bandwidth_limit_arguments(parser) + + def args2body(self, parsed_args): + body = {} + update_bandwidth_limit_args2body(parsed_args, body) + return {self.resource: body} + + +class DeleteQoSBandwidthLimitRule(qos_rule.QosRuleMixin, + neutronv20.DeleteCommand): + """Delete a given qos bandwidth limit rule.""" + + resource = BANDWIDTH_LIMIT_RULE_RESOURCE + allow_names = False diff --git a/neutronclient/neutron/v2_0/qos/dscp_marking_rule.py b/neutronclient/neutron/v2_0/qos/dscp_marking_rule.py new file mode 100644 index 000000000..963c481a7 --- /dev/null +++ b/neutronclient/neutron/v2_0/qos/dscp_marking_rule.py @@ -0,0 +1,112 @@ +# Copyright 2016 Comcast, 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 neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.neutron import v2_0 as neutronv20 +from neutronclient.neutron.v2_0.qos import rule as qos_rule + + +DSCP_MARKING_RESOURCE = 'dscp_marking_rule' +# DSCP DETAILS +# 0 - none | 8 - cs1 | 10 - af11 | 12 - af12 | 14 - af13 | +# 16 - cs2 | 18 - af21 | 20 - af22 | 22 - af23 | 24 - cs3 | +# 26 - af31 | 28 - af32 | 30 - af33 | 32 - cs4 | 34 - af41 | +# 36 - af42 | 38 - af43 | 40 - cs5 | 46 - ef | 48 - cs6 | +# 56 - cs7 + +DSCP_VALID_MARKS = [0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, + 34, 36, 38, 40, 46, 48, 56] + + +def add_dscp_marking_arguments(parser): + parser.add_argument( + '--dscp-mark', + required=True, + type=str, + help=_('DSCP mark: value can be 0, even numbers from 8-56, \ + excluding 42, 44, 50, 52, and 54.')) + + +def update_dscp_args2body(parsed_args, body): + dscp_mark = parsed_args.dscp_mark + if int(dscp_mark) not in DSCP_VALID_MARKS: + raise exceptions.CommandError(_("DSCP mark: %s not supported. " + "Please note value can either be 0 " + "or any even number from 8-56 " + "excluding 42, 44, 50, 52 and " + "54.") % dscp_mark) + neutronv20.update_dict(parsed_args, body, + ['dscp_mark']) + + +class CreateQoSDscpMarkingRule(qos_rule.QosRuleMixin, + neutronv20.CreateCommand): + """Create a QoS DSCP marking rule.""" + + resource = DSCP_MARKING_RESOURCE + + def add_known_arguments(self, parser): + super(CreateQoSDscpMarkingRule, self).add_known_arguments(parser) + add_dscp_marking_arguments(parser) + + def args2body(self, parsed_args): + body = {} + update_dscp_args2body(parsed_args, body) + return {self.resource: body} + + +class ListQoSDscpMarkingRules(qos_rule.QosRuleMixin, + neutronv20.ListCommand): + """List all QoS DSCP marking rules belonging to the specified policy.""" + + _formatters = {} + pagination_support = True + sorting_support = True + resource = DSCP_MARKING_RESOURCE + + +class ShowQoSDscpMarkingRule(qos_rule.QosRuleMixin, + neutronv20.ShowCommand): + """Show information about the given qos dscp marking rule.""" + + resource = DSCP_MARKING_RESOURCE + allow_names = False + + +class UpdateQoSDscpMarkingRule(qos_rule.QosRuleMixin, + neutronv20.UpdateCommand): + """Update the given QoS DSCP marking rule.""" + + allow_names = False + resource = DSCP_MARKING_RESOURCE + + def add_known_arguments(self, parser): + super(UpdateQoSDscpMarkingRule, self).add_known_arguments(parser) + add_dscp_marking_arguments(parser) + + def args2body(self, parsed_args): + body = {} + update_dscp_args2body(parsed_args, body) + return {self.resource: body} + + +class DeleteQoSDscpMarkingRule(qos_rule.QosRuleMixin, + neutronv20.DeleteCommand): + """Delete a given qos dscp marking rule.""" + + allow_names = False + resource = DSCP_MARKING_RESOURCE diff --git a/neutronclient/neutron/v2_0/qos/minimum_bandwidth_rule.py b/neutronclient/neutron/v2_0/qos/minimum_bandwidth_rule.py new file mode 100644 index 000000000..0dda0fb3d --- /dev/null +++ b/neutronclient/neutron/v2_0/qos/minimum_bandwidth_rule.py @@ -0,0 +1,111 @@ +# Copyright (c) 2016 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 neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronv20 +from neutronclient.neutron.v2_0.qos import rule as qos_rule + + +MINIMUM_BANDWIDTH_RULE_RESOURCE = 'minimum_bandwidth_rule' + + +def add_minimum_bandwidth_arguments(parser): + parser.add_argument( + '--min-kbps', + required=True, + type=str, + help=_('QoS minimum bandwidth assurance, expressed in kilobits ' + 'per second.')) + # NOTE(ralonsoh): the only direction implemented is "egress". Please, + # refer to the spec (https://review.opendev.org/#/c/316082/). + parser.add_argument( + '--direction', + # NOTE(ihrachys): though server picks the default for us (egress), it's + # better to require the argument to make the UI more explicit and the + # intentions more clear in the future when we add other values for the + # attribute on server side. + required=True, + type=utils.convert_to_lowercase, + choices=['egress'], + help=_('Traffic direction.')) + + +def update_minimum_bandwidth_args2body(parsed_args, body): + neutronv20.update_dict(parsed_args, body, ['min_kbps', 'direction']) + + +class CreateQoSMinimumBandwidthRule(qos_rule.QosRuleMixin, + neutronv20.CreateCommand): + """Create a qos minimum bandwidth rule.""" + + resource = MINIMUM_BANDWIDTH_RULE_RESOURCE + + def add_known_arguments(self, parser): + super(CreateQoSMinimumBandwidthRule, self).add_known_arguments( + parser) + add_minimum_bandwidth_arguments(parser) + + def args2body(self, parsed_args): + body = {} + update_minimum_bandwidth_args2body(parsed_args, body) + return {self.resource: body} + + +class ListQoSMinimumBandwidthRules(qos_rule.QosRuleMixin, + neutronv20.ListCommand): + """List all qos minimum bandwidth rules belonging to the specified policy. + + """ + + resource = MINIMUM_BANDWIDTH_RULE_RESOURCE + _formatters = {} + pagination_support = True + sorting_support = True + + +class ShowQoSMinimumBandwidthRule(qos_rule.QosRuleMixin, + neutronv20.ShowCommand): + """Show information about the given qos minimum bandwidth rule.""" + + resource = MINIMUM_BANDWIDTH_RULE_RESOURCE + allow_names = False + + +class UpdateQoSMinimumBandwidthRule(qos_rule.QosRuleMixin, + neutronv20.UpdateCommand): + """Update the given qos minimum bandwidth rule.""" + + resource = MINIMUM_BANDWIDTH_RULE_RESOURCE + allow_names = False + + def add_known_arguments(self, parser): + super(UpdateQoSMinimumBandwidthRule, self).add_known_arguments( + parser) + add_minimum_bandwidth_arguments(parser) + + def args2body(self, parsed_args): + body = {} + update_minimum_bandwidth_args2body(parsed_args, body) + return {self.resource: body} + + +class DeleteQoSMinimumBandwidthRule(qos_rule.QosRuleMixin, + neutronv20.DeleteCommand): + """Delete a given qos minimum bandwidth rule.""" + + resource = MINIMUM_BANDWIDTH_RULE_RESOURCE + allow_names = False diff --git a/neutronclient/neutron/v2_0/qos/policy.py b/neutronclient/neutron/v2_0/qos/policy.py new file mode 100644 index 000000000..3dc982f31 --- /dev/null +++ b/neutronclient/neutron/v2_0/qos/policy.py @@ -0,0 +1,160 @@ +# Copyright 2015 Huawei Technologies India Pvt Ltd, 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 os + +from neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronv20 + + +def get_qos_policy_id(client, policy_id_or_name): + _policy_id = neutronv20.find_resourceid_by_name_or_id( + client, 'policy', policy_id_or_name, cmd_resource='qos_policy') + return _policy_id + + +class CreateQosPolicyMixin(object): + def add_arguments_qos_policy(self, parser): + qos_policy_args = parser.add_mutually_exclusive_group() + qos_policy_args.add_argument( + '--qos-policy', + help=_('ID or name of the QoS policy that should' + 'be attached to the resource.')) + return qos_policy_args + + def args2body_qos_policy(self, parsed_args, resource): + if parsed_args.qos_policy: + _policy_id = get_qos_policy_id(self.get_client(), + parsed_args.qos_policy) + resource['qos_policy_id'] = _policy_id + + +class UpdateQosPolicyMixin(CreateQosPolicyMixin): + def add_arguments_qos_policy(self, parser): + qos_policy_args = (super(UpdateQosPolicyMixin, self). + add_arguments_qos_policy(parser)) + qos_policy_args.add_argument( + '--no-qos-policy', + action='store_true', + help=_('Detach QoS policy from the resource.')) + return qos_policy_args + + def args2body_qos_policy(self, parsed_args, resource): + super(UpdateQosPolicyMixin, self).args2body_qos_policy(parsed_args, + resource) + if parsed_args.no_qos_policy: + resource['qos_policy_id'] = None + + +class ListQoSPolicy(neutronv20.ListCommand): + """List QoS policies that belong to a given tenant connection.""" + + resource = 'policy' + shadow_resource = 'qos_policy' + list_columns = ['id', 'name'] + pagination_support = True + sorting_support = True + + +class ShowQoSPolicy(neutronv20.ShowCommand): + """Show information of a given qos policy.""" + + resource = 'policy' + shadow_resource = 'qos_policy' + + def format_output_data(self, data): + rules = [] + for rule in data['policy'].get('rules', []): + rules.append("%s (type: %s)" % (rule['id'], rule['type'])) + data['policy']['rules'] = os.linesep.join(rules) + + super(ShowQoSPolicy, self).format_output_data(data) + + +class CreateQoSPolicy(neutronv20.CreateCommand): + """Create a qos policy.""" + + resource = 'policy' + shadow_resource = 'qos_policy' + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of the QoS policy to be created.')) + parser.add_argument( + '--description', + help=_('Description of the QoS policy to be created.')) + parser.add_argument( + '--shared', + action='store_true', + help=_('Accessible by other tenants. ' + 'Set shared to True (default is False).')) + + def args2body(self, parsed_args): + body = {'name': parsed_args.name} + if parsed_args.description: + body['description'] = parsed_args.description + if parsed_args.shared: + body['shared'] = parsed_args.shared + if parsed_args.tenant_id: + body['tenant_id'] = parsed_args.tenant_id + return {self.resource: body} + + +class UpdateQoSPolicy(neutronv20.UpdateCommand): + """Update a given qos policy.""" + + resource = 'policy' + shadow_resource = 'qos_policy' + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help=_('Name of the QoS policy.')) + parser.add_argument( + '--description', + help=_('Description of the QoS policy.')) + shared_group = parser.add_mutually_exclusive_group() + shared_group.add_argument( + '--shared', + action='store_true', + help=_('Accessible by other tenants. ' + 'Set shared to True (default is False).')) + shared_group.add_argument( + '--no-shared', + action='store_true', + help=_('Not accessible by other tenants. ' + 'Set shared to False.')) + + def args2body(self, parsed_args): + body = {} + if parsed_args.name: + body['name'] = parsed_args.name + if parsed_args.description: + body['description'] = parsed_args.description + if parsed_args.shared: + body['shared'] = True + if parsed_args.no_shared: + body['shared'] = False + + return {self.resource: body} + + +class DeleteQoSPolicy(neutronv20.DeleteCommand): + """Delete a given qos policy.""" + + resource = 'policy' + shadow_resource = 'qos_policy' diff --git a/neutronclient/neutron/v2_0/qos/rule.py b/neutronclient/neutron/v2_0/qos/rule.py new file mode 100644 index 000000000..a2fc9a472 --- /dev/null +++ b/neutronclient/neutron/v2_0/qos/rule.py @@ -0,0 +1,63 @@ +# Copyright 2015 Huawei Technologies India Pvt Ltd, 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 neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronv20 +from neutronclient.neutron.v2_0.qos import policy as qos_policy + + +def add_policy_argument(parser): + parser.add_argument( + 'policy', metavar='QOS_POLICY', + help=_('ID or name of the QoS policy.')) + + +def add_rule_argument(parser): + parser.add_argument( + 'rule', metavar='QOS_RULE', + help=_('ID of the QoS rule.')) + + +def update_policy_args2body(parsed_args, body): + neutronv20.update_dict(parsed_args, body, ['policy']) + + +def update_rule_args2body(parsed_args, body): + neutronv20.update_dict(parsed_args, body, ['rule']) + + +class QosRuleMixin(object): + def add_known_arguments(self, parser): + add_policy_argument(parser) + + def set_extra_attrs(self, parsed_args): + self.parent_id = qos_policy.get_qos_policy_id(self.get_client(), + parsed_args.policy) + + def args2body(self, parsed_args): + body = {} + update_policy_args2body(parsed_args, body) + return {'qos_rule': body} + + +class ListQoSRuleTypes(neutronv20.ListCommand): + """List available qos rule types.""" + + resource = 'rule_type' + shadow_resource = 'qos_rule_type' + pagination_support = True + sorting_support = True diff --git a/neutronclient/neutron/v2_0/quota.py b/neutronclient/neutron/v2_0/quota.py new file mode 100644 index 000000000..a55e29f6d --- /dev/null +++ b/neutronclient/neutron/v2_0/quota.py @@ -0,0 +1,256 @@ +# 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 abc +import argparse + +from cliff import lister +from cliff import show +from oslo_serialization import jsonutils + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def get_tenant_id(args, client): + return (args.pos_tenant_id or args.tenant_id or + client.get_quotas_tenant()['tenant']['tenant_id']) + + +class DeleteQuota(neutronV20.NeutronCommand): + """Delete defined quotas of a given tenant.""" + + resource = 'quota' + + def get_parser(self, prog_name): + parser = super(DeleteQuota, 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) + parser.add_argument( + 'pos_tenant_id', + help=argparse.SUPPRESS, nargs='?') + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + tenant_id = get_tenant_id(parsed_args, neutron_client) + obj_deleter = getattr(neutron_client, + "delete_%s" % self.resource) + obj_deleter(tenant_id) + print((_('Deleted %(resource)s: %(tenant_id)s') + % {'tenant_id': tenant_id, + 'resource': self.resource}), + file=self.app.stdout) + return + + +class ListQuota(neutronV20.NeutronCommand, lister.Lister): + """List quotas of all tenants who have non-default quota values.""" + + resource = 'quota' + + def get_parser(self, prog_name): + parser = super(ListQuota, self).get_parser(prog_name) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + search_opts = {} + self.log.debug('search options: %s', search_opts) + obj_lister = getattr(neutron_client, + "list_%ss" % self.resource) + data = obj_lister(**search_opts) + info = [] + collection = self.resource + "s" + if collection in data: + info = data[collection] + _columns = len(info) > 0 and sorted(info[0].keys()) or [] + return (_columns, (utils.get_item_properties(s, _columns) + for s in info)) + + +class ShowQuotaBase(neutronV20.NeutronCommand, show.ShowOne): + """Base class to show quotas of a given tenant.""" + + resource = "quota" + + @abc.abstractmethod + def retrieve_data(self, tenant_id, neutron_client): + """Retrieve data using neutron client for the given tenant.""" + + def get_parser(self, prog_name): + parser = super(ShowQuotaBase, 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) + # allow people to do neutron quota-show . + # we use a different name for this because the default will + # override whatever is in the named arg otherwise. + parser.add_argument( + 'pos_tenant_id', + help=argparse.SUPPRESS, nargs='?') + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + tenant_id = get_tenant_id(parsed_args, neutron_client) + data = self.retrieve_data(tenant_id, neutron_client) + if self.resource in data: + return zip(*sorted(data[self.resource].items())) + return + + +class ShowQuota(ShowQuotaBase): + """Show quotas for a given tenant.""" + + def retrieve_data(self, tenant_id, neutron_client): + return neutron_client.show_quota(tenant_id) + + +class ShowQuotaDefault(ShowQuotaBase): + """Show default quotas for a given tenant.""" + + def retrieve_data(self, tenant_id, neutron_client): + return neutron_client.show_quota_default(tenant_id) + + +class UpdateQuota(neutronV20.NeutronCommand, show.ShowOne): + """Update a given tenant's quotas.""" + + resource = 'quota' + + def get_parser(self, prog_name): + parser = super(UpdateQuota, 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) + parser.add_argument( + '--network', metavar='networks', + help=_('The limit of networks.')) + parser.add_argument( + '--subnet', metavar='subnets', + help=_('The limit of subnets.')) + parser.add_argument( + '--port', metavar='ports', + help=_('The limit of ports.')) + parser.add_argument( + '--router', metavar='routers', + help=_('The limit of routers.')) + parser.add_argument( + '--floatingip', metavar='floatingips', + help=_('The limit of floating IPs.')) + parser.add_argument( + '--security-group', metavar='security_groups', + help=_('The limit of security groups.')) + parser.add_argument( + '--security-group-rule', metavar='security_group_rules', + help=_('The limit of security groups rules.')) + parser.add_argument( + '--vip', metavar='vips', + help=_('The limit of vips.')) + parser.add_argument( + '--pool', metavar='pools', + help=_('The limit of pools.')) + parser.add_argument( + '--member', metavar='members', + help=_('The limit of pool members.')) + parser.add_argument( + '--health-monitor', metavar='health_monitors', + dest='healthmonitor', + help=_('The limit of health monitors.')) + parser.add_argument( + '--loadbalancer', metavar='loadbalancers', + help=_('The limit of load balancers.')) + parser.add_argument( + '--listener', metavar='listeners', + help=_('The limit of listeners.')) + parser.add_argument( + '--rbac-policy', metavar='rbac_policies', + help=_('The limit of RBAC policies.')) + parser.add_argument( + 'pos_tenant_id', + help=argparse.SUPPRESS, nargs='?') + + return parser + + def _validate_int(self, name, value): + try: + return_value = int(value) + except Exception: + message = (_('Quota limit for %(name)s must be an integer') % + {'name': name}) + raise exceptions.CommandError(message=message) + return return_value + + def args2body(self, parsed_args): + quota = {} + for resource in ('network', 'subnet', 'port', 'router', 'floatingip', + 'security_group', 'security_group_rule', + 'vip', 'pool', 'member', 'healthmonitor', + 'loadbalancer', 'listener', 'rbac_policy'): + if getattr(parsed_args, resource): + quota[resource] = self._validate_int( + resource, + getattr(parsed_args, resource)) + if not quota: + raise exceptions.CommandError( + message=_('Must specify a valid resource with new quota ' + 'value')) + return {self.resource: quota} + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _extra_values = neutronV20.parse_args_to_dict(self.values_specs) + neutronV20._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 + obj_updator = getattr(neutron_client, + "update_%s" % self.resource) + tenant_id = get_tenant_id(parsed_args, neutron_client) + data = obj_updator(tenant_id, body) + if self.resource in data: + for k, v in data[self.resource].items(): + if isinstance(v, list): + value = "" + for _item in v: + if value: + value += "\n" + if isinstance(_item, dict): + value += jsonutils.dumps(_item) + else: + value += str(_item) + data[self.resource][k] = value + elif v is None: + data[self.resource][k] = '' + return zip(*sorted(data[self.resource].items())) + else: + return diff --git a/neutronclient/neutron/v2_0/rbac.py b/neutronclient/neutron/v2_0/rbac.py new file mode 100644 index 000000000..8b356d3ec --- /dev/null +++ b/neutronclient/neutron/v2_0/rbac.py @@ -0,0 +1,118 @@ +# Copyright 2015 Huawei Technologies India Pvt Ltd. +# 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 neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + +# key=object_type: value={key=resource, value=cmd_resource} +RBAC_OBJECTS = {'network': {'network': 'network'}, + 'qos-policy': {'policy': 'qos_policy'}} + + +def _get_cmd_resource(obj_type): + resource = list(RBAC_OBJECTS[obj_type])[0] + cmd_resource = RBAC_OBJECTS[obj_type][resource] + return resource, cmd_resource + + +def get_rbac_obj_params(client, obj_type, obj_id_or_name): + resource, cmd_resource = _get_cmd_resource(obj_type) + obj_id = neutronV20.find_resourceid_by_name_or_id( + client=client, resource=resource, name_or_id=obj_id_or_name, + cmd_resource=cmd_resource) + + return obj_id, cmd_resource + + +class ListRBACPolicy(neutronV20.ListCommand): + """List RBAC policies that belong to a given tenant.""" + + resource = 'rbac_policy' + list_columns = ['id', 'object_type', 'object_id'] + pagination_support = True + sorting_support = True + allow_names = False + + +class ShowRBACPolicy(neutronV20.ShowCommand): + """Show information of a given RBAC policy.""" + + resource = 'rbac_policy' + allow_names = False + + +class CreateRBACPolicy(neutronV20.CreateCommand): + """Create a RBAC policy for a given tenant.""" + + resource = 'rbac_policy' + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', + metavar='RBAC_OBJECT', + help=_('ID or name of the RBAC object.')) + parser.add_argument( + '--type', choices=RBAC_OBJECTS.keys(), + required=True, + type=utils.convert_to_lowercase, + help=_('Type of the object that RBAC policy affects.')) + parser.add_argument( + '--target-tenant', + default='*', + help=_('ID of the tenant to which the RBAC ' + 'policy will be enforced.')) + parser.add_argument( + '--action', choices=['access_as_external', 'access_as_shared'], + type=utils.convert_to_lowercase, + required=True, + help=_('Action for the RBAC policy.')) + + def args2body(self, parsed_args): + neutron_client = self.get_client() + _object_id, _object_type = get_rbac_obj_params(neutron_client, + parsed_args.type, + parsed_args.name) + body = { + 'object_id': _object_id, + 'object_type': _object_type, + 'target_tenant': parsed_args.target_tenant, + 'action': parsed_args.action, + } + return {self.resource: body} + + +class UpdateRBACPolicy(neutronV20.UpdateCommand): + """Update RBAC policy for given tenant.""" + + resource = 'rbac_policy' + allow_names = False + + def add_known_arguments(self, parser): + parser.add_argument( + '--target-tenant', + help=_('ID of the tenant to which the RBAC ' + 'policy will be enforced.')) + + def args2body(self, parsed_args): + body = {'target_tenant': parsed_args.target_tenant} + return {self.resource: body} + + +class DeleteRBACPolicy(neutronV20.DeleteCommand): + """Delete a RBAC policy.""" + + resource = 'rbac_policy' + allow_names = False diff --git a/neutronclient/neutron/v2_0/router.py b/neutronclient/neutron/v2_0/router.py new file mode 100644 index 000000000..1f78cb85c --- /dev/null +++ b/neutronclient/neutron/v2_0/router.py @@ -0,0 +1,297 @@ +# 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 argparse + +from oslo_serialization import jsonutils + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 +from neutronclient.neutron.v2_0 import availability_zone + + +def _format_external_gateway_info(router): + try: + return jsonutils.dumps(router['external_gateway_info']) + except (TypeError, KeyError): + return '' + + +class ListRouter(neutronV20.ListCommand): + """List routers that belong to a given tenant.""" + + resource = 'router' + _formatters = {'external_gateway_info': _format_external_gateway_info, } + list_columns = ['id', 'name', 'external_gateway_info', 'distributed', 'ha'] + pagination_support = True + sorting_support = True + + +class ShowRouter(neutronV20.ShowCommand): + """Show information of a given router.""" + + resource = 'router' + + +class CreateRouter(neutronV20.CreateCommand): + """Create a router for a given tenant.""" + + resource = 'router' + _formatters = {'external_gateway_info': _format_external_gateway_info, } + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--admin_state_down', + dest='admin_state', action='store_false', + help=argparse.SUPPRESS) + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of the router to be created.')) + parser.add_argument( + '--description', + help=_('Description of router.')) + parser.add_argument( + '--flavor', + help=_('ID or name of flavor.')) + utils.add_boolean_argument( + parser, '--distributed', dest='distributed', + help=_('Create a distributed router.')) + utils.add_boolean_argument( + parser, '--ha', dest='ha', + help=_('Create a highly available router.')) + + availability_zone.add_az_hint_argument(parser, self.resource) + + def args2body(self, parsed_args): + body = {'admin_state_up': parsed_args.admin_state} + if parsed_args.flavor: + _flavor_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'flavor', parsed_args.flavor) + body['flavor_id'] = _flavor_id + neutronV20.update_dict(parsed_args, body, + ['name', 'tenant_id', 'distributed', 'ha', + 'description']) + availability_zone.args2body_az_hint(parsed_args, body) + return {self.resource: body} + + +class DeleteRouter(neutronV20.DeleteCommand): + """Delete a given router.""" + + resource = 'router' + + +class UpdateRouter(neutronV20.UpdateCommand): + """Update router's information.""" + + resource = 'router' + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help=_('Updated name of the router.')) + parser.add_argument( + '--description', + help=_('Description of router.')) + utils.add_boolean_argument( + parser, '--admin-state-up', dest='admin_state', + help=_('Specify the administrative state of the router ' + '(True means "Up").')) + utils.add_boolean_argument( + parser, '--admin_state_up', dest='admin_state', + help=argparse.SUPPRESS) + utils.add_boolean_argument( + parser, '--distributed', dest='distributed', + help=_('True means this router should operate in ' + 'distributed mode.')) + routes_group = parser.add_mutually_exclusive_group() + routes_group.add_argument( + '--route', metavar='destination=CIDR,nexthop=IP_ADDR', + action='append', dest='routes', + type=utils.str2dict_type(required_keys=['destination', 'nexthop']), + help=_('Route to associate with the router.' + ' You can repeat this option.')) + routes_group.add_argument( + '--no-routes', + action='store_true', + help=_('Remove routes associated with the router.')) + + def args2body(self, parsed_args): + body = {} + if hasattr(parsed_args, 'admin_state'): + body['admin_state_up'] = parsed_args.admin_state + neutronV20.update_dict(parsed_args, body, + ['name', 'distributed', 'description']) + if parsed_args.no_routes: + body['routes'] = None + elif parsed_args.routes: + body['routes'] = parsed_args.routes + return {self.resource: body} + + +class RouterInterfaceCommand(neutronV20.NeutronCommand): + """Based class to Add/Remove router interface.""" + + resource = 'router' + + def call_api(self, neutron_client, router_id, body): + raise NotImplementedError() + + def success_message(self, router_id, portinfo): + raise NotImplementedError() + + def get_parser(self, prog_name): + parser = super(RouterInterfaceCommand, self).get_parser(prog_name) + parser.add_argument( + 'router', metavar='ROUTER', + help=_('ID or name of the router.')) + parser.add_argument( + 'interface', metavar='INTERFACE', + help=_('The format is "SUBNET|subnet=SUBNET|port=PORT". ' + 'Either a subnet or port must be specified. ' + 'Both ID and name are accepted as SUBNET or PORT. ' + 'Note that "subnet=" can be omitted when specifying a ' + 'subnet.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + + if '=' in parsed_args.interface: + resource, value = parsed_args.interface.split('=', 1) + if resource not in ['subnet', 'port']: + exceptions.CommandError(_('You must specify either subnet or ' + 'port for INTERFACE parameter.')) + else: + resource = 'subnet' + value = parsed_args.interface + + _router_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, self.resource, parsed_args.router) + + _interface_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, resource, value) + body = {'%s_id' % resource: _interface_id} + + portinfo = self.call_api(neutron_client, _router_id, body) + print(self.success_message(parsed_args.router, portinfo), + file=self.app.stdout) + + +class AddInterfaceRouter(RouterInterfaceCommand): + """Add an internal network interface to a router.""" + + def call_api(self, neutron_client, router_id, body): + return neutron_client.add_interface_router(router_id, body) + + def success_message(self, router_id, portinfo): + return (_('Added interface %(port)s to router %(router)s.') % + {'router': router_id, 'port': portinfo['port_id']}) + + +class RemoveInterfaceRouter(RouterInterfaceCommand): + """Remove an internal network interface from a router.""" + + def call_api(self, neutron_client, router_id, body): + return neutron_client.remove_interface_router(router_id, body) + + def success_message(self, router_id, portinfo): + # portinfo is not used since it is None for router-interface-delete. + return _('Removed interface from router %s.') % router_id + + +class SetGatewayRouter(neutronV20.NeutronCommand): + """Set the external network gateway for a router.""" + + resource = 'router' + + def get_parser(self, prog_name): + parser = super(SetGatewayRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', metavar='ROUTER', + help=_('ID or name of the router.')) + parser.add_argument( + 'external_network', metavar='EXTERNAL-NETWORK', + help=_('ID or name of the external network for the gateway.')) + parser.add_argument( + '--enable-snat', action='store_true', + help=_('Enable source NAT on the router gateway.')) + parser.add_argument( + '--disable-snat', action='store_true', + help=_('Disable source NAT on the router gateway.')) + parser.add_argument( + '--fixed-ip', metavar='subnet_id=SUBNET,ip_address=IP_ADDR', + action='append', + type=utils.str2dict_type(optional_keys=['subnet_id', + 'ip_address']), + help=_('Desired IP and/or subnet on external network: ' + 'subnet_id=,ip_address=. ' + 'You can specify both of subnet_id and ip_address or ' + 'specify one of them as well. ' + 'You can repeat this option.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _router_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, self.resource, parsed_args.router) + _ext_net_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'network', parsed_args.external_network) + router_dict = {'network_id': _ext_net_id} + if parsed_args.enable_snat: + router_dict['enable_snat'] = True + if parsed_args.disable_snat: + router_dict['enable_snat'] = False + if parsed_args.fixed_ip: + ips = [] + for ip_spec in parsed_args.fixed_ip: + subnet_name_id = ip_spec.get('subnet_id') + if subnet_name_id: + subnet_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, 'subnet', subnet_name_id) + ip_spec['subnet_id'] = subnet_id + ips.append(ip_spec) + router_dict['external_fixed_ips'] = ips + neutron_client.add_gateway_router(_router_id, router_dict) + print(_('Set gateway for router %s') % parsed_args.router, + file=self.app.stdout) + + +class RemoveGatewayRouter(neutronV20.NeutronCommand): + """Remove an external network gateway from a router.""" + + resource = 'router' + + def get_parser(self, prog_name): + parser = super(RemoveGatewayRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', metavar='ROUTER', + help=_('ID or name of the router.')) + return parser + + def take_action(self, parsed_args): + neutron_client = self.get_client() + _router_id = neutronV20.find_resourceid_by_name_or_id( + neutron_client, self.resource, parsed_args.router) + neutron_client.remove_gateway_router(_router_id) + print(_('Removed gateway from router %s') % parsed_args.router, + file=self.app.stdout) diff --git a/neutronclient/neutron/v2_0/securitygroup.py b/neutronclient/neutron/v2_0/securitygroup.py new file mode 100644 index 000000000..3c0bafa0e --- /dev/null +++ b/neutronclient/neutron/v2_0/securitygroup.py @@ -0,0 +1,379 @@ +# 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 argparse + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _get_remote(rule): + if rule['remote_ip_prefix']: + remote = '%s (CIDR)' % rule['remote_ip_prefix'] + elif rule['remote_group_id']: + remote = '%s (group)' % rule['remote_group_id'] + else: + remote = None + return remote + + +def _get_protocol_port(rule): + proto = rule['protocol'] + port_min = rule['port_range_min'] + port_max = rule['port_range_max'] + if proto in ('tcp', 'udp'): + if (port_min and port_min == port_max): + protocol_port = '%s/%s' % (port_min, proto) + elif port_min: + protocol_port = '%s-%s/%s' % (port_min, port_max, proto) + else: + protocol_port = proto + elif proto == 'icmp': + icmp_opts = [] + if port_min is not None: + icmp_opts.append('type:%s' % port_min) + if port_max is not None: + icmp_opts.append('code:%s' % port_max) + + if icmp_opts: + protocol_port = 'icmp (%s)' % ', '.join(icmp_opts) + else: + protocol_port = 'icmp' + elif proto is not None: + # port_range_min/max are not recognized for protocol + # other than TCP, UDP and ICMP. + protocol_port = proto + else: + protocol_port = None + + return protocol_port + + +def _format_sg_rule(rule): + formatted = [] + for field in ['direction', + 'ethertype', + ('protocol_port', _get_protocol_port), + 'remote_ip_prefix', + 'remote_group_id']: + if isinstance(field, tuple): + field, get_method = field + data = get_method(rule) + else: + data = rule[field] + if not data: + continue + if field in ('remote_ip_prefix', 'remote_group_id'): + data = '%s: %s' % (field, data) + formatted.append(data) + return ', '.join(formatted) + + +def _format_sg_rules(secgroup): + try: + return '\n'.join(sorted([_format_sg_rule(rule) for rule + in secgroup['security_group_rules']])) + except Exception: + return '' + + +def generate_default_ethertype(protocol): + if protocol == 'icmpv6': + return 'IPv6' + return 'IPv4' + + +class ListSecurityGroup(neutronV20.ListCommand): + """List security groups that belong to a given tenant.""" + + resource = 'security_group' + list_columns = ['id', 'name', 'security_group_rules'] + _formatters = {'security_group_rules': _format_sg_rules} + pagination_support = True + sorting_support = True + + +class ShowSecurityGroup(neutronV20.ShowCommand): + """Show information of a given security group.""" + + resource = 'security_group' + allow_names = True + json_indent = 5 + + +class CreateSecurityGroup(neutronV20.CreateCommand): + """Create a security group.""" + + resource = 'security_group' + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of the security group to be created.')) + parser.add_argument( + '--description', + help=_('Description of the security group to be created.')) + + def args2body(self, parsed_args): + body = {'name': parsed_args.name} + neutronV20.update_dict(parsed_args, body, + ['description', 'tenant_id']) + return {'security_group': body} + + +class DeleteSecurityGroup(neutronV20.DeleteCommand): + """Delete a given security group.""" + + resource = 'security_group' + allow_names = True + + +class UpdateSecurityGroup(neutronV20.UpdateCommand): + """Update a given security group.""" + + resource = 'security_group' + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help=_('Updated name of the security group.')) + parser.add_argument( + '--description', + help=_('Updated description of the security group.')) + + def args2body(self, parsed_args): + body = {} + neutronV20.update_dict(parsed_args, body, + ['name', 'description']) + return {'security_group': body} + + +class ListSecurityGroupRule(neutronV20.ListCommand): + """List security group rules that belong to a given tenant.""" + + resource = 'security_group_rule' + list_columns = ['id', 'security_group_id', 'direction', + 'ethertype', 'port/protocol', 'remote'] + # replace_rules: key is an attribute name in Neutron API and + # corresponding value is a display name shown by CLI. + replace_rules = {'security_group_id': 'security_group', + 'remote_group_id': 'remote_group'} + digest_fields = { + # The entry 'protocol/port' is left deliberately for backwards + # compatibility. + 'remote': { + 'method': _get_remote, + 'depends_on': ['remote_ip_prefix', 'remote_group_id']}, + 'port/protocol': { + 'method': _get_protocol_port, + 'depends_on': ['protocol', 'port_range_min', 'port_range_max']}, + 'protocol/port': { + 'method': _get_protocol_port, + 'depends_on': ['protocol', 'port_range_min', 'port_range_max']}} + pagination_support = True + sorting_support = True + + def get_parser(self, prog_name): + parser = super(ListSecurityGroupRule, self).get_parser(prog_name) + parser.add_argument( + '--no-nameconv', action='store_true', + help=_('Do not convert security group ID to its name.')) + return parser + + @staticmethod + def replace_columns(cols, rules, reverse=False): + if reverse: + rules = dict((rules[k], k) for k in rules.keys()) + return [rules.get(col, col) for col in cols] + + def get_required_fields(self, fields): + fields = self.replace_columns(fields, self.replace_rules, reverse=True) + for field, digest_fields in self.digest_fields.items(): + if field in fields: + fields += digest_fields['depends_on'] + fields.remove(field) + return fields + + def retrieve_list(self, parsed_args): + parsed_args.fields = self.get_required_fields(parsed_args.fields) + return super(ListSecurityGroupRule, self).retrieve_list(parsed_args) + + def _get_sg_name_dict(self, data, page_size, no_nameconv): + """Get names of security groups referred in the retrieved rules. + + :return: a dict from secgroup ID to secgroup name + """ + if no_nameconv: + return {} + neutron_client = self.get_client() + search_opts = {'fields': ['id', 'name']} + if self.pagination_support: + if page_size: + search_opts.update({'limit': page_size}) + sec_group_ids = set() + for rule in data: + for key in self.replace_rules: + if rule.get(key): + sec_group_ids.add(rule[key]) + sec_group_ids = list(sec_group_ids) + + def _get_sec_group_list(sec_group_ids): + search_opts['id'] = sec_group_ids + return neutron_client.list_security_groups( + **search_opts).get('security_groups', []) + + try: + secgroups = _get_sec_group_list(sec_group_ids) + except exceptions.RequestURITooLong as uri_len_exc: + # Length of a query filter on security group rule id + # id=& (with len(uuid)=36) + sec_group_id_filter_len = 40 + # The URI is too long because of too many sec_group_id filters + # Use the excess attribute of the exception to know how many + # sec_group_id filters can be inserted into a single request + sec_group_count = len(sec_group_ids) + max_size = ((sec_group_id_filter_len * sec_group_count) - + uri_len_exc.excess) + chunk_size = max_size // sec_group_id_filter_len + secgroups = [] + for i in range(0, sec_group_count, chunk_size): + secgroups.extend( + _get_sec_group_list(sec_group_ids[i: i + chunk_size])) + + return dict([(sg['id'], sg['name']) + for sg in secgroups if sg['name']]) + + @staticmethod + def _has_fields(rule, required_fields): + return all([key in rule for key in required_fields]) + + def extend_list(self, data, parsed_args): + sg_dict = self._get_sg_name_dict(data, parsed_args.page_size, + parsed_args.no_nameconv) + for rule in data: + # Replace security group UUID with its name. + for key in self.replace_rules: + if key in rule: + rule[key] = sg_dict.get(rule[key], rule[key]) + for field, digest_rule in self.digest_fields.items(): + if self._has_fields(rule, digest_rule['depends_on']): + rule[field] = digest_rule['method'](rule) or 'any' + + def setup_columns(self, info, parsed_args): + # Translate the specified columns from the command line + # into field names used in "info". + parsed_args.columns = self.replace_columns(parsed_args.columns, + self.replace_rules, + reverse=True) + # NOTE(amotoki): 2nd element of the tuple returned by setup_columns() + # is a generator, so if you need to create a look using the generator + # object, you need to recreate a generator to show a list expectedly. + info = super(ListSecurityGroupRule, self).setup_columns(info, + parsed_args) + cols = info[0] + if not parsed_args.no_nameconv: + # Replace column names in the header line (in info[0]) + cols = self.replace_columns(info[0], self.replace_rules) + parsed_args.columns = cols + return (cols, info[1]) + + +class ShowSecurityGroupRule(neutronV20.ShowCommand): + """Show information of a given security group rule.""" + + resource = 'security_group_rule' + allow_names = False + + +class CreateSecurityGroupRule(neutronV20.CreateCommand): + """Create a security group rule.""" + + resource = 'security_group_rule' + + def add_known_arguments(self, parser): + parser.add_argument( + '--description', + help=_('Description of security group rule.')) + parser.add_argument( + 'security_group_id', metavar='SECURITY_GROUP', + help=_('ID or name of the security group to ' + 'which the rule is added.')) + parser.add_argument( + '--direction', + type=utils.convert_to_lowercase, + default='ingress', choices=['ingress', 'egress'], + help=_('Direction of traffic: ingress/egress.')) + parser.add_argument( + '--ethertype', + help=_('IPv4/IPv6')) + parser.add_argument( + '--protocol', + type=utils.convert_to_lowercase, + help=_('Protocol of packet. Allowed values are ' + '[icmp, icmpv6, tcp, udp] and ' + 'integer representations [0-255].')) + parser.add_argument( + '--port-range-min', + help=_('Starting port range. For ICMP it is type.')) + parser.add_argument( + '--port_range_min', + help=argparse.SUPPRESS) + parser.add_argument( + '--port-range-max', + help=_('Ending port range. For ICMP it is code.')) + parser.add_argument( + '--port_range_max', + help=argparse.SUPPRESS) + parser.add_argument( + '--remote-ip-prefix', + help=_('CIDR to match on.')) + parser.add_argument( + '--remote_ip_prefix', + help=argparse.SUPPRESS) + parser.add_argument( + '--remote-group-id', metavar='REMOTE_GROUP', + help=_('ID or name of the remote security group ' + 'to which the rule is applied.')) + parser.add_argument( + '--remote_group_id', + help=argparse.SUPPRESS) + + def args2body(self, parsed_args): + _security_group_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'security_group', parsed_args.security_group_id) + body = {'security_group_id': _security_group_id, + 'direction': parsed_args.direction, + 'ethertype': parsed_args.ethertype or + generate_default_ethertype(parsed_args.protocol)} + neutronV20.update_dict(parsed_args, body, + ['protocol', 'port_range_min', 'port_range_max', + 'remote_ip_prefix', 'tenant_id', + 'description']) + if parsed_args.remote_group_id: + _remote_group_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'security_group', + parsed_args.remote_group_id) + body['remote_group_id'] = _remote_group_id + return {'security_group_rule': body} + + +class DeleteSecurityGroupRule(neutronV20.DeleteCommand): + """Delete a given security group rule.""" + + resource = 'security_group_rule' + allow_names = False diff --git a/neutronclient/neutron/v2_0/servicetype.py b/neutronclient/neutron/v2_0/servicetype.py new file mode 100644 index 000000000..ec8454f94 --- /dev/null +++ b/neutronclient/neutron/v2_0/servicetype.py @@ -0,0 +1,27 @@ +# 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. +# + +from neutronclient.neutron import v2_0 as neutronV20 + + +class ListServiceProvider(neutronV20.ListCommand): + """List service providers.""" + + resource = 'service_provider' + list_columns = ['service_type', 'name', 'default'] + _formatters = {} + pagination_support = True + sorting_support = True diff --git a/neutronclient/neutron/v2_0/subnet.py b/neutronclient/neutron/v2_0/subnet.py new file mode 100644 index 000000000..633826422 --- /dev/null +++ b/neutronclient/neutron/v2_0/subnet.py @@ -0,0 +1,276 @@ +# 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 argparse + +from oslo_serialization import jsonutils + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _format_allocation_pools(subnet): + try: + return '\n'.join([jsonutils.dumps(pool) for pool in + subnet['allocation_pools']]) + except (TypeError, KeyError): + return '' + + +def _format_dns_nameservers(subnet): + try: + return '\n'.join([jsonutils.dumps(server) for server in + subnet['dns_nameservers']]) + except (TypeError, KeyError): + return '' + + +def _format_host_routes(subnet): + try: + return '\n'.join([jsonutils.dumps(route) for route in + subnet['host_routes']]) + except (TypeError, KeyError): + return '' + + +def add_updatable_arguments(parser): + parser.add_argument( + '--name', + help=_('Name of this subnet.')) + parser.add_argument( + '--description', + help=_('Description of this subnet.')) + gateway_sg = parser.add_mutually_exclusive_group() + gateway_sg.add_argument( + '--gateway', metavar='GATEWAY_IP', + help=_('Gateway IP of this subnet.')) + gateway_sg.add_argument( + '--no-gateway', + action='store_true', + help=_('Do not configure a gateway for this subnet.')) + parser.add_argument( + '--allocation-pool', metavar='start=IP_ADDR,end=IP_ADDR', + action='append', dest='allocation_pools', + type=utils.str2dict_type(required_keys=['start', 'end']), + help=_('Allocation pool IP addresses for this subnet ' + '(This option can be repeated).')) + parser.add_argument( + '--allocation_pool', + action='append', dest='allocation_pools', + type=utils.str2dict_type(required_keys=['start', 'end']), + help=argparse.SUPPRESS) + parser.add_argument( + '--host-route', metavar='destination=CIDR,nexthop=IP_ADDR', + action='append', dest='host_routes', + type=utils.str2dict_type(required_keys=['destination', 'nexthop']), + help=_('Additional route (This option can be repeated).')) + parser.add_argument( + '--dns-nameserver', metavar='DNS_NAMESERVER', + action='append', dest='dns_nameservers', + help=_('DNS name server for this subnet ' + '(This option can be repeated).')) + parser.add_argument( + '--disable-dhcp', + action='store_true', + help=_('Disable DHCP for this subnet.')) + parser.add_argument( + '--enable-dhcp', + action='store_true', + help=_('Enable DHCP for this subnet.')) + # NOTE(ihrachys): yes, that's awful, but should be left as-is for + # backwards compatibility for versions <=2.3.4 that passed the + # boolean values through to the server without any argument + # validation. + parser.add_argument( + '--enable-dhcp=True', + action='store_true', + dest='enable_dhcp', + help=argparse.SUPPRESS) + parser.add_argument( + '--enable-dhcp=False', + action='store_true', + dest='disable_dhcp', + help=argparse.SUPPRESS) + + +def updatable_args2body(parsed_args, body, for_create=True, ip_version=None): + if parsed_args.disable_dhcp and parsed_args.enable_dhcp: + raise exceptions.CommandError(_( + "You cannot enable and disable DHCP at the same time.")) + + neutronV20.update_dict(parsed_args, body, + ['name', 'allocation_pools', + 'host_routes', 'dns_nameservers', + 'description']) + if parsed_args.no_gateway: + body['gateway_ip'] = None + elif parsed_args.gateway: + body['gateway_ip'] = parsed_args.gateway + if parsed_args.disable_dhcp: + body['enable_dhcp'] = False + if parsed_args.enable_dhcp: + body['enable_dhcp'] = True + if for_create and parsed_args.ipv6_ra_mode: + if ip_version == 4: + raise exceptions.CommandError(_("--ipv6-ra-mode is invalid " + "when --ip-version is 4")) + body['ipv6_ra_mode'] = parsed_args.ipv6_ra_mode + if for_create and parsed_args.ipv6_address_mode: + if ip_version == 4: + raise exceptions.CommandError(_("--ipv6-address-mode is " + "invalid when --ip-version " + "is 4")) + body['ipv6_address_mode'] = parsed_args.ipv6_address_mode + + +class ListSubnet(neutronV20.ListCommand): + """List subnets that belong to a given tenant.""" + + resource = 'subnet' + _formatters = {'allocation_pools': _format_allocation_pools, + 'dns_nameservers': _format_dns_nameservers, + 'host_routes': _format_host_routes, } + list_columns = ['id', 'name', 'cidr', 'allocation_pools'] + pagination_support = True + sorting_support = True + + +class ShowSubnet(neutronV20.ShowCommand): + """Show information of a given subnet.""" + + resource = 'subnet' + + +class CreateSubnet(neutronV20.CreateCommand): + """Create a subnet for a given tenant.""" + + resource = 'subnet' + + def add_known_arguments(self, parser): + add_updatable_arguments(parser) + parser.add_argument( + '--ip-version', + type=int, + default=4, choices=[4, 6], + help=_('IP version to use, default is 4. ' + 'Note that when subnetpool is specified, ' + 'IP version is determined from the subnetpool ' + 'and this option is ignored.')) + parser.add_argument( + '--ip_version', + type=int, + choices=[4, 6], + help=argparse.SUPPRESS) + parser.add_argument( + 'network_id', metavar='NETWORK', + help=_('Network ID or name this subnet belongs to.')) + parser.add_argument( + 'cidr', nargs='?', metavar='CIDR', + help=_('CIDR of subnet to create.')) + parser.add_argument( + '--ipv6-ra-mode', + type=utils.convert_to_lowercase, + choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'], + help=_('IPv6 RA (Router Advertisement) mode.')) + parser.add_argument( + '--ipv6-address-mode', + type=utils.convert_to_lowercase, + choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'], + help=_('IPv6 address mode.')) + parser.add_argument( + '--subnetpool', metavar='SUBNETPOOL', + help=_('ID or name of subnetpool from which this subnet ' + 'will obtain a CIDR.')) + parser.add_argument( + '--use-default-subnetpool', + action='store_true', + help=_('Use default subnetpool for ip_version, if it exists.')) + parser.add_argument( + '--prefixlen', metavar='PREFIX_LENGTH', + help=_('Prefix length for subnet allocation from subnetpool.')) + parser.add_argument( + '--segment', metavar='SEGMENT', + help=_('ID of segment with which this subnet will be associated.')) + + def args2body(self, parsed_args): + _network_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'network', parsed_args.network_id) + body = {'network_id': _network_id} + + if parsed_args.prefixlen: + body['prefixlen'] = parsed_args.prefixlen + ip_version = parsed_args.ip_version + if parsed_args.use_default_subnetpool: + body['use_default_subnetpool'] = True + if parsed_args.segment: + body['segment_id'] = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'segment', parsed_args.segment) + if parsed_args.subnetpool: + if parsed_args.subnetpool == 'None': + _subnetpool_id = None + else: + _subnetpool = neutronV20.find_resource_by_name_or_id( + self.get_client(), 'subnetpool', parsed_args.subnetpool) + _subnetpool_id = _subnetpool['id'] + # Now that we have the pool_id - let's just have a check on the + # ip version used in the pool + ip_version = _subnetpool['ip_version'] + body['subnetpool_id'] = _subnetpool_id + + # IP version needs to be set as IP version can be + # determined by subnetpool. + body['ip_version'] = ip_version + + if parsed_args.cidr: + # With subnetpool, cidr is now optional for creating subnet. + cidr = parsed_args.cidr + body['cidr'] = cidr + unusable_cidr = '/32' if ip_version == 4 else '/128' + if cidr.endswith(unusable_cidr): + self.log.warning(_("An IPv%(ip)d subnet with a %(cidr)s CIDR " + "will have only one usable IP address so " + "the device attached to it will not have " + "any IP connectivity."), + {"ip": ip_version, + "cidr": unusable_cidr}) + + updatable_args2body(parsed_args, body, ip_version=ip_version) + if parsed_args.tenant_id: + body['tenant_id'] = parsed_args.tenant_id + + return {'subnet': body} + + +class DeleteSubnet(neutronV20.DeleteCommand): + """Delete a given subnet.""" + + resource = 'subnet' + + +class UpdateSubnet(neutronV20.UpdateCommand): + """Update subnet's information.""" + + resource = 'subnet' + + def add_known_arguments(self, parser): + add_updatable_arguments(parser) + + def args2body(self, parsed_args): + body = {} + updatable_args2body(parsed_args, body, for_create=False) + return {'subnet': body} diff --git a/neutronclient/neutron/v2_0/subnetpool.py b/neutronclient/neutron/v2_0/subnetpool.py new file mode 100644 index 000000000..fdefc8a47 --- /dev/null +++ b/neutronclient/neutron/v2_0/subnetpool.py @@ -0,0 +1,154 @@ +# Copyright 2015 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 neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronV20 + + +def _format_prefixes(subnetpool): + try: + return '\n'.join(pool for pool in subnetpool['prefixes']) + except (TypeError, KeyError): + return subnetpool['prefixes'] + + +def add_updatable_arguments(parser, for_create=False): + parser.add_argument( + '--description', + help=_('Description of subnetpool.')) + parser.add_argument( + '--min-prefixlen', type=int, + help=_('Subnetpool minimum prefix length.')) + parser.add_argument( + '--max-prefixlen', type=int, + help=_('Subnetpool maximum prefix length.')) + parser.add_argument( + '--default-prefixlen', type=int, + help=_('Subnetpool default prefix length.')) + parser.add_argument( + '--pool-prefix', + action='append', dest='prefixes', + required=for_create, + help=_('Subnetpool prefixes (This option can be repeated).')) + utils.add_boolean_argument( + parser, '--is-default', + help=_('Specify whether this should be the default subnetpool ' + '(True meaning default).')) + + +def updatable_args2body(parsed_args, body): + neutronV20.update_dict(parsed_args, body, + ['name', 'prefixes', 'default_prefixlen', + 'min_prefixlen', 'max_prefixlen', 'is_default', + 'description']) + + +class ListSubnetPool(neutronV20.ListCommand): + """List subnetpools that belong to a given tenant.""" + + _formatters = {'prefixes': _format_prefixes, } + resource = 'subnetpool' + list_columns = ['id', 'name', 'prefixes', + 'default_prefixlen', 'address_scope_id', 'is_default'] + pagination_support = True + sorting_support = True + + +class ShowSubnetPool(neutronV20.ShowCommand): + """Show information of a given subnetpool.""" + + resource = 'subnetpool' + + +class CreateSubnetPool(neutronV20.CreateCommand): + """Create a subnetpool for a given tenant.""" + + resource = 'subnetpool' + + def add_known_arguments(self, parser): + add_updatable_arguments(parser, for_create=True) + parser.add_argument( + '--shared', + action='store_true', + help=_('Set the subnetpool as shared.')) + parser.add_argument( + 'name', + metavar='NAME', + help=_('Name of the subnetpool to be created.')) + parser.add_argument( + '--address-scope', + metavar='ADDRSCOPE', + help=_('ID or name of the address scope with which the subnetpool ' + 'is associated. Prefixes must be unique across address ' + 'scopes.')) + + def args2body(self, parsed_args): + body = {'prefixes': parsed_args.prefixes} + updatable_args2body(parsed_args, body) + neutronV20.update_dict(parsed_args, body, ['tenant_id']) + if parsed_args.shared: + body['shared'] = True + + # Parse and update for "address-scope" option + if parsed_args.address_scope: + _addrscope_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'address_scope', + parsed_args.address_scope) + body['address_scope_id'] = _addrscope_id + return {'subnetpool': body} + + +class DeleteSubnetPool(neutronV20.DeleteCommand): + """Delete a given subnetpool.""" + + resource = 'subnetpool' + + +class UpdateSubnetPool(neutronV20.UpdateCommand): + """Update subnetpool's information.""" + + resource = 'subnetpool' + + def add_known_arguments(self, parser): + add_updatable_arguments(parser) + parser.add_argument('--name', + help=_('Updated name of the subnetpool.')) + addrscope_args = parser.add_mutually_exclusive_group() + addrscope_args.add_argument('--address-scope', + metavar='ADDRSCOPE', + help=_('ID or name of the address scope ' + 'with which the subnetpool is ' + 'associated. Prefixes must be ' + 'unique across address scopes.')) + addrscope_args.add_argument('--no-address-scope', + action='store_true', + help=_('Detach subnetpool from the ' + 'address scope.')) + + def args2body(self, parsed_args): + body = {} + updatable_args2body(parsed_args, body) + + # Parse and update for "address-scope" option/s + if parsed_args.no_address_scope: + body['address_scope_id'] = None + elif parsed_args.address_scope: + _addrscope_id = neutronV20.find_resourceid_by_name_or_id( + self.get_client(), 'address_scope', + parsed_args.address_scope) + body['address_scope_id'] = _addrscope_id + return {'subnetpool': body} diff --git a/neutronclient/neutron/v2_0/tag.py b/neutronclient/neutron/v2_0/tag.py new file mode 100644 index 000000000..ff59b74ec --- /dev/null +++ b/neutronclient/neutron/v2_0/tag.py @@ -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 neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.neutron import v2_0 as neutronv20 + + +# List of resources can be set tag +TAG_RESOURCES = ['network', 'subnet', 'port', 'router', 'subnetpool'] + + +def _convert_resource_args(client, parsed_args): + resource_type = client.get_resource_plural(parsed_args.resource_type) + resource_id = neutronv20.find_resourceid_by_name_or_id( + client, parsed_args.resource_type, parsed_args.resource) + return resource_type, resource_id + + +def _add_common_arguments(parser): + parser.add_argument('--resource-type', + choices=TAG_RESOURCES, + dest='resource_type', + required=True, + help=_('Resource Type.')) + parser.add_argument('--resource', + required=True, + help=_('Resource name or ID.')) + + +class AddTag(neutronv20.NeutronCommand): + """Add a tag into the resource.""" + + def get_parser(self, prog_name): + parser = super(AddTag, self).get_parser(prog_name) + _add_common_arguments(parser) + parser.add_argument('--tag', + required=True, + help=_('Tag to be added.')) + return parser + + def take_action(self, parsed_args): + client = self.get_client() + if not parsed_args.tag: + raise exceptions.CommandError( + _('Cannot add an empty value as tag')) + resource_type, resource_id = _convert_resource_args(client, + parsed_args) + client.add_tag(resource_type, resource_id, parsed_args.tag) + + +class ReplaceTag(neutronv20.NeutronCommand): + """Replace all tags on the resource.""" + + def get_parser(self, prog_name): + parser = super(ReplaceTag, self).get_parser(prog_name) + _add_common_arguments(parser) + parser.add_argument('--tag', + metavar='TAG', + action='append', + dest='tags', + required=True, + help=_('Tag (This option can be repeated).')) + return parser + + def take_action(self, parsed_args): + client = self.get_client() + resource_type, resource_id = _convert_resource_args(client, + parsed_args) + body = {'tags': parsed_args.tags} + client.replace_tag(resource_type, resource_id, body) + + +class RemoveTag(neutronv20.NeutronCommand): + """Remove a tag on the resource.""" + + def get_parser(self, prog_name): + parser = super(RemoveTag, self).get_parser(prog_name) + _add_common_arguments(parser) + tag_opt = parser.add_mutually_exclusive_group() + tag_opt.add_argument('--all', + action='store_true', + help=_('Remove all tags on the resource.')) + tag_opt.add_argument('--tag', + help=_('Tag to be removed.')) + return parser + + def take_action(self, parsed_args): + if not parsed_args.all and not parsed_args.tag: + raise exceptions.CommandError( + _("--all or --tag must be specified")) + client = self.get_client() + resource_type, resource_id = _convert_resource_args(client, + parsed_args) + if parsed_args.all: + client.remove_tag_all(resource_type, resource_id) + else: + client.remove_tag(resource_type, resource_id, parsed_args.tag) diff --git a/neutronclient/neutron/v2_0/vpn/__init__.py b/neutronclient/neutron/v2_0/vpn/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/neutron/v2_0/vpn/endpoint_group.py b/neutronclient/neutron/v2_0/vpn/endpoint_group.py new file mode 100644 index 000000000..a112b315e --- /dev/null +++ b/neutronclient/neutron/v2_0/vpn/endpoint_group.py @@ -0,0 +1,100 @@ +# (c) Copyright 2015 Cisco 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 neutronclient._i18n import _ +from neutronclient.neutron import v2_0 as neutronv20 + + +def add_known_endpoint_group_arguments(parser, is_create=True): + parser.add_argument( + '--name', + help=_('Set a name for the endpoint group.')) + parser.add_argument( + '--description', + help=_('Set a description for the endpoint group.')) + if is_create: + parser.add_argument( + '--type', + required=is_create, + help=_('Type of endpoints in group (e.g. subnet, cidr, vlan).')) + parser.add_argument( + '--value', + action='append', dest='endpoints', + required=is_create, + help=_('Endpoint(s) for the group. Must all be of the same type.')) + + +class ListEndpointGroup(neutronv20.ListCommand): + """List VPN endpoint groups that belong to a given tenant.""" + + resource = 'endpoint_group' + list_columns = ['id', 'name', 'type', 'endpoints'] + _formatters = {} + pagination_support = True + sorting_support = True + + +class ShowEndpointGroup(neutronv20.ShowCommand): + """Show a specific VPN endpoint group.""" + + resource = 'endpoint_group' + + +class CreateEndpointGroup(neutronv20.CreateCommand): + """Create a VPN endpoint group.""" + resource = 'endpoint_group' + + def add_known_arguments(self, parser): + add_known_endpoint_group_arguments(parser) + + def subnet_name2id(self, endpoint): + return neutronv20.find_resourceid_by_name_or_id(self.get_client(), + 'subnet', endpoint) + + def args2body(self, parsed_args): + if parsed_args.type == 'subnet': + endpoints = [self.subnet_name2id(ep) + for ep in parsed_args.endpoints] + else: + endpoints = parsed_args.endpoints + + body = {'endpoints': endpoints} + + neutronv20.update_dict(parsed_args, body, + ['name', 'description', + 'tenant_id', 'type']) + return {self.resource: body} + + +class UpdateEndpointGroup(neutronv20.UpdateCommand): + """Update a given VPN endpoint group.""" + + resource = 'endpoint_group' + + def add_known_arguments(self, parser): + add_known_endpoint_group_arguments(parser, is_create=False) + + def args2body(self, parsed_args): + body = {} + neutronv20.update_dict(parsed_args, body, + ['name', 'description']) + return {self.resource: body} + + +class DeleteEndpointGroup(neutronv20.DeleteCommand): + """Delete a given VPN endpoint group.""" + + resource = 'endpoint_group' diff --git a/neutronclient/neutron/v2_0/vpn/ikepolicy.py b/neutronclient/neutron/v2_0/vpn/ikepolicy.py new file mode 100644 index 000000000..20a96aaea --- /dev/null +++ b/neutronclient/neutron/v2_0/vpn/ikepolicy.py @@ -0,0 +1,128 @@ +# (c) Copyright 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 argparse + +from neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronv20 +from neutronclient.neutron.v2_0.vpn import utils as vpn_utils + + +def add_common_args(parser, is_create=True): + parser.add_argument( + '--description', + help=_('Description of the IKE policy.')) + parser.add_argument( + '--auth-algorithm', + type=utils.convert_to_lowercase, + default='sha1' if is_create else argparse.SUPPRESS, + choices=['sha1', 'sha256', 'sha384', 'sha512'], + help=_('Authentication algorithm, default:sha1.')) + parser.add_argument( + '--encryption-algorithm', + default='aes-128' if is_create else argparse.SUPPRESS, + type=utils.convert_to_lowercase, + help=_('Encryption algorithm, default:aes-128.')) + parser.add_argument( + '--phase1-negotiation-mode', + default='main' if is_create else argparse.SUPPRESS, + choices=['main'], + type=utils.convert_to_lowercase, + help=_('IKE Phase1 negotiation mode, default:main.')) + parser.add_argument( + '--ike-version', + default='v1' if is_create else argparse.SUPPRESS, + choices=['v1', 'v2'], + type=utils.convert_to_lowercase, + help=_('IKE version for the policy, default:v1.')) + parser.add_argument( + '--pfs', + default='group5' if is_create else argparse.SUPPRESS, + type=utils.convert_to_lowercase, + help=_('Perfect Forward Secrecy, default:group5.')) + parser.add_argument( + '--lifetime', + metavar="units=UNITS,value=VALUE", + type=utils.str2dict_type(optional_keys=['units', 'value']), + help=vpn_utils.lifetime_help("IKE")) + + +def parse_common_args2body(parsed_args, body): + neutronv20.update_dict(parsed_args, body, + ['auth_algorithm', 'encryption_algorithm', + 'phase1_negotiation_mode', 'ike_version', + 'pfs', 'name', 'description', 'tenant_id']) + if parsed_args.lifetime: + vpn_utils.validate_lifetime_dict(parsed_args.lifetime) + body['lifetime'] = parsed_args.lifetime + return body + + +class ListIKEPolicy(neutronv20.ListCommand): + """List IKE policies that belong to a tenant.""" + + resource = 'ikepolicy' + list_columns = ['id', 'name', 'auth_algorithm', + 'encryption_algorithm', 'ike_version', 'pfs'] + _formatters = {} + pagination_support = True + sorting_support = True + + +class ShowIKEPolicy(neutronv20.ShowCommand): + """Show information of a given IKE policy.""" + + resource = 'ikepolicy' + help_resource = 'IKE policy' + + +class CreateIKEPolicy(neutronv20.CreateCommand): + """Create an IKE policy.""" + + resource = 'ikepolicy' + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of the IKE policy.')) + add_common_args(parser) + + def args2body(self, parsed_args): + return {'ikepolicy': parse_common_args2body(parsed_args, body={})} + + +class UpdateIKEPolicy(neutronv20.UpdateCommand): + """Update a given IKE policy.""" + + resource = 'ikepolicy' + help_resource = 'IKE policy' + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help=_('Updated name of the IKE policy.')) + add_common_args(parser, is_create=False) + + def args2body(self, parsed_args): + return {'ikepolicy': parse_common_args2body(parsed_args, body={})} + + +class DeleteIKEPolicy(neutronv20.DeleteCommand): + """Delete a given IKE policy.""" + + resource = 'ikepolicy' + help_resource = 'IKE policy' diff --git a/neutronclient/neutron/v2_0/vpn/ipsec_site_connection.py b/neutronclient/neutron/v2_0/vpn/ipsec_site_connection.py new file mode 100644 index 000000000..03a635f6e --- /dev/null +++ b/neutronclient/neutron/v2_0/vpn/ipsec_site_connection.py @@ -0,0 +1,220 @@ +# (c) Copyright 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 argparse + +from oslo_serialization import jsonutils + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronv20 +from neutronclient.neutron.v2_0.vpn import utils as vpn_utils + + +def _format_peer_cidrs(ipsec_site_connection): + try: + return '\n'.join([jsonutils.dumps(cidrs) for cidrs in + ipsec_site_connection['peer_cidrs']]) + except (TypeError, KeyError): + return '' + + +class ListIPsecSiteConnection(neutronv20.ListCommand): + """List IPsec site connections that belong to a given tenant.""" + + resource = 'ipsec_site_connection' + _formatters = {'peer_cidrs': _format_peer_cidrs} + list_columns = [ + 'id', 'name', 'peer_address', 'auth_mode', 'status'] + pagination_support = True + sorting_support = True + + +class ShowIPsecSiteConnection(neutronv20.ShowCommand): + """Show information of a given IPsec site connection.""" + + resource = 'ipsec_site_connection' + help_resource = 'IPsec site connection' + + +class IPsecSiteConnectionMixin(object): + + def add_known_arguments(self, parser, is_create=True): + parser.add_argument( + '--name', + help=_('Set friendly name for the connection.')) + parser.add_argument( + '--description', + help=_('Set a description for the connection.')) + parser.add_argument( + '--dpd', + metavar="action=ACTION,interval=INTERVAL,timeout=TIMEOUT", + type=utils.str2dict_type( + optional_keys=['action', 'interval', 'timeout']), + help=vpn_utils.dpd_help("IPsec connection.")) + parser.add_argument( + '--local-ep-group', + help=_('Local endpoint group ID/name with subnet(s) for ' + 'IPSec connection.')) + parser.add_argument( + '--peer-ep-group', + help=_('Peer endpoint group ID/name with CIDR(s) for ' + 'IPSec connection.')) + parser.add_argument( + '--peer-cidr', + action='append', dest='peer_cidrs', + help=_('[DEPRECATED in Mitaka] Remote subnet(s) in CIDR format. ' + 'Cannot be specified when using endpoint groups. Only ' + 'applicable, if subnet provided for VPN service.')) + parser.add_argument( + '--peer-id', + required=is_create, + help=_('Peer router identity for authentication. Can be ' + 'IPv4/IPv6 address, e-mail address, key id, or FQDN.')) + parser.add_argument( + '--peer-address', + required=is_create, + help=_('Peer gateway public IPv4/IPv6 address or FQDN.')) + parser.add_argument( + '--psk', + required=is_create, + help=_('Pre-shared key string.')) + parser.add_argument( + '--mtu', + default='1500' if is_create else argparse.SUPPRESS, + help=_('MTU size for the connection, default:1500.')) + parser.add_argument( + '--initiator', + default='bi-directional' if is_create else argparse.SUPPRESS, + choices=['bi-directional', 'response-only'], + help=_('Initiator state in lowercase, default:bi-directional')) + + def args2body(self, parsed_args, body=None): + """Add in conditional args and then return all conn info.""" + + if body is None: + body = {} + if parsed_args.dpd: + vpn_utils.validate_dpd_dict(parsed_args.dpd) + body['dpd'] = parsed_args.dpd + if parsed_args.local_ep_group: + _local_epg = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'endpoint_group', + parsed_args.local_ep_group) + body['local_ep_group_id'] = _local_epg + if parsed_args.peer_ep_group: + _peer_epg = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'endpoint_group', + parsed_args.peer_ep_group) + body['peer_ep_group_id'] = _peer_epg + if hasattr(parsed_args, 'mtu') and int(parsed_args.mtu) < 68: + message = _("Invalid MTU value: MTU must be " + "greater than or equal to 68.") + raise exceptions.CommandError(message) + # ToDo (Reedip) : Remove below check when peer-cidr is removed + if parsed_args.peer_cidrs and parsed_args.local_ep_group: + message = _("You cannot specify both endpoint groups and peer " + "CIDR(s).") + raise exceptions.CommandError(message) + neutronv20.update_dict(parsed_args, body, + ['peer_id', 'mtu', 'initiator', 'psk', + 'peer_address', 'name', 'description', + 'peer_cidrs']) + return {self.resource: body} + + +class CreateIPsecSiteConnection(IPsecSiteConnectionMixin, + neutronv20.CreateCommand): + """Create an IPsec site connection.""" + resource = 'ipsec_site_connection' + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + default=True, action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + '--vpnservice-id', metavar='VPNSERVICE', + required=True, + help=_('VPN service instance ID associated with this connection.')) + parser.add_argument( + '--ikepolicy-id', metavar='IKEPOLICY', + required=True, + help=_('IKE policy ID associated with this connection.')) + parser.add_argument( + '--ipsecpolicy-id', metavar='IPSECPOLICY', + required=True, + help=_('IPsec policy ID associated with this connection.')) + super(CreateIPsecSiteConnection, self).add_known_arguments(parser) + + def args2body(self, parsed_args): + _vpnservice_id = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'vpnservice', + parsed_args.vpnservice_id) + _ikepolicy_id = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'ikepolicy', + parsed_args.ikepolicy_id) + _ipsecpolicy_id = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'ipsecpolicy', + parsed_args.ipsecpolicy_id) + body = { + 'vpnservice_id': _vpnservice_id, + 'ikepolicy_id': _ikepolicy_id, + 'ipsecpolicy_id': _ipsecpolicy_id, + 'admin_state_up': parsed_args.admin_state_down, + } + if parsed_args.tenant_id: + body['tenant_id'] = parsed_args.tenant_id + + if (bool(parsed_args.local_ep_group) != + bool(parsed_args.peer_ep_group)): + message = _("You must specify both local and peer endpoint " + "groups.") + raise exceptions.CommandError(message) + if not parsed_args.peer_cidrs and not parsed_args.local_ep_group: + message = _("You must specify endpoint groups or peer CIDR(s).") + raise exceptions.CommandError(message) + return super(CreateIPsecSiteConnection, self).args2body(parsed_args, + body) + + +class UpdateIPsecSiteConnection(IPsecSiteConnectionMixin, + neutronv20.UpdateCommand): + """Update a given IPsec site connection.""" + + resource = 'ipsec_site_connection' + help_resource = 'IPsec site connection' + + def add_known_arguments(self, parser): + utils.add_boolean_argument( + parser, '--admin-state-up', + help=_('Update the administrative state. (True meaning "Up")')) + super(UpdateIPsecSiteConnection, self).add_known_arguments(parser, + False) + + def args2body(self, parsed_args): + body = {} + neutronv20.update_dict(parsed_args, body, ['admin_state_up']) + return super(UpdateIPsecSiteConnection, self).args2body(parsed_args, + body) + + +class DeleteIPsecSiteConnection(neutronv20.DeleteCommand): + """Delete a given IPsec site connection.""" + + resource = 'ipsec_site_connection' + help_resource = 'IPsec site connection' diff --git a/neutronclient/neutron/v2_0/vpn/ipsecpolicy.py b/neutronclient/neutron/v2_0/vpn/ipsecpolicy.py new file mode 100644 index 000000000..ea70155b7 --- /dev/null +++ b/neutronclient/neutron/v2_0/vpn/ipsecpolicy.py @@ -0,0 +1,129 @@ +# (c) Copyright 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 argparse + +from neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronv20 +from neutronclient.neutron.v2_0.vpn import utils as vpn_utils + + +def add_common_args(parser, is_create=True): + parser.add_argument( + '--auth-algorithm', + default='sha1' if is_create else argparse.SUPPRESS, + type=utils.convert_to_lowercase, + choices=['sha1', 'sha256', 'sha384', 'sha512'], + help=_('Authentication algorithm for IPsec policy, default:sha1.')) + parser.add_argument( + '--description', + help=_('Description of the IPsec policy.')) + parser.add_argument( + '--encapsulation-mode', + default='tunnel' if is_create else argparse.SUPPRESS, + choices=['tunnel', 'transport'], + type=utils.convert_to_lowercase, + help=_('Encapsulation mode for IPsec policy, default:tunnel.')) + parser.add_argument( + '--encryption-algorithm', + default='aes-128' if is_create else argparse.SUPPRESS, + type=utils.convert_to_lowercase, + help=_('Encryption algorithm for IPsec policy, default:aes-128.')) + parser.add_argument( + '--lifetime', + metavar="units=UNITS,value=VALUE", + type=utils.str2dict_type(optional_keys=['units', 'value']), + help=vpn_utils.lifetime_help("IPsec")) + parser.add_argument( + '--pfs', + default='group5' if is_create else argparse.SUPPRESS, + type=utils.convert_to_lowercase, + help=_('Perfect Forward Secrecy for IPsec policy, default:group5.')) + parser.add_argument( + '--transform-protocol', + default='esp' if is_create else argparse.SUPPRESS, + type=utils.convert_to_lowercase, + choices=['esp', 'ah', 'ah-esp'], + help=_('Transform protocol for IPsec policy, default:esp.')) + + +def parse_common_args2body(parsed_args, body): + neutronv20.update_dict(parsed_args, body, + ['auth_algorithm', 'encryption_algorithm', + 'encapsulation_mode', 'transform_protocol', + 'pfs', 'name', 'description', 'tenant_id']) + if parsed_args.lifetime: + vpn_utils.validate_lifetime_dict(parsed_args.lifetime) + body['lifetime'] = parsed_args.lifetime + return body + + +class ListIPsecPolicy(neutronv20.ListCommand): + """List IPsec policies that belong to a given tenant connection.""" + + resource = 'ipsecpolicy' + list_columns = ['id', 'name', 'auth_algorithm', + 'encryption_algorithm', 'pfs'] + _formatters = {} + pagination_support = True + sorting_support = True + + +class ShowIPsecPolicy(neutronv20.ShowCommand): + """Show information of a given IPsec policy.""" + + resource = 'ipsecpolicy' + help_resource = 'IPsec policy' + + +class CreateIPsecPolicy(neutronv20.CreateCommand): + """Create an IPsec policy.""" + + resource = 'ipsecpolicy' + help_resource = 'IPsec policy' + + def add_known_arguments(self, parser): + parser.add_argument( + 'name', metavar='NAME', + help=_('Name of the IPsec policy.')) + add_common_args(parser) + + def args2body(self, parsed_args): + return {'ipsecpolicy': parse_common_args2body(parsed_args, body={})} + + +class UpdateIPsecPolicy(neutronv20.UpdateCommand): + """Update a given IPsec policy.""" + + resource = 'ipsecpolicy' + help_resource = 'IPsec policy' + + def add_known_arguments(self, parser): + parser.add_argument( + '--name', + help=_('Updated name of the IPsec policy.')) + add_common_args(parser, is_create=False) + + def args2body(self, parsed_args): + return {'ipsecpolicy': parse_common_args2body(parsed_args, body={})} + + +class DeleteIPsecPolicy(neutronv20.DeleteCommand): + """Delete a given IPsec policy.""" + + resource = 'ipsecpolicy' + help_resource = 'IPsec policy' diff --git a/neutronclient/neutron/v2_0/vpn/utils.py b/neutronclient/neutron/v2_0/vpn/utils.py new file mode 100644 index 000000000..65d9c7c4b --- /dev/null +++ b/neutronclient/neutron/v2_0/vpn/utils.py @@ -0,0 +1,112 @@ +# (c) Copyright 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. +# + + +"""VPN Utilities and helper functions.""" + + +from neutronclient._i18n import _ +from neutronclient.common import exceptions + +dpd_supported_actions = ['hold', 'clear', 'restart', + 'restart-by-peer', 'disabled'] +dpd_supported_keys = ['action', 'interval', 'timeout'] + +lifetime_keys = ['units', 'value'] +lifetime_units = ['seconds'] + + +def validate_dpd_dict(dpd_dict): + for key, value in dpd_dict.items(): + if key not in dpd_supported_keys: + message = _( + "DPD Dictionary KeyError: " + "Reason-Invalid DPD key : " + "'%(key)s' not in %(supported_key)s") % { + 'key': key, 'supported_key': dpd_supported_keys} + raise exceptions.CommandError(message) + if key == 'action' and value not in dpd_supported_actions: + message = _( + "DPD Dictionary ValueError: " + "Reason-Invalid DPD action : " + "'%(key_value)s' not in %(supported_action)s") % { + 'key_value': value, + 'supported_action': dpd_supported_actions} + raise exceptions.CommandError(message) + if key in ('interval', 'timeout'): + try: + if int(value) <= 0: + raise ValueError() + except ValueError: + message = _( + "DPD Dictionary ValueError: " + "Reason-Invalid positive integer value: " + "'%(key)s' = %(value)s") % { + 'key': key, 'value': value} + raise exceptions.CommandError(message) + else: + dpd_dict[key] = int(value) + return + + +def validate_lifetime_dict(lifetime_dict): + + for key, value in lifetime_dict.items(): + if key not in lifetime_keys: + message = _( + "Lifetime Dictionary KeyError: " + "Reason-Invalid unit key : " + "'%(key)s' not in %(supported_key)s") % { + 'key': key, 'supported_key': lifetime_keys} + raise exceptions.CommandError(message) + if key == 'units' and value not in lifetime_units: + message = _( + "Lifetime Dictionary ValueError: " + "Reason-Invalid units : " + "'%(key_value)s' not in %(supported_units)s") % { + 'key_value': key, 'supported_units': lifetime_units} + raise exceptions.CommandError(message) + if key == 'value': + try: + if int(value) < 60: + raise ValueError() + except ValueError: + message = _( + "Lifetime Dictionary ValueError: " + "Reason-Invalid value should be at least 60:" + "'%(key_value)s' = %(value)s") % { + 'key_value': key, 'value': value} + raise exceptions.CommandError(message) + else: + lifetime_dict['value'] = int(value) + return + + +def lifetime_help(policy): + lifetime = _("%s lifetime attributes. " + "'units'-seconds, default:seconds. " + "'value'-non negative integer, default:3600.") % policy + return lifetime + + +def dpd_help(policy): + dpd = _(" %s Dead Peer Detection attributes." + " 'action'-hold,clear,disabled,restart,restart-by-peer." + " 'interval' and 'timeout' are non negative integers. " + " 'interval' should be less than 'timeout' value. " + " 'action', default:hold 'interval', default:30, " + " 'timeout', default:120.") % policy.capitalize() + return dpd diff --git a/neutronclient/neutron/v2_0/vpn/vpnservice.py b/neutronclient/neutron/v2_0/vpn/vpnservice.py new file mode 100644 index 000000000..dae531408 --- /dev/null +++ b/neutronclient/neutron/v2_0/vpn/vpnservice.py @@ -0,0 +1,117 @@ +# (c) Copyright 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. +# + +from neutronclient._i18n import _ +from neutronclient.common import utils +from neutronclient.neutron import v2_0 as neutronv20 + + +def add_common_args(parser): + parser.add_argument( + '--name', + help=_('Name for the VPN service.')) + parser.add_argument( + '--description', + help=_('Description for the VPN service.')) + + +def common_args2body(parsed_args, body): + neutronv20.update_dict(parsed_args, body, + ['name', 'description']) + + +class ListVPNService(neutronv20.ListCommand): + """List VPN service configurations that belong to a given tenant.""" + + resource = 'vpnservice' + list_columns = [ + 'id', 'name', 'router_id', 'status' + ] + _formatters = {} + pagination_support = True + sorting_support = True + + +class ShowVPNService(neutronv20.ShowCommand): + """Show information of a given VPN service.""" + + resource = 'vpnservice' + help_resource = 'VPN service' + + +class CreateVPNService(neutronv20.CreateCommand): + """Create a VPN service.""" + resource = 'vpnservice' + + def add_known_arguments(self, parser): + parser.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help=_('Set admin state up to false.')) + parser.add_argument( + 'router', metavar='ROUTER', + help=_('Router unique identifier for the VPN service.')) + parser.add_argument( + 'subnet', nargs='?', metavar='SUBNET', + help=_('[DEPRECATED in Mitaka] Unique identifier for the local ' + 'private subnet.')) + add_common_args(parser) + + def args2body(self, parsed_args): + if parsed_args.subnet: + _subnet_id = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'subnet', parsed_args.subnet) + else: + _subnet_id = None + _router_id = neutronv20.find_resourceid_by_name_or_id( + self.get_client(), 'router', + parsed_args.router) + + body = {'subnet_id': _subnet_id, + 'router_id': _router_id, + 'admin_state_up': parsed_args.admin_state} + neutronv20.update_dict(parsed_args, body, + ['tenant_id']) + common_args2body(parsed_args, body) + return {self.resource: body} + + +class UpdateVPNService(neutronv20.UpdateCommand): + """Update a given VPN service.""" + + resource = 'vpnservice' + help_resource = 'VPN service' + + def add_known_arguments(self, parser): + add_common_args(parser) + utils.add_boolean_argument( + parser, '--admin-state-up', + help=_('Update the admin state for the VPN Service.' + '(True means UP)')) + + def args2body(self, parsed_args): + body = {} + common_args2body(parsed_args, body) + neutronv20.update_dict(parsed_args, body, + ['admin_state_up']) + return {self.resource: body} + + +class DeleteVPNService(neutronv20.DeleteCommand): + """Delete a given VPN service.""" + + resource = 'vpnservice' + help_resource = 'VPN service' diff --git a/neutronclient/osc/__init__.py b/neutronclient/osc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/osc/plugin.py b/neutronclient/osc/plugin.py new file mode 100644 index 000000000..c98099f2a --- /dev/null +++ b/neutronclient/osc/plugin.py @@ -0,0 +1,63 @@ +# 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. +# + +"""OpenStackClient plugin for advanced Networking service.""" + +import logging + +# TODO(rtheis/amotoki): Add functional test infrastructure for OSC +# plugin commands. +# TODO(amotoki): Add and update document on OSC plugin. + +from osc_lib import utils + +LOG = logging.getLogger(__name__) + +DEFAULT_API_VERSION = '2.0' +API_VERSION_OPTION = 'os_network_api_version' +# NOTE(rtheis): API_NAME must NOT be set to 'network' since +# 'network' is owned by OSC! The OSC 'network' client uses +# the OpenStack SDK. +API_NAME = 'neutronclient' +API_VERSIONS = { + '2.0': 'neutronclient.v2_0.client.Client', + '2': 'neutronclient.v2_0.client.Client', +} + + +def make_client(instance): + """Returns an neutron client.""" + neutron_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS) + LOG.debug('Instantiating neutron client: %s', neutron_client) + + # TODO(amotoki): Check the following arguments need to be passed + # to neutronclient class. Check keystoneauth code. + # - endpoint_type (do we need to specify it explicitly?) + # - auth (session object contains auth. Is it required?) + client = neutron_client(session=instance.session, + region_name=instance.region_name, + endpoint_type=instance.interface, + insecure=not instance.verify, + ca_cert=instance.cacert) + return client + + +def build_option_parser(parser): + """Hook to add global options""" + + # NOTE(amotoki): At now we register no option. + # OSC itself has an option for Network API version # and we refer to it. + return parser diff --git a/neutronclient/osc/utils.py b/neutronclient/osc/utils.py new file mode 100644 index 000000000..39b17b640 --- /dev/null +++ b/neutronclient/osc/utils.py @@ -0,0 +1,130 @@ +# Copyright 2016 NEC Corporation +# +# 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 module should contain OSC plugin generic methods. + +Methods in this module are candidates adopted to osc-lib. + +Stuffs specific to neutronclient OSC plugin should not be added +to this module. They should go to neutronclient.osc.v2.utils. +""" + +from keystoneclient import exceptions as identity_exc +from keystoneclient.v3 import domains +from keystoneclient.v3 import projects +from osc_lib import utils + +from neutronclient._i18n import _ + + +# TODO(amotoki): Use osc-lib version once osc-lib provides this. +def add_project_owner_option_to_parser(parser): + """Register project and project domain options. + + :param parser: argparse.Argument parser object. + + """ + parser.add_argument( + '--project', + metavar='', + help=_("Owner's project (name or ID)") + ) + # Borrowed from openstackclient.identity.common + # as it is not exposed officially. + parser.add_argument( + '--project-domain', + metavar='', + help=_('Domain the project belongs to (name or ID). ' + 'This can be used in case collisions between project names ' + 'exist.'), + ) + + +# The following methods are borrowed from openstackclient.identity.common +# as it is not exposed officially. +# TODO(amotoki): Use osc-lib version once osc-lib provides this. + + +def find_domain(identity_client, name_or_id): + return _find_identity_resource(identity_client.domains, name_or_id, + domains.Domain) + + +def find_project(identity_client, name_or_id, domain_name_or_id=None): + domain_id = _get_domain_id_if_requested(identity_client, domain_name_or_id) + if not domain_id: + return _find_identity_resource(identity_client.projects, name_or_id, + projects.Project) + else: + return _find_identity_resource(identity_client.projects, name_or_id, + projects.Project, domain_id=domain_id) + + +def _get_domain_id_if_requested(identity_client, domain_name_or_id): + if not domain_name_or_id: + return None + domain = find_domain(identity_client, domain_name_or_id) + return domain.id + + +def _find_identity_resource(identity_client_manager, name_or_id, + resource_type, **kwargs): + """Find a specific identity resource. + + Using keystoneclient's manager, attempt to find a specific resource by its + name or ID. If Forbidden to find the resource (a common case if the user + does not have permission), then return the resource by creating a local + instance of keystoneclient's Resource. + + The parameter identity_client_manager is a keystoneclient manager, + for example: keystoneclient.v3.users or keystoneclient.v3.projects. + + The parameter resource_type is a keystoneclient resource, for example: + keystoneclient.v3.users.User or keystoneclient.v3.projects.Project. + + :param identity_client_manager: the manager that contains the resource + :type identity_client_manager: `keystoneclient.base.CrudManager` + :param name_or_id: the resources's name or ID + :type name_or_id: string + :param resource_type: class that represents the resource type + :type resource_type: `keystoneclient.base.Resource` + + :returns: the resource in question + :rtype: `keystoneclient.base.Resource` + + """ + + try: + identity_resource = utils.find_resource(identity_client_manager, + name_or_id, **kwargs) + if identity_resource is not None: + return identity_resource + except identity_exc.Forbidden: + pass + + return resource_type(None, {'id': name_or_id, 'name': name_or_id}) + + +# The above are borrowed from openstackclient.identity.common. +# DO NOT ADD original methods in neutronclient repo to the above area. + + +def _get_columns(item): + column_map = {} + hidden_columns = ['location', 'tenant_id'] + return utils.get_osc_show_columns_for_sdk_resource( + item, + column_map, + hidden_columns + ) diff --git a/neutronclient/osc/v2/__init__.py b/neutronclient/osc/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/osc/v2/dynamic_routing/__init__.py b/neutronclient/osc/v2/dynamic_routing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/osc/v2/dynamic_routing/bgp_dragent.py b/neutronclient/osc/v2/dynamic_routing/bgp_dragent.py new file mode 100644 index 000000000..91dc2a75b --- /dev/null +++ b/neutronclient/osc/v2/dynamic_routing/bgp_dragent.py @@ -0,0 +1,112 @@ +# 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 osc_lib.command import command +from osc_lib import utils + +from neutronclient._i18n import _ + + +def _format_alive_state(item): + return ':-)' if item else 'XXX' + + +_formatters = { + 'alive': _format_alive_state +} + + +def add_common_args(parser): + parser.add_argument('dragent_id', + metavar='', + help=_("ID of the dynamic routing agent")) + parser.add_argument('bgp_speaker', + metavar='', + help=_("ID or name of the BGP speaker")) + + +class AddBgpSpeakerToDRAgent(command.Command): + """Add a BGP speaker to a dynamic routing agent""" + + def get_parser(self, prog_name): + parser = super(AddBgpSpeakerToDRAgent, self).get_parser(prog_name) + add_common_args(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker(parsed_args.bgp_speaker).id + client.add_bgp_speaker_to_dragent(parsed_args.dragent_id, speaker_id) + + +class RemoveBgpSpeakerFromDRAgent(command.Command): + """Removes a BGP speaker from a dynamic routing agent""" + + def get_parser(self, prog_name): + parser = super(RemoveBgpSpeakerFromDRAgent, self).get_parser( + prog_name) + add_common_args(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker(parsed_args.bgp_speaker).id + client.remove_bgp_speaker_from_dragent(parsed_args.dragent_id, + speaker_id) + + +class ListDRAgent(command.Lister): + """List dynamic routing agents""" + + resource = 'agent' + list_columns = ['id', 'host', 'admin_state_up', 'alive'] + unknown_parts_flag = False + + def get_parser(self, prog_name): + parser = super(ListDRAgent, + self).get_parser(prog_name) + parser.add_argument('--bgp-speaker', + metavar='', + help=_("List dynamic routing agents hosting a " + "BGP speaker (name or ID)")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + if parsed_args.bgp_speaker is not None: + speaker_id = client.find_bgp_speaker(parsed_args.bgp_speaker).id + data = client.get_bgp_dragents_hosting_speaker(speaker_id) + else: + attrs = {'agent_type': 'BGP dynamic routing agent'} + data = client.agents(**attrs) + columns = ( + 'id', + 'agent_type', + 'host', + 'availability_zone', + 'is_alive', + 'is_admin_state_up', + 'binary' + ) + column_headers = ( + 'ID', + 'Agent Type', + 'Host', + 'Availability Zone', + 'Alive', + 'State', + 'Binary' + ) + return (column_headers, + (utils.get_item_properties( + s, columns,) for s in data)) diff --git a/neutronclient/osc/v2/dynamic_routing/bgp_peer.py b/neutronclient/osc/v2/dynamic_routing/bgp_peer.py new file mode 100644 index 000000000..0610a3ac0 --- /dev/null +++ b/neutronclient/osc/v2/dynamic_routing/bgp_peer.py @@ -0,0 +1,181 @@ +# 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 osc_lib.command import command +from osc_lib import utils + +from neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.common import utils as nc_utils +from neutronclient.osc import utils as nc_osc_utils +from neutronclient.osc.v2.dynamic_routing import constants + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + + # Validate password + if 'auth_type' in parsed_args: + if parsed_args.auth_type != 'none': + if 'password' not in parsed_args or parsed_args.password is None: + raise exceptions.CommandError(_('Must provide password if ' + 'auth-type is specified.')) + if ( + parsed_args.auth_type == 'none' and + parsed_args.password is not None + ): + raise exceptions.CommandError(_('Must provide auth-type if ' + 'password is specified.')) + attrs['auth_type'] = parsed_args.auth_type + + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if 'remote_as' in parsed_args: + attrs['remote_as'] = parsed_args.remote_as + if 'peer_ip' in parsed_args: + attrs['peer_ip'] = parsed_args.peer_ip + if 'password' in parsed_args: + attrs['password'] = parsed_args.password + + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = client_manager.identity + project_id = nc_osc_utils.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + return attrs + + +class CreateBgpPeer(command.ShowOne): + _description = _("Create a BGP peer") + + def get_parser(self, prog_name): + parser = super(CreateBgpPeer, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_("Name of the BGP peer to create")) + parser.add_argument( + '--peer-ip', + metavar='', + required=True, + help=_("Peer IP address")) + parser.add_argument( + '--remote-as', + required=True, + metavar='', + help=_("Peer AS number. (Integer in [%(min_val)s, %(max_val)s] " + "is allowed)") % {'min_val': constants.MIN_AS_NUM, + 'max_val': constants.MAX_AS_NUM}) + parser.add_argument( + '--auth-type', + metavar='', + choices=['none', 'md5'], + type=nc_utils.convert_to_lowercase, + default='none', + help=_("Authentication algorithm. Supported algorithms: " + "none (default), md5")) + parser.add_argument( + '--password', + metavar='', + help=_("Authentication password")) + nc_osc_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_bgp_peer(**attrs) + display_columns, columns = nc_osc_utils._get_columns(obj) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteBgpPeer(command.Command): + _description = _("Delete a BGP peer") + + def get_parser(self, prog_name): + parser = super(DeleteBgpPeer, self).get_parser(prog_name) + parser.add_argument( + 'bgp_peer', + metavar="", + help=_("BGP peer to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + id = client.find_bgp_peer(parsed_args.bgp_peer)['id'] + client.delete_bgp_peer(id) + + +class ListBgpPeer(command.Lister): + _description = _("List BGP peers") + + def take_action(self, parsed_args): + data = self.app.client_manager.network.bgp_peers(retrieve_all=True) + headers = ('ID', 'Name', 'Peer IP', 'Remote AS') + columns = ('id', 'name', 'peer_ip', 'remote_as') + return (headers, + (utils.get_dict_properties(s, columns,) for s in data)) + + +class SetBgpPeer(command.Command): + _description = _("Update a BGP peer") + resource = constants.BGP_PEER + + def get_parser(self, prog_name): + parser = super(SetBgpPeer, self).get_parser(prog_name) + parser.add_argument( + '--name', + help=_("Updated name of the BGP peer")) + parser.add_argument( + '--password', + metavar='', + help=_("Updated authentication password")) + parser.add_argument( + 'bgp_peer', + metavar="", + help=_("BGP peer to update (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + id = client.find_bgp_peer(parsed_args.bgp_peer)['id'] + attrs = _get_attrs(self.app.client_manager, parsed_args) + client.update_bgp_peer(id, **attrs) + + +class ShowBgpPeer(command.ShowOne): + _description = _("Show information for a BGP peer") + + def get_parser(self, prog_name): + parser = super(ShowBgpPeer, self).get_parser(prog_name) + parser.add_argument( + 'bgp_peer', + metavar="", + help=_("BGP peer to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + id = client.find_bgp_peer(parsed_args.bgp_peer, + ignore_missing=False).id + obj = client.get_bgp_peer(id) + display_columns, columns = nc_osc_utils._get_columns(obj) + data = utils.get_dict_properties(obj, columns) + return display_columns, data diff --git a/neutronclient/osc/v2/dynamic_routing/bgp_speaker.py b/neutronclient/osc/v2/dynamic_routing/bgp_speaker.py new file mode 100644 index 000000000..76d8340b9 --- /dev/null +++ b/neutronclient/osc/v2/dynamic_routing/bgp_speaker.py @@ -0,0 +1,305 @@ +# 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 osc_lib.command import command +from osc_lib import utils + +from neutronclient._i18n import _ +from neutronclient.osc import utils as nc_osc_utils +from neutronclient.osc.v2.dynamic_routing import constants + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + if parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + if 'local_as' in parsed_args: + attrs['local_as'] = parsed_args.local_as + if 'ip_version' in parsed_args: + attrs['ip_version'] = parsed_args.ip_version + if parsed_args.advertise_tenant_networks: + attrs['advertise_tenant_networks'] = True + if parsed_args.no_advertise_tenant_networks: + attrs['advertise_tenant_networks'] = False + if parsed_args.advertise_floating_ip_host_routes: + attrs['advertise_floating_ip_host_routes'] = True + if parsed_args.no_advertise_floating_ip_host_routes: + attrs['advertise_floating_ip_host_routes'] = False + + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = client_manager.identity + project_id = nc_osc_utils.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + return attrs + + +def add_common_arguments(parser): + parser.add_argument( + '--advertise-floating-ip-host-routes', + action='store_true', + help=_("Enable the advertisement of floating IP host routes " + "by the BGP speaker. (default)")) + parser.add_argument( + '--no-advertise-floating-ip-host-routes', + action='store_true', + help=_("Disable the advertisement of floating IP host routes " + "by the BGP speaker.")) + parser.add_argument( + '--advertise-tenant-networks', + action='store_true', + help=_("Enable the advertisement of tenant network routes " + "by the BGP speaker. (default)")) + parser.add_argument( + '--no-advertise-tenant-networks', + action='store_true', + help=_("Disable the advertisement of tenant network routes " + "by the BGP speaker.")) + + +class AddNetworkToSpeaker(command.Command): + _description = _("Add a network to a BGP speaker") + + def get_parser(self, prog_name): + parser = super(AddNetworkToSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("BGP speaker (name or ID)")) + parser.add_argument( + 'network', + metavar='', + help=_("Network to add (name or ID)")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker(parsed_args.bgp_speaker, + ignore_missing=False).id + net_id = client.find_network(parsed_args.network, + ignore_missing=False).id + client.add_gateway_network_to_speaker(speaker_id, net_id) + + +class AddPeerToSpeaker(command.Command): + _description = _("Add a peer to a BGP speaker") + + def get_parser(self, prog_name): + parser = super(AddPeerToSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("BGP speaker (name or ID)")) + parser.add_argument( + 'bgp_peer', + metavar='', + help=_("BGP Peer to add (name or ID)")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker(parsed_args.bgp_speaker)['id'] + peer_id = client.find_bgp_peer(parsed_args.bgp_peer)['id'] + client.add_bgp_peer_to_speaker(speaker_id, peer_id) + + +class CreateBgpSpeaker(command.ShowOne): + _description = _("Create a BGP speaker") + + def get_parser(self, prog_name): + parser = super(CreateBgpSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_("Name of the BGP speaker to create")) + parser.add_argument( + '--local-as', + metavar='', + required=True, + help=_("Local AS number. (Integer in [%(min_val)s, %(max_val)s] " + "is allowed.)") % {'min_val': constants.MIN_AS_NUM, + 'max_val': constants.MAX_AS_NUM}) + parser.add_argument( + '--ip-version', + type=int, choices=[4, 6], + default=4, + help=_("IP version for the BGP speaker (default is 4)")) + add_common_arguments(parser) + nc_osc_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_bgp_speaker(**attrs) + display_columns, columns = nc_osc_utils._get_columns(obj) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteBgpSpeaker(command.Command): + _description = _("Delete a BGP speaker") + + def get_parser(self, prog_name): + parser = super(DeleteBgpSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar="", + help=_("BGP speaker to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + id = client.find_bgp_speaker(parsed_args.bgp_speaker)['id'] + client.delete_bgp_speaker(id) + + +class ListBgpSpeaker(command.Lister): + _description = _("List BGP speakers") + + def get_parser(self, prog_name): + parser = super(ListBgpSpeaker, self).get_parser(prog_name) + parser.add_argument( + '--agent', + metavar='', + help=_("List BGP speakers hosted by an agent (ID only)")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + if parsed_args.agent is not None: + data = client.get_bgp_speakers_hosted_by_dragent(parsed_args.agent) + else: + data = client.bgp_speakers(retrieve_all=True) + + headers = ('ID', 'Name', 'Local AS', 'IP Version') + columns = ('id', 'name', 'local_as', 'ip_version') + return (headers, (utils.get_dict_properties(s, columns) + for s in data)) + + +class ListRoutesAdvertisedBySpeaker(command.Lister): + _description = _("List routes advertised") + + def get_parser(self, prog_name): + parser = super(ListRoutesAdvertisedBySpeaker, + self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("BGP speaker (name or ID)")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker(parsed_args.bgp_speaker)['id'] + data = client.get_advertised_routes_of_speaker(speaker_id) + headers = ('Destination', 'Nexthop') + columns = ('destination', 'next_hop') + return (headers, (utils.get_dict_properties(s, columns) + for s in data['advertised_routes'])) + + +class RemoveNetworkFromSpeaker(command.Command): + _description = _("Remove a network from a BGP speaker") + + def get_parser(self, prog_name): + parser = super(RemoveNetworkFromSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("BGP speaker (name or ID)")) + parser.add_argument( + 'network', + metavar='', + help=_("Network to remove (name or ID)")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker(parsed_args.bgp_speaker)['id'] + net_id = client.find_network(parsed_args.network)['id'] + client.remove_gateway_network_from_speaker(speaker_id, net_id) + + +class RemovePeerFromSpeaker(command.Command): + _description = _("Remove a peer from a BGP speaker") + + def get_parser(self, prog_name): + parser = super(RemovePeerFromSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("BGP speaker (name or ID)")) + parser.add_argument( + 'bgp_peer', + metavar='', + help=_("BGP Peer to remove (name or ID)")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker(parsed_args.bgp_speaker)['id'] + peer_id = client.find_bgp_peer(parsed_args.bgp_peer)['id'] + client.remove_bgp_peer_from_speaker(speaker_id, peer_id) + + +class SetBgpSpeaker(command.Command): + _description = _("Set BGP speaker properties") + + resource = constants.BGP_SPEAKER + + def get_parser(self, prog_name): + parser = super(SetBgpSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar="", + help=_("BGP speaker to update (name or ID)") + ) + parser.add_argument( + '--name', + help=_("New name for the BGP speaker")) + add_common_arguments(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + id = client.find_bgp_speaker(parsed_args.bgp_speaker)['id'] + attrs = _get_attrs(self.app.client_manager, parsed_args) + client.update_bgp_speaker(id, **attrs) + + +class ShowBgpSpeaker(command.ShowOne): + _description = _("Show a BGP speaker") + + def get_parser(self, prog_name): + parser = super(ShowBgpSpeaker, self).get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar="", + help=_("BGP speaker to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + id = client.find_bgp_speaker(parsed_args.bgp_speaker, + ignore_missing=False).id + obj = client.get_bgp_speaker(id) + display_columns, columns = nc_osc_utils._get_columns(obj) + data = utils.get_dict_properties(obj, columns) + return display_columns, data diff --git a/quantumclient/common/__init__.py b/neutronclient/osc/v2/dynamic_routing/constants.py similarity index 78% rename from quantumclient/common/__init__.py rename to neutronclient/osc/v2/dynamic_routing/constants.py index 7e695ff08..8885b42ed 100644 --- a/quantumclient/common/__init__.py +++ b/neutronclient/osc/v2/dynamic_routing/constants.py @@ -1,7 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 Nicira Networks, 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 @@ -13,4 +9,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# @author: Somik Behera, Nicira Networks, Inc. + +BGP_SPEAKERS = 'bgp_speakers' +BGP_SPEAKER = 'bgp_speaker' +BGP_PEERS = 'bgp_peers' +BGP_PEER = 'bgp_peer' +MIN_AS_NUM = 1 +MAX_AS_NUM = 4294967295 diff --git a/neutronclient/osc/v2/fwaas/__init__.py b/neutronclient/osc/v2/fwaas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/osc/v2/fwaas/constants.py b/neutronclient/osc/v2/fwaas/constants.py new file mode 100644 index 000000000..a5a74af73 --- /dev/null +++ b/neutronclient/osc/v2/fwaas/constants.py @@ -0,0 +1,28 @@ +# Copyright 2016-2017 FUJITSU LIMITED +# 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. +# + +# TODO(slaweq): remove it once "logging" OSC plugin will be moved to the +# python-openstackclient repo + +FWG = 'firewall_group' +FWGS = 'firewall_groups' +FWP = 'firewall_policy' +FWPS = 'firewall_policies' +FWR = 'firewall_rule' +FWRS = 'firewall_rules' +CMD_FWG = 'fwaas_' + FWG +CMD_FWP = 'fwaas_' + FWP +CMD_FWR = 'fwaas_' + FWR diff --git a/neutronclient/osc/v2/lbaas/__init__.py b/neutronclient/osc/v2/lbaas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/osc/v2/logging/__init__.py b/neutronclient/osc/v2/logging/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/osc/v2/logging/network_log.py b/neutronclient/osc/v2/logging/network_log.py new file mode 100644 index 000000000..b3b8204c3 --- /dev/null +++ b/neutronclient/osc/v2/logging/network_log.py @@ -0,0 +1,304 @@ +# Copyright 2017-2018 FUJTISU LIMITED. +# 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 + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util +from oslo_log import log as logging + +from neutronclient._i18n import _ +from neutronclient.common import utils as nc_utils +from neutronclient.osc import utils as osc_utils +from neutronclient.osc.v2.fwaas import constants as fwaas_const + +LOG = logging.getLogger(__name__) + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('enabled', 'Enabled', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('target_id', 'Target', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('resource_id', 'Resource', column_util.LIST_LONG_ONLY), + ('resource_type', 'Type', column_util.LIST_BOTH), + ('event', 'Event', column_util.LIST_LONG_ONLY), + ('summary', 'Summary', column_util.LIST_SHORT_ONLY), +) + +_attr_map_for_loggable = ( + ('type', 'Supported types', column_util.LIST_BOTH), +) + +NET_LOG = 'network_log' + + +def _get_common_parser(parser): + parser.add_argument( + '--description', + metavar='', + help=_('Description of the network log')) + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--enable', + action='store_true', + help=_('Enable this log')) + enable_group.add_argument( + '--disable', + action='store_true', + help=_('Disable this log (default is enabled)')) + return parser + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + client = client_manager.neutronclient + + if is_create: + if 'project' in parsed_args and parsed_args.project is not None: + attrs['project_id'] = osc_utils.find_project( + client_manager.identity, + parsed_args.project, + parsed_args.project_domain, + ).id + resource_type = parsed_args.resource_type + attrs['resource_type'] = resource_type + if parsed_args.resource: + cmd_resource = None + if resource_type == fwaas_const.FWG: + cmd_resource = fwaas_const.CMD_FWG + attrs['resource_id'] = client.find_resource( + resource_type, + parsed_args.resource, + cmd_resource=cmd_resource)['id'] + + if parsed_args.target: + # NOTE(yushiro) Currently, we're supporting only port + attrs['target_id'] = client.find_resource( + 'port', parsed_args.target)['id'] + if parsed_args.event: + attrs['event'] = parsed_args.event + if parsed_args.enable: + attrs['enabled'] = True + if parsed_args.disable: + attrs['enabled'] = False + if parsed_args.name: + attrs['name'] = parsed_args.name + if parsed_args.description: + attrs['description'] = parsed_args.description + return attrs + + +class CreateNetworkLog(command.ShowOne): + _description = _("Create a new network log") + + def get_parser(self, prog_name): + parser = super(CreateNetworkLog, self).get_parser(prog_name) + _get_common_parser(parser) + osc_utils.add_project_owner_option_to_parser(parser) + parser.add_argument( + 'name', + metavar='', + help=_('Name for the network log')) + parser.add_argument( + '--event', + metavar='{ALL,ACCEPT,DROP}', + choices=['ALL', 'ACCEPT', 'DROP'], + type=nc_utils.convert_to_uppercase, + help=_('An event to store with log')) + # NOTE(yushiro) '--resource-type' is managed by following command: + # "openstack network loggable resources list". Therefore, this option + # shouldn't have "choices" like ['security_group', 'firewall_group'] + parser.add_argument( + '--resource-type', + metavar='', + required=True, + type=nc_utils.convert_to_lowercase, + help=_('Network log type(s). ' + 'You can see supported type(s) with following command:\n' + '$ openstack network loggable resources list')) + parser.add_argument( + '--resource', + metavar='', + help=_('Name or ID of resource (security group or firewall group) ' + 'that used for logging. You can control for logging target ' + 'combination with --target option.')) + parser.add_argument( + '--target', + metavar='', + help=_('Port (name or ID) for logging. You can control ' + 'for logging target combination with --resource option.')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.neutronclient + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + obj = client.create_network_log({'log': attrs})['log'] + columns, display_columns = column_util.get_columns(obj, _attr_map) + data = utils.get_dict_properties(obj, columns) + return (display_columns, data) + + +class DeleteNetworkLog(command.Command): + _description = _("Delete network log(s)") + + def get_parser(self, prog_name): + parser = super(DeleteNetworkLog, self).get_parser(prog_name) + parser.add_argument( + 'network_log', + metavar='', + nargs='+', + help=_('Network log(s) to delete (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.neutronclient + result = 0 + for log_res in parsed_args.network_log: + try: + log_id = client.find_resource( + 'log', log_res, cmd_resource=NET_LOG)['id'] + client.delete_network_log(log_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete network log with " + "name or ID '%(network_log)s': %(e)s"), + {'network_log': log_res, 'e': e}) + + if result > 0: + total = len(parsed_args.network_log) + msg = (_("%(result)s of %(total)s network log(s) " + "failed to delete") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListLoggableResource(command.Lister): + _description = _("List supported loggable resources") + + def get_parser(self, prog_name): + parser = super(ListLoggableResource, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.neutronclient + obj = client.list_network_loggable_resources()['loggable_resources'] + headers, columns = column_util.get_column_definitions( + _attr_map_for_loggable, long_listing=parsed_args.long) + return (headers, (utils.get_dict_properties(s, columns) for s in obj)) + + +class ListNetworkLog(command.Lister): + _description = _("List network logs") + + def get_parser(self, prog_name): + parser = super(ListNetworkLog, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output") + ) + # TODO(yushiro): We'll support filtering in the future. + return parser + + def _extend_list(self, data, parsed_args): + ext_data = copy.deepcopy(data) + for d in ext_data: + e_prefix = 'Event: ' + if d['event']: + event = e_prefix + d['event'].upper() + port = '(port) ' + d['target_id'] if d['target_id'] else '' + resource_type = d['resource_type'] + if d['resource_id']: + res = '(%s) %s' % (resource_type, d['resource_id']) + else: + res = '' + t_prefix = 'Logged: ' + if port and res: + t = '%s on %s' % (res, port) + else: + # Either of res and port is empty, so concatenation works fine + t = res + port + target = t_prefix + t if t else t_prefix + '(None specified)' + d['summary'] = ',\n'.join([event, target]) + return ext_data + + def take_action(self, parsed_args): + client = self.app.client_manager.neutronclient + obj = client.list_network_logs()['logs'] + obj_extend = self._extend_list(obj, parsed_args) + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, ( + utils.get_dict_properties(s, columns) for s in obj_extend)) + + +class SetNetworkLog(command.Command): + _description = _("Set network log properties") + + def get_parser(self, prog_name): + parser = super(SetNetworkLog, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'network_log', + metavar='', + help=_('Network log to set (name or ID)')) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the network log')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.neutronclient + log_id = client.find_resource( + 'log', parsed_args.network_log, cmd_resource=NET_LOG)['id'] + attrs = _get_common_attrs(self.app.client_manager, parsed_args, + is_create=False) + try: + client.update_network_log(log_id, {'log': attrs}) + except Exception as e: + msg = (_("Failed to set network log '%(logging)s': %(e)s") + % {'logging': parsed_args.network_log, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowNetworkLog(command.ShowOne): + _description = _("Display network log details") + + def get_parser(self, prog_name): + parser = super(ShowNetworkLog, self).get_parser(prog_name) + parser.add_argument( + 'network_log', + metavar='', + help=_('Network log to show (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.neutronclient + log_id = client.find_resource( + 'log', parsed_args.network_log, cmd_resource=NET_LOG)['id'] + obj = client.show_network_log(log_id)['log'] + columns, display_columns = column_util.get_columns(obj, _attr_map) + data = utils.get_dict_properties(obj, columns) + return (display_columns, data) diff --git a/neutronclient/osc/v2/sfc/__init__.py b/neutronclient/osc/v2/sfc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/osc/v2/sfc/sfc_flow_classifier.py b/neutronclient/osc/v2/sfc/sfc_flow_classifier.py new file mode 100755 index 000000000..775730fc2 --- /dev/null +++ b/neutronclient/osc/v2/sfc/sfc_flow_classifier.py @@ -0,0 +1,346 @@ +# Copyright (c) 2017 Huawei Technologies India Pvt.Limited. +# 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 + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from neutronclient._i18n import _ +from neutronclient.common import exceptions as nc_exc + +LOG = logging.getLogger(__name__) + +resource = 'flow_classifier' + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('summary', 'Summary', column_util.LIST_SHORT_ONLY), + ('protocol', 'Protocol', column_util.LIST_LONG_ONLY), + ('ethertype', 'Ethertype', column_util.LIST_LONG_ONLY), + ('source_ip_prefix', 'Source IP', + column_util.LIST_LONG_ONLY), + ('destination_ip_prefix', 'Destination IP', + column_util.LIST_LONG_ONLY), + ('logical_source_port', 'Logical Source Port', + column_util.LIST_LONG_ONLY), + ('logical_destination_port', 'Logical Destination Port', + column_util.LIST_LONG_ONLY), + ('source_port_range_min', 'Source Port Range Min', + column_util.LIST_LONG_ONLY), + ('source_port_range_max', 'Source Port Range Max', + column_util.LIST_LONG_ONLY), + ('destination_port_range_min', 'Destination Port Range Min', + column_util.LIST_LONG_ONLY), + ('destination_port_range_max', 'Destination Port Range Max', + column_util.LIST_LONG_ONLY), + ('l7_parameters', 'L7 Parameters', column_util.LIST_LONG_ONLY), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'description': 'Description', + 'summary': 'Summary', + 'protocol': 'Protocol', + 'ethertype': 'Ethertype', + 'source_ip_prefix': 'Source IP', + 'destination_ip_prefix': 'Destination IP', + 'logical_source_port': 'Logical Source Port', + 'logical_destination_port': 'Logical Destination Port', + 'source_port_range_min': 'Source Port Range Min', + 'source_port_range_max': 'Source Port Range Max', + 'destination_port_range_min': 'Destination Port Range Min', + 'destination_port_range_max': 'Destination Port Range Max', + 'l7_parameters': 'L7 Parameters', + 'tenant_id': 'Project', + 'project_id': 'Project', +} + + +class CreateSfcFlowClassifier(command.ShowOne): + _description = _("Create a flow classifier") + + def get_parser(self, prog_name): + parser = super(CreateSfcFlowClassifier, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the flow classifier')) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the flow classifier')) + parser.add_argument( + '--protocol', + metavar='', + help=_('IP protocol name. Protocol name should be as per ' + 'IANA standard.')) + parser.add_argument( + '--ethertype', + metavar='{IPv4,IPv6}', + default='IPv4', choices=['IPv4', 'IPv6'], + help=_('L2 ethertype, default is IPv4')) + parser.add_argument( + '--source-port', + metavar=':', + help=_('Source protocol port (allowed range [1,65535]. Must be ' + 'specified as a:b, where a=min-port and b=max-port) ' + 'in the allowed range.')) + parser.add_argument( + '--destination-port', + metavar=':', + help=_('Destination protocol port (allowed range [1,65535]. Must ' + 'be specified as a:b, where a=min-port and b=max-port) ' + 'in the allowed range.')) + parser.add_argument( + '--source-ip-prefix', + metavar='', + help=_('Source IP address in CIDR notation')) + parser.add_argument( + '--destination-ip-prefix', + metavar='', + help=_('Destination IP address in CIDR notation')) + parser.add_argument( + '--logical-source-port', + metavar='', + help=_('Neutron source port (name or ID)')) + parser.add_argument( + '--logical-destination-port', + metavar='', + help=_('Neutron destination port (name or ID)')) + parser.add_argument( + '--l7-parameters', + help=_('Dictionary of L7 parameters. Currently, no value is ' + 'supported for this option.')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + obj = client.create_sfc_flow_classifier(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id', 'summary']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteSfcFlowClassifier(command.Command): + _description = _("Delete a given flow classifier") + + def get_parser(self, prog_name): + parser = super(DeleteSfcFlowClassifier, self).get_parser(prog_name) + parser.add_argument( + 'flow_classifier', + metavar='', + nargs='+', + help=_("Flow classifier(s) to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + for fcl in parsed_args.flow_classifier: + try: + fc_id = client.find_sfc_flow_classifier( + fcl, ignore_missing=False)['id'] + client.delete_sfc_flow_classifier(fc_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete flow classifier with name " + "or ID '%(fc)s': %(e)s"), {'fc': fcl, 'e': e}) + if result > 0: + total = len(parsed_args.flow_classifier) + msg = (_("%(result)s of %(total)s flow classifier(s) " + "failed to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListSfcFlowClassifier(command.Lister): + _description = _("List flow classifiers") + + def get_parser(self, prog_name): + parser = super(ListSfcFlowClassifier, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output") + ) + return parser + + def extend_list(self, data, parsed_args): + ext_data = [] + for d in data: + val = [] + protocol = d['protocol'].upper() if d['protocol'] else 'any' + val.append('protocol: ' + protocol) + val.append(self._get_protocol_port_details(d, 'source')) + val.append(self._get_protocol_port_details(d, 'destination')) + if 'logical_source_port' in d: + val.append('neutron_source_port: ' + + str(d['logical_source_port'])) + + if 'logical_destination_port' in d: + val.append('neutron_destination_port: ' + + str(d['logical_destination_port'])) + + if 'l7_parameters' in d: + l7_param = 'l7_parameters: {%s}' % ','.join(d['l7_parameters']) + val.append(l7_param) + d['summary'] = ',\n'.join(val) + ext_data.append(d) + return ext_data + + def _get_protocol_port_details(self, data, val): + type_ip_prefix = val + '_ip_prefix' + ip_prefix = data.get(type_ip_prefix) + if not ip_prefix: + ip_prefix = 'any' + min_port = data.get(val + '_port_range_min') + if min_port is None: + min_port = 'any' + max_port = data.get(val + '_port_range_max') + if max_port is None: + max_port = 'any' + return '%s[port]: %s[%s:%s]' % ( + val, ip_prefix, min_port, max_port) + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.sfc_flow_classifiers() + obj_extend = self.extend_list(obj, parsed_args) + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, (utils.get_dict_properties( + s, columns) for s in obj_extend)) + + +class SetSfcFlowClassifier(command.Command): + _description = _("Set flow classifier properties") + + def get_parser(self, prog_name): + parser = super(SetSfcFlowClassifier, self).get_parser(prog_name) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the flow classifier')) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the flow classifier')) + parser.add_argument( + 'flow_classifier', + metavar='', + help=_("Flow classifier to modify (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + fc_id = client.find_sfc_flow_classifier(parsed_args.flow_classifier, + ignore_missing=False)['id'] + attrs = _get_common_attrs(self.app.client_manager, parsed_args, + is_create=False) + try: + client.update_sfc_flow_classifier(fc_id, **attrs) + except Exception as e: + msg = (_("Failed to update flow classifier '%(fc)s': %(e)s") + % {'fc': parsed_args.flow_classifier, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowSfcFlowClassifier(command.ShowOne): + _description = _("Display flow classifier details") + + def get_parser(self, prog_name): + parser = super(ShowSfcFlowClassifier, self).get_parser(prog_name) + parser.add_argument( + 'flow_classifier', + metavar='', + help=_("Flow classifier to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + fc_id = client.find_sfc_flow_classifier(parsed_args.flow_classifier, + ignore_missing=False)['id'] + obj = client.get_sfc_flow_classifier(fc_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id', 'summary']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if is_create: + _get_attrs(client_manager, attrs, parsed_args) + return attrs + + +def _get_attrs(client_manager, attrs, parsed_args): + if parsed_args.protocol is not None: + attrs['protocol'] = parsed_args.protocol + if parsed_args.ethertype: + attrs['ethertype'] = parsed_args.ethertype + if parsed_args.source_ip_prefix is not None: + attrs['source_ip_prefix'] = parsed_args.source_ip_prefix + if parsed_args.destination_ip_prefix is not None: + attrs['destination_ip_prefix'] = parsed_args.destination_ip_prefix + if parsed_args.logical_source_port is not None: + attrs['logical_source_port'] = client_manager.network.find_port( + parsed_args.logical_source_port, ignore_missing=False + )['id'] + if parsed_args.logical_destination_port is not None: + attrs['logical_destination_port'] = client_manager.network.find_port( + parsed_args.logical_destination_port, ignore_missing=False + )['id'] + if parsed_args.source_port is not None: + _fill_protocol_port_info(attrs, 'source', + parsed_args.source_port) + if parsed_args.destination_port is not None: + _fill_protocol_port_info(attrs, 'destination', + parsed_args.destination_port) + if parsed_args.l7_parameters is not None: + attrs['l7_parameters'] = parsed_args.l7_parameters + + +def _fill_protocol_port_info(attrs, port_type, port_val): + min_port, sep, max_port = port_val.partition(":") + if not min_port: + msg = ("Invalid port value '%s', expected format is " + "min-port:max-port or min-port.") + raise argparse.ArgumentTypeError(msg % port_val) + if not max_port: + max_port = min_port + try: + attrs[port_type + '_port_range_min'] = int(min_port) + attrs[port_type + '_port_range_max'] = int(max_port) + except ValueError: + message = (_("Protocol port value %s must be an integer " + "or integer:integer.") % port_val) + raise nc_exc.CommandError(message=message) diff --git a/neutronclient/osc/v2/sfc/sfc_port_chain.py b/neutronclient/osc/v2/sfc/sfc_port_chain.py new file mode 100755 index 000000000..d96007b85 --- /dev/null +++ b/neutronclient/osc/v2/sfc/sfc_port_chain.py @@ -0,0 +1,381 @@ +# Copyright (c) 2017 Huawei Technologies India Pvt.Limited. +# 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 osc_lib.cli import parseractions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from neutronclient._i18n import _ + +LOG = logging.getLogger(__name__) + +resource = 'port_chain' + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('port_pair_groups', 'Port Pair Groups', column_util.LIST_BOTH), + ('flow_classifiers', 'Flow Classifiers', + column_util.LIST_BOTH), + ('chain_parameters', 'Chain Parameters', + column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'port_pair_groups': 'Port Pair Groups', + 'flow_classifiers': 'Flow Classifiers', + 'chain_parameters': 'Chain Parameters', + 'description': 'Description', + 'tenant_id': 'Project', + 'project_id': 'Project', +} + + +class CreateSfcPortChain(command.ShowOne): + _description = _("Create a port chain") + + def get_parser(self, prog_name): + parser = super(CreateSfcPortChain, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the port chain')) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the port chain')) + parser.add_argument( + '--flow-classifier', + default=[], + metavar='', + dest='flow_classifiers', + action='append', + help=_('Add flow classifier (name or ID). ' + 'This option can be repeated.')) + parser.add_argument( + '--chain-parameters', + metavar='correlation=,symmetric=', + action=parseractions.MultiKeyValueAction, + optional_keys=['correlation', 'symmetric'], + help=_('Dictionary of chain parameters. Supports ' + 'correlation=(mpls|nsh) (default is mpls) ' + 'and symmetric=(true|false).')) + parser.add_argument( + '--port-pair-group', + metavar='', + dest='port_pair_groups', + required=True, + action='append', + help=_('Add port pair group (name or ID). ' + 'This option can be repeated.')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + obj = client.create_sfc_port_chain(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteSfcPortChain(command.Command): + _description = _("Delete a given port chain") + + def get_parser(self, prog_name): + parser = super(DeleteSfcPortChain, self).get_parser(prog_name) + parser.add_argument( + 'port_chain', + metavar="", + nargs='+', + help=_("Port chain(s) to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + for pc in parsed_args.port_chain: + try: + pc_id = client.find_sfc_port_chain( + pc, ignore_missing=False)['id'] + client.delete_sfc_port_chain(pc_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete port chain with name " + "or ID '%(pc)s': %(e)s"), {'pc': pc, 'e': e}) + if result > 0: + total = len(parsed_args.port_chain) + msg = (_("%(result)s of %(total)s port chain(s) " + "failed to delete.") % {'result': result, + 'total': total}) + raise exceptions.CommandError(msg) + + +class ListSfcPortChain(command.Lister): + _description = _("List port chains") + + def get_parser(self, prog_name): + parser = super(ListSfcPortChain, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + data = client.sfc_port_chains() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, + (utils.get_dict_properties(s, columns) + for s in data)) + + +class SetSfcPortChain(command.Command): + _description = _("Set port chain properties") + + def get_parser(self, prog_name): + parser = super(SetSfcPortChain, self).get_parser(prog_name) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the port chain')) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the port chain')) + parser.add_argument( + '--flow-classifier', + metavar='', + dest='flow_classifiers', + action='append', + help=_('Add flow classifier (name or ID). ' + 'This option can be repeated.')) + parser.add_argument( + '--no-flow-classifier', + action='store_true', + help=_('Remove associated flow classifiers from the port chain')) + parser.add_argument( + '--port-pair-group', + metavar='', + dest='port_pair_groups', + action='append', + help=_('Add port pair group (name or ID). ' + 'Current port pair groups order is kept, the added port ' + 'pair group will be placed at the end of the port chain. ' + 'This option can be repeated.')) + parser.add_argument( + '--no-port-pair-group', + action='store_true', + help=_('Remove associated port pair groups from the port chain. ' + 'At least one --port-pair-group must be specified ' + 'together.')) + parser.add_argument( + 'port_chain', + metavar='', + help=_("Port chain to modify (name or ID)")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + pc_id = client.find_sfc_port_chain(parsed_args.port_chain, + ignore_missing=False)['id'] + attrs = _get_common_attrs(self.app.client_manager, parsed_args, + is_create=False) + if parsed_args.no_flow_classifier: + attrs['flow_classifiers'] = [] + if parsed_args.flow_classifiers: + if parsed_args.no_flow_classifier: + fc_list = [] + else: + fc_list = client.find_sfc_port_chain( + parsed_args.port_chain, + ignore_missing=False + )['flow_classifiers'] + for fc in parsed_args.flow_classifiers: + fc_id = client.find_sfc_flow_classifier( + fc, + ignore_missing=False)['id'] + if fc_id not in fc_list: + fc_list.append(fc_id) + attrs['flow_classifiers'] = fc_list + if (parsed_args.no_port_pair_group and not + parsed_args.port_pair_groups): + message = _('At least one --port-pair-group must be specified.') + raise exceptions.CommandError(message) + if parsed_args.no_port_pair_group and parsed_args.port_pair_groups: + ppg_list = [] + for ppg in parsed_args.port_pair_groups: + ppg_id = client.find_sfc_port_pair_group( + ppg, ignore_missing=False)['id'] + if ppg_id not in ppg_list: + ppg_list.append(ppg_id) + attrs['port_pair_groups'] = ppg_list + if (parsed_args.port_pair_groups and + not parsed_args.no_port_pair_group): + ppg_list = client.find_sfc_port_chain( + parsed_args.port_chain, + ignore_missing=False + )['port_pair_groups'] + for ppg in parsed_args.port_pair_groups: + ppg_id = client.find_sfc_port_pair_group( + ppg, ignore_missing=False)['id'] + if ppg_id not in ppg_list: + ppg_list.append(ppg_id) + attrs['port_pair_groups'] = ppg_list + try: + client.update_sfc_port_chain(pc_id, **attrs) + except Exception as e: + msg = (_("Failed to update port chain '%(pc)s': %(e)s") + % {'pc': parsed_args.port_chain, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowSfcPortChain(command.ShowOne): + _description = _("Display port chain details") + + def get_parser(self, prog_name): + parser = super(ShowSfcPortChain, self).get_parser(prog_name) + parser.add_argument( + 'port_chain', + metavar="", + help=_("Port chain to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + pc_id = client.find_sfc_port_chain(parsed_args.port_chain, + ignore_missing=False)['id'] + obj = client.get_sfc_port_chain(pc_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class UnsetSfcPortChain(command.Command): + _description = _("Unset port chain properties") + + def get_parser(self, prog_name): + parser = super(UnsetSfcPortChain, self).get_parser(prog_name) + parser.add_argument( + 'port_chain', + metavar='', + help=_("Port chain to unset (name or ID)")) + port_chain = parser.add_mutually_exclusive_group() + port_chain.add_argument( + '--flow-classifier', + action='append', + metavar='', + dest='flow_classifiers', + help=_('Remove flow classifier(s) from the port chain ' + '(name or ID). This option can be repeated.')) + port_chain.add_argument( + '--all-flow-classifier', + action='store_true', + help=_('Remove all flow classifiers from the port chain')) + parser.add_argument( + '--port-pair-group', + metavar='', + dest='port_pair_groups', + action='append', + help=_('Remove port pair group(s) from the port chain ' + '(name or ID). This option can be repeated.')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + pc_id = client.find_sfc_port_chain(parsed_args.port_chain, + ignore_missing=False)['id'] + attrs = {} + if parsed_args.flow_classifiers: + fc_list = client.find_sfc_port_chain( + parsed_args.port_chain, ignore_missing=False + )['flow_classifiers'] + for fc in parsed_args.flow_classifiers: + fc_id = client.find_sfc_flow_classifier( + fc, + ignore_missing=False)['id'] + if fc_id in fc_list: + fc_list.remove(fc_id) + attrs['flow_classifiers'] = fc_list + if parsed_args.all_flow_classifier: + attrs['flow_classifiers'] = [] + if parsed_args.port_pair_groups: + ppg_list = client.find_sfc_port_chain( + parsed_args.port_chain, + ignore_missing=False)['port_pair_groups'] + for ppg in parsed_args.port_pair_groups: + ppg_id = client.find_sfc_port_pair_group( + ppg, + ignore_missing=False)['id'] + if ppg_id in ppg_list: + ppg_list.remove(ppg_id) + if ppg_list == []: + message = _('At least one port pair group must be' + ' specified.') + raise exceptions.CommandError(message) + attrs['port_pair_groups'] = ppg_list + try: + client.update_sfc_port_chain(pc_id, **attrs) + except Exception as e: + msg = (_("Failed to unset port chain '%(pc)s': %(e)s") + % {'pc': parsed_args.port_chain, 'e': e}) + raise exceptions.CommandError(msg) + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + client = client_manager.network + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if parsed_args.port_pair_groups: + attrs['port_pair_groups'] = [client.find_sfc_port_pair_group( + ppg, ignore_missing=False)['id'] + for ppg in parsed_args.port_pair_groups] + if parsed_args.flow_classifiers: + attrs['flow_classifiers'] = [client.find_sfc_flow_classifier( + fc, ignore_missing=False)['id'] + for fc in parsed_args.flow_classifiers] + if is_create is True: + _get_attrs(attrs, parsed_args) + return attrs + + +def _get_attrs(attrs, parsed_args): + if parsed_args.chain_parameters is not None: + chain_params = {} + for chain_param in parsed_args.chain_parameters: + if 'correlation' in chain_param: + chain_params['correlation'] = chain_param['correlation'] + if 'symmetric' in chain_param: + chain_params['symmetric'] = chain_param['symmetric'] + attrs['chain_parameters'] = chain_params diff --git a/neutronclient/osc/v2/sfc/sfc_port_pair.py b/neutronclient/osc/v2/sfc/sfc_port_pair.py new file mode 100755 index 000000000..11e8e7631 --- /dev/null +++ b/neutronclient/osc/v2/sfc/sfc_port_pair.py @@ -0,0 +1,251 @@ +# Copyright (c) 2017 Huawei Technologies India Pvt.Limited. +# 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 osc_lib.cli import parseractions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from neutronclient._i18n import _ + +LOG = logging.getLogger(__name__) + +resource = 'port_pair' + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('ingress', 'Ingress Logical Port', column_util.LIST_BOTH), + ('egress', 'Egress Logical Port', column_util.LIST_BOTH), + ('service_function_parameters', 'Service Function Parameters', + column_util.LIST_LONG_ONLY), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'description': 'Description', + 'ingress': 'Ingress Logical Port', + 'egress': 'Egress Logical Port', + 'service_function_parameters': 'Service Function Parameters', + 'tenant_id': 'Project', + 'project_id': 'Project', +} + + +class CreateSfcPortPair(command.ShowOne): + _description = _("Create a port pair") + + def get_parser(self, prog_name): + parser = super(CreateSfcPortPair, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the port pair')) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the port pair')) + parser.add_argument( + '--service-function-parameters', + metavar='correlation=,weight=', + action=parseractions.MultiKeyValueAction, + optional_keys=['correlation', 'weight'], + help=_('Dictionary of service function parameters. ' + 'Currently, correlation=(None|mpls|nsh) and weight ' + 'are supported. Weight is an integer that influences ' + 'the selection of a port pair within a port pair group ' + 'for a flow. The higher the weight, the more flows will ' + 'hash to the port pair. The default weight is 1.')) + parser.add_argument( + '--ingress', + metavar='', + required=True, + help=_('Ingress neutron port (name or ID)')) + parser.add_argument( + '--egress', + metavar='', + required=True, + help=_('Egress neutron port (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + obj = client.create_sfc_port_pair(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteSfcPortPair(command.Command): + _description = _("Delete a given port pair") + + def get_parser(self, prog_name): + parser = super(DeleteSfcPortPair, self).get_parser(prog_name) + parser.add_argument( + 'port_pair', + metavar="", + nargs='+', + help=_("Port pair(s) to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + for pp in parsed_args.port_pair: + try: + port_pair_id = client.find_sfc_port_pair( + pp, ignore_missing=False)['id'] + client.delete_sfc_port_pair(port_pair_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete port pair with name " + "or ID '%(port_pair)s': %(e)s"), + {'port_pair': pp, 'e': e}) + if result > 0: + total = len(parsed_args.port_pair) + msg = (_("%(result)s of %(total)s port pair(s) " + "failed to delete.") % {'result': result, + 'total': total}) + raise exceptions.CommandError(msg) + + +class ListSfcPortPair(command.Lister): + _description = _("List port pairs") + + def get_parser(self, prog_name): + parser = super(ListSfcPortPair, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + data = client.sfc_port_pairs() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, + (utils.get_dict_properties( + s, columns, + ) for s in data)) + + +class SetSfcPortPair(command.Command): + _description = _("Set port pair properties") + + def get_parser(self, prog_name): + parser = super(SetSfcPortPair, self).get_parser(prog_name) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the port pair')) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the port pair')) + parser.add_argument( + 'port_pair', + metavar='', + help=_("Port pair to modify (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + port_pair_id = client.find_sfc_port_pair( + parsed_args.port_pair, ignore_missing=False + )['id'] + attrs = _get_common_attrs(self.app.client_manager, parsed_args, + is_create=False) + try: + client.update_sfc_port_pair(port_pair_id, **attrs) + except Exception as e: + msg = (_("Failed to update port pair '%(port_pair)s': %(e)s") + % {'port_pair': parsed_args.port_pair, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowSfcPortPair(command.ShowOne): + _description = _("Display port pair details") + + def get_parser(self, prog_name): + parser = super(ShowSfcPortPair, self).get_parser(prog_name) + parser.add_argument( + 'port_pair', + metavar='', + help=_("Port pair to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + port_pair_id = client.find_sfc_port_pair( + parsed_args.port_pair, ignore_missing=False + )['id'] + obj = client.get_sfc_port_pair(port_pair_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if is_create: + _get_attrs(client_manager, attrs, parsed_args) + return attrs + + +def _get_attrs(client_manager, attrs, parsed_args): + client = client_manager.network + if parsed_args.ingress is not None: + attrs['ingress'] = client.find_port( + parsed_args.ingress, ignore_missing=False + )['id'] + if parsed_args.egress is not None: + attrs['egress'] = client.find_port( + parsed_args.egress, ignore_missing=False + )['id'] + if parsed_args.service_function_parameters is not None: + attrs['service_function_parameters'] = _get_service_function_params( + parsed_args.service_function_parameters) + + +def _get_service_function_params(sf_params): + attrs = {} + for sf_param in sf_params: + if 'correlation' in sf_param: + if sf_param['correlation'] == 'None': + attrs['correlation'] = None + else: + attrs['correlation'] = sf_param['correlation'] + if 'weight' in sf_param: + attrs['weight'] = sf_param['weight'] + return attrs diff --git a/neutronclient/osc/v2/sfc/sfc_port_pair_group.py b/neutronclient/osc/v2/sfc/sfc_port_pair_group.py new file mode 100755 index 000000000..ec8859b42 --- /dev/null +++ b/neutronclient/osc/v2/sfc/sfc_port_pair_group.py @@ -0,0 +1,326 @@ +# Copyright (c) 2017 Huawei Technologies India Pvt.Limited. +# 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 osc_lib.cli import parseractions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from neutronclient._i18n import _ + +LOG = logging.getLogger(__name__) + +resource = 'port_pair_group' + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('port_pairs', 'Port Pair', column_util.LIST_BOTH), + ('port_pair_group_parameters', 'Port Pair Group Parameters', + column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('is_tap_enabled', 'Tap Enabled', column_util.LIST_BOTH) +) + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'description': 'Description', + 'port_pairs': 'Port Pair', + 'port_pair_group_parameters': 'Port Pair Group Parameters', + 'is_tap_enabled': 'Tap Enabled', + 'tenant_id': 'Project', + 'project_id': 'Project', +} + + +class CreateSfcPortPairGroup(command.ShowOne): + _description = _("Create a port pair group") + + def get_parser(self, prog_name): + parser = super(CreateSfcPortPairGroup, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the port pair group')) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the port pair group')) + parser.add_argument( + '--port-pair', + metavar='', + dest='port_pairs', + default=[], + action='append', + help=_('Port pair (name or ID). ' + 'This option can be repeated.')) + tap_enable = parser.add_mutually_exclusive_group() + tap_enable.add_argument( + '--enable-tap', + action='store_true', + help=_('Port pairs of this port pair group are deployed as ' + 'passive tap service function') + ) + tap_enable.add_argument( + '--disable-tap', + action='store_true', + help=_('Port pairs of this port pair group are deployed as l3 ' + 'service function (default)') + ) + parser.add_argument( + '--port-pair-group-parameters', + metavar='lb-fields=', + action=parseractions.KeyValueAction, + help=_('Dictionary of port pair group parameters. ' + 'Currently only one parameter lb-fields is supported. ' + ' is a & separated list of load-balancing ' + 'fields.')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + obj = client.create_sfc_port_pair_group(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteSfcPortPairGroup(command.Command): + _description = _("Delete a given port pair group") + + def get_parser(self, prog_name): + parser = super(DeleteSfcPortPairGroup, self).get_parser(prog_name) + parser.add_argument( + 'port_pair_group', + metavar='', + nargs='+', + help=_("Port pair group(s) to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + for ppg in parsed_args.port_pair_group: + try: + ppg_id = client.find_sfc_port_pair_group( + ppg, ignore_missing=False)['id'] + client.delete_sfc_port_pair_group(ppg_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete port pair group with name " + "or ID '%(ppg)s': %(e)s"), {'ppg': ppg, 'e': e}) + if result > 0: + total = len(parsed_args.port_pair_group) + msg = (_("%(result)s of %(total)s port pair group(s) " + "failed to delete.") % {'result': result, + 'total': total}) + raise exceptions.CommandError(msg) + + +class ListSfcPortPairGroup(command.Lister): + _description = _("List port pair group") + + def get_parser(self, prog_name): + parser = super(ListSfcPortPairGroup, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + data = client.sfc_port_pair_groups() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, + (utils.get_dict_properties( + s, columns, + ) for s in data)) + + +class SetSfcPortPairGroup(command.Command): + _description = _("Set port pair group properties") + + def get_parser(self, prog_name): + parser = super(SetSfcPortPairGroup, self).get_parser(prog_name) + parser.add_argument( + 'port_pair_group', + metavar='', + help=_("Port pair group to modify (name or ID)")) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the port pair group')) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the port pair group')) + parser.add_argument( + '--port-pair', + metavar='', + dest='port_pairs', + default=[], + action='append', + help=_('Port pair (name or ID). ' + 'This option can be repeated.')) + parser.add_argument( + '--no-port-pair', + action='store_true', + help=_('Remove all port pair from port pair group')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + ppg_id = client.find_sfc_port_pair_group( + parsed_args.port_pair_group)['id'] + attrs = _get_common_attrs(self.app.client_manager, parsed_args, + is_create=False) + if parsed_args.no_port_pair: + attrs['port_pairs'] = [] + if parsed_args.port_pairs: + added = [client.find_sfc_port_pair(pp, + ignore_missing=False)['id'] + for pp in parsed_args.port_pairs] + if parsed_args.no_port_pair: + existing = [] + else: + existing = client.find_sfc_port_pair_group( + parsed_args.port_pair_group, + ignore_missing=False)['port_pairs'] + attrs['port_pairs'] = sorted(list(set(existing) | set(added))) + try: + client.update_sfc_port_pair_group(ppg_id, **attrs) + except Exception as e: + msg = (_("Failed to update port pair group '%(ppg)s': %(e)s") + % {'ppg': parsed_args.port_pair_group, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowSfcPortPairGroup(command.ShowOne): + _description = _("Display port pair group details") + + def get_parser(self, prog_name): + parser = super(ShowSfcPortPairGroup, self).get_parser(prog_name) + parser.add_argument( + 'port_pair_group', + metavar='', + help=_("Port pair group to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + ppg_id = client.find_sfc_port_pair_group( + parsed_args.port_pair_group, ignore_missing=False)['id'] + obj = client.get_sfc_port_pair_group(ppg_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class UnsetSfcPortPairGroup(command.Command): + _description = _("Unset port pairs from port pair group") + + def get_parser(self, prog_name): + parser = super(UnsetSfcPortPairGroup, self).get_parser(prog_name) + parser.add_argument( + 'port_pair_group', + metavar='', + help=_("Port pair group to unset (name or ID)")) + port_pair_group = parser.add_mutually_exclusive_group() + port_pair_group.add_argument( + '--port-pair', + action='append', + metavar='', + dest='port_pairs', + help=_('Remove port pair(s) from the port pair group ' + '(name or ID). This option can be repeated.')) + port_pair_group.add_argument( + '--all-port-pair', + action='store_true', + help=_('Remove all port pairs from the port pair group')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + ppg_id = client.find_sfc_port_pair_group( + parsed_args.port_pair_group, ignore_missing=False)['id'] + attrs = {} + if parsed_args.port_pairs: + existing = client.find_sfc_port_pair_group( + parsed_args.port_pair_group, + ignore_missing=False)['port_pairs'] + removed = [client.find_sfc_port_pair(pp, + ignore_missing=False)['id'] + for pp in parsed_args.port_pairs] + attrs['port_pairs'] = list(set(existing) - set(removed)) + if parsed_args.all_port_pair: + attrs['port_pairs'] = [] + try: + client.update_sfc_port_pair_group(ppg_id, **attrs) + except Exception as e: + msg = (_("Failed to unset port pair group '%(ppg)s': %(e)s") + % {'ppg': parsed_args.port_pair_group, 'e': e}) + raise exceptions.CommandError(msg) + + +def _get_ppg_param(attrs, ppg): + attrs['port_pair_group_parameters'] = {} + for key, value in ppg.items(): + if key == 'lb-fields': + attrs['port_pair_group_parameters']['lb_fields'] = ([ + field for field in value.split('&') if field]) + else: + attrs['port_pair_group_parameters'][key] = value + return attrs['port_pair_group_parameters'] + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + client = client_manager.network + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if parsed_args.port_pairs: + attrs['port_pairs'] = [client.find_sfc_port_pair( + pp, ignore_missing=False)['id'] + for pp in parsed_args.port_pairs] + + if is_create: + _get_attrs(attrs, parsed_args) + return attrs + + +def _get_attrs(attrs, parsed_args): + if parsed_args.port_pair_group_parameters is not None: + attrs['port_pair_group_parameters'] = ( + _get_ppg_param(attrs, parsed_args.port_pair_group_parameters)) + if parsed_args.enable_tap: + attrs['tap_enabled'] = True + if parsed_args.disable_tap: + attrs['tap_enabled'] = False diff --git a/neutronclient/osc/v2/sfc/sfc_service_graph.py b/neutronclient/osc/v2/sfc/sfc_service_graph.py new file mode 100644 index 000000000..2edf2ccd0 --- /dev/null +++ b/neutronclient/osc/v2/sfc/sfc_service_graph.py @@ -0,0 +1,272 @@ +# Copyright 2017 Intel Corporation. +# +# 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 osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from neutronclient._i18n import _ + +LOG = logging.getLogger(__name__) + +resource = 'service_graph' + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('port_chains', 'Branching Points', column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'description': 'Description', + 'port_chains': 'Branching Points', + 'tenant_id': 'Project', + 'project_id': 'Project', +} + + +class CreateSfcServiceGraph(command.ShowOne): + """Create a service graph.""" + def get_parser(self, prog_name): + parser = super(CreateSfcServiceGraph, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the service graph.')) + parser.add_argument( + '--description', + help=_('Description for the service graph.')) + parser.add_argument( + '--branching-point', + metavar='SRC_CHAIN:DST_CHAIN_1,DST_CHAIN_2,DST_CHAIN_N', + dest='branching_points', + action='append', + default=[], required=True, + help=_('Service graph branching point: the key is the source ' + 'Port Chain while the value is a list of destination ' + 'Port Chains. This option can be repeated.')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + try: + obj = client.create_sfc_service_graph(**attrs) + display_columns, columns = \ + utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + except Exception as e: + msg = (_("Failed to create service graph using '%(pcs)s': %(e)s") + % {'pcs': parsed_args.branching_points, 'e': e}) + raise exceptions.CommandError(msg) + + +class SetSfcServiceGraph(command.Command): + _description = _("Set service graph properties") + + def get_parser(self, prog_name): + parser = super(SetSfcServiceGraph, self).get_parser(prog_name) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the service graph')) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the service graph')) + parser.add_argument( + 'service_graph', + metavar='', + help=_("Service graph to modify (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + service_graph_id = client.find_sfc_service_graph( + parsed_args.service_graph, ignore_missing=False)['id'] + attrs = _get_common_attrs(self.app.client_manager, parsed_args, + is_create=False) + try: + client.update_sfc_service_graph(service_graph_id, **attrs) + except Exception as e: + msg = (_("Failed to update service graph " + "'%(service_graph)s': %(e)s") + % {'service_graph': parsed_args.service_graph, 'e': e}) + raise exceptions.CommandError(msg) + + +class DeleteSfcServiceGraph(command.Command): + """Delete a given service graph.""" + + def get_parser(self, prog_name): + parser = super(DeleteSfcServiceGraph, self).get_parser(prog_name) + parser.add_argument( + 'service_graph', + metavar="", + nargs='+', + help=_("ID or name of the service graph(s) to delete.") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + for sg in parsed_args.service_graph: + try: + sg_id = client.find_sfc_service_graph( + sg, ignore_missing=False)['id'] + client.delete_sfc_service_graph(sg_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete service graph with name " + "or ID '%(sg)s': %(e)s"), + {'sg': sg, 'e': e}) + if result > 0: + total = len(parsed_args.service_graph) + msg = (_("%(result)s of %(total)s service graph(s) " + "failed to delete.") % {'result': result, + 'total': total}) + raise exceptions.CommandError(msg) + + +class ListSfcServiceGraph(command.Lister): + _description = _("List service graphs") + + def get_parser(self, prog_name): + parser = super(ListSfcServiceGraph, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + data = client.sfc_service_graphs() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, + (utils.get_dict_properties(s, columns) + for s in data)) + + +class ShowSfcServiceGraph(command.ShowOne): + """Show information of a given service graph.""" + + def get_parser(self, prog_name): + parser = super(ShowSfcServiceGraph, self).get_parser(prog_name) + parser.add_argument( + 'service_graph', + metavar="", + help=_("ID or name of the service graph to display.") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + sg_id = client.find_sfc_service_graph(parsed_args.service_graph, + ignore_missing=False)['id'] + obj = client.get_sfc_service_graph(sg_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + if parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + if parsed_args.description is not None: + attrs['description'] = str(parsed_args.description) + if is_create: + _get_attrs_for_create(client_manager, attrs, parsed_args) + return attrs + + +def _validate_destination_chains(comma_split, attrs, client, sc_): + for e in comma_split: + if e != "": + dc_ = client.find_sfc_port_chain(e, ignore_missing=False)['id'] + attrs['port_chains'][sc_].append(dc_) + if _check_cycle(attrs['port_chains'], sc_, dc_): + raise exceptions.CommandError( + "Error: Service graph contains a cycle") + else: + raise exceptions.CommandError( + "Error: you must specify at least one " + "destination chain for each source chain") + return attrs + + +def _check_cycle(graph, new_src, new_dest): + for src in graph: + if src == new_dest: + if _visit(graph, src, new_dest, new_src): + return True + return False + + +def _visit(graph, src, new_dest, new_src): + if src in graph: + found_cycle = False + for dest in graph[src]: + if new_src == dest or found_cycle: + return True + else: + found_cycle = _visit(graph, dest, new_dest, new_src) + return False + + +def _get_attrs_for_create(client_manager, attrs, parsed_args): + client = client_manager.network + if parsed_args.branching_points: + attrs['port_chains'] = {} + src_chain = None + for c in parsed_args.branching_points: + if ':' not in c: + raise exceptions.CommandError( + "Error: You must specify at least one " + "destination chain for each source chain.") + colon_split = c.split(':') + src_chain = colon_split.pop(0) + sc_ = client.find_sfc_port_chain(src_chain, + ignore_missing=False)['id'] + for i in colon_split: + comma_split = i.split(',') + unique = set(comma_split) + if len(unique) != len(comma_split): + raise exceptions.CommandError( + "Error: Duplicate " + "destination chains from " + "source chain {}".format(src_chain)) + if sc_ in attrs['port_chains']: + raise exceptions.CommandError( + "Error: Source chain {} is in " + "use already ".format(src_chain)) + attrs['port_chains'][sc_] = [] + _validate_destination_chains( + comma_split, attrs, client, sc_) diff --git a/neutronclient/osc/v2/subnet_onboard/__init__.py b/neutronclient/osc/v2/subnet_onboard/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/osc/v2/subnet_onboard/subnet_onboard.py b/neutronclient/osc/v2/subnet_onboard/subnet_onboard.py new file mode 100644 index 000000000..10c6ab4c2 --- /dev/null +++ b/neutronclient/osc/v2/subnet_onboard/subnet_onboard.py @@ -0,0 +1,59 @@ +# Copyright (c) 2019 SUSE Linux Products GmbH +# 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. +# + +"""Subnet onboard action implementation""" +import logging + +from osc_lib.command import command +from osc_lib import exceptions + +from neutronclient._i18n import _ + +LOG = logging.getLogger(__name__) + + +class NetworkOnboardSubnets(command.Command): + """Onboard network subnets into a subnet pool""" + + def get_parser(self, prog_name): + parser = super(NetworkOnboardSubnets, self).get_parser(prog_name) + parser.add_argument( + 'network', + metavar="", + help=_("Onboard all subnets associated with this network") + ) + parser.add_argument( + 'subnetpool', + metavar="", + help=_("Target subnet pool for onboarding subnets") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.neutronclient + subnetpool_id = _get_id(client, parsed_args.subnetpool, 'subnetpool') + network_id = _get_id(client, parsed_args.network, 'network') + body = {'network_id': network_id} + try: + client.onboard_network_subnets(subnetpool_id, body) + except Exception as e: + msg = (_("Failed to onboard subnets for network '%(n)s': %(e)s") + % {'n': parsed_args.network, 'e': e}) + raise exceptions.CommandError(msg) + + +def _get_id(client, id_or_name, resource): + return client.find_resource(resource, str(id_or_name))['id'] diff --git a/neutronclient/osc/v2/utils.py b/neutronclient/osc/v2/utils.py new file mode 100644 index 000000000..b03e29c3d --- /dev/null +++ b/neutronclient/osc/v2/utils.py @@ -0,0 +1,24 @@ +# Copyright 2016 NEC Corporation +# +# 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 module is intended to contain methods specific +to Networking v2 API and its extensions. +""" + +from cliff import columns as cliff_columns + + +class AdminStateColumn(cliff_columns.FormattableColumn): + def human_readable(self): + return 'UP' if self._value else 'DOWN' diff --git a/neutronclient/osc/v2/vpnaas/__init__.py b/neutronclient/osc/v2/vpnaas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/osc/v2/vpnaas/endpoint_group.py b/neutronclient/osc/v2/vpnaas/endpoint_group.py new file mode 100644 index 000000000..e37c2cef4 --- /dev/null +++ b/neutronclient/osc/v2/vpnaas/endpoint_group.py @@ -0,0 +1,222 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util +from oslo_log import log as logging + +from neutronclient._i18n import _ +from neutronclient.osc import utils as osc_utils + + +LOG = logging.getLogger(__name__) + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('type', 'Type', column_util.LIST_BOTH), + ('endpoints', 'Endpoints', column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'type': 'Type', + 'endpoints': 'Endpoints', + 'description': 'Description', + 'tenant_id': 'Project', + 'project_id': 'Project', +} + + +def _get_common_parser(parser): + parser.add_argument( + '--description', + metavar='', + help=_('Description for the endpoint group')) + return parser + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + if is_create: + if parsed_args.project is not None: + attrs['tenant_id'] = osc_utils.find_project( + client_manager.identity, + parsed_args.project, + parsed_args.project_domain, + ).id + if parsed_args.description: + attrs['description'] = parsed_args.description + return attrs + + +class CreateEndpointGroup(command.ShowOne): + _description = _("Create an endpoint group") + + def get_parser(self, prog_name): + parser = super(CreateEndpointGroup, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'name', + metavar='', + help=_('Name for the endpoint group')) + parser.add_argument( + '--type', + required=True, + help=_('Type of endpoints in group (e.g. subnet, cidr)')) + parser.add_argument( + '--value', + action='append', + dest='endpoints', + required=True, + help=_('Endpoint(s) for the group. Must all be of the same type. ' + '(--value) option can be repeated')) + osc_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + attrs['type'] = parsed_args.type + if parsed_args.type == 'subnet': + _subnet_ids = [client.find_subnet( + endpoint, + ignore_missing=False)['id'] + for endpoint in parsed_args.endpoints] + attrs['endpoints'] = _subnet_ids + else: + attrs['endpoints'] = parsed_args.endpoints + obj = client.create_vpn_endpoint_group(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteEndpointGroup(command.Command): + _description = _("Delete endpoint group(s)") + + def get_parser(self, prog_name): + parser = super(DeleteEndpointGroup, self).get_parser(prog_name) + parser.add_argument( + 'endpoint_group', + metavar='', + nargs='+', + help=_('Endpoint group(s) to delete (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + for endpoint in parsed_args.endpoint_group: + try: + endpoint_id = client.find_vpn_endpoint_group( + endpoint, ignore_missing=False)['id'] + client.delete_vpn_endpoint_group(endpoint_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete endpoint group with " + "name or ID '%(endpoint_group)s': %(e)s"), + {'endpoint_group': endpoint, 'e': e}) + + if result > 0: + total = len(parsed_args.endpoint_group) + msg = (_("%(result)s of %(total)s endpoint group failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListEndpointGroup(command.Lister): + _description = _("List endpoint groups that belong to a given project") + + def get_parser(self, prog_name): + parser = super(ListEndpointGroup, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.vpn_endpoint_groups() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, (utils.get_dict_properties(s, columns) for s in obj)) + + +class SetEndpointGroup(command.Command): + _description = _("Set endpoint group properties") + + def get_parser(self, prog_name): + parser = super(SetEndpointGroup, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--name', + metavar='', + help=_('Set a name for the endpoint group')) + parser.add_argument( + 'endpoint_group', + metavar='', + help=_('Endpoint group to set (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, + parsed_args, is_create=False) + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + endpoint_id = client.find_vpn_endpoint_group( + parsed_args.endpoint_group, ignore_missing=False)['id'] + try: + client.update_vpn_endpoint_group(endpoint_id, **attrs) + except Exception as e: + msg = (_("Failed to set endpoint group " + "%(endpoint_group)s: %(e)s") + % {'endpoint_group': parsed_args.endpoint_group, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowEndpointGroup(command.ShowOne): + _description = _("Display endpoint group details") + + def get_parser(self, prog_name): + parser = super(ShowEndpointGroup, self).get_parser(prog_name) + parser.add_argument( + 'endpoint_group', + metavar='', + help=_('Endpoint group to display (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + endpoint_id = client.find_vpn_endpoint_group( + parsed_args.endpoint_group, ignore_missing=False)['id'] + obj = client.get_vpn_endpoint_group(endpoint_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return (display_columns, data) diff --git a/neutronclient/osc/v2/vpnaas/ikepolicy.py b/neutronclient/osc/v2/vpnaas/ikepolicy.py new file mode 100644 index 000000000..0a8aecb08 --- /dev/null +++ b/neutronclient/osc/v2/vpnaas/ikepolicy.py @@ -0,0 +1,318 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util +from oslo_log import log as logging + +from neutronclient._i18n import _ +from neutronclient.common import utils as nc_utils +from neutronclient.osc import utils as osc_utils +from neutronclient.osc.v2.vpnaas import utils as vpn_utils + + +LOG = logging.getLogger(__name__) + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('auth_algorithm', 'Authentication Algorithm', column_util.LIST_BOTH), + ('encryption_algorithm', 'Encryption Algorithm', column_util.LIST_BOTH), + ('ike_version', 'IKE Version', column_util.LIST_BOTH), + ('pfs', 'Perfect Forward Secrecy (PFS)', column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('phase1_negotiation_mode', 'Phase1 Negotiation Mode', + column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('lifetime', 'Lifetime', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'auth_algorithm': 'Authentication Algorithm', + 'encryption_algorithm': 'Encryption Algorithm', + 'ike_version': 'IKE Version', + 'pfs': 'Perfect Forward Secrecy (PFS)', + 'phase1_negotiation_mode': 'Phase1 Negotiation Mode', + 'lifetime': 'Lifetime', + 'description': 'Description', + 'tenant_id': 'Project', + 'project_id': 'Project', +} + +_auth_algorithms = [ + 'sha1', + 'sha256', + 'sha384', + 'sha512', + 'aes-xcbc', + 'aes-cmac', +] + +_encryption_algorithms = [ + '3des', + 'aes-128', + 'aes-192', + 'aes-256', + 'aes-128-ccm-8', + 'aes-192-ccm-8', + 'aes-256-ccm-8', + 'aes-128-ccm-12', + 'aes-192-ccm-12', + 'aes-256-ccm-12', + 'aes-128-ccm-16', + 'aes-192-ccm-16', + 'aes-256-ccm-16', + 'aes-128-gcm-8', + 'aes-192-gcm-8', + 'aes-256-gcm-8', + 'aes-128-gcm-12', + 'aes-192-gcm-12', + 'aes-256-gcm-12', + 'aes-128-gcm-16', + 'aes-192-gcm-16', + 'aes-256-gcm-16', + 'aes-128-ctr', + 'aes-192-ctr', + 'aes-256-ctr', +] + +_pfs_groups = [ + 'group2', + 'group5', + 'group14', + 'group15', + 'group16', + 'group17', + 'group18', + 'group19', + 'group20', + 'group21', + 'group22', + 'group23', + 'group24', + 'group25', + 'group26', + 'group27', + 'group28', + 'group29', + 'group30', + 'group31', +] + + +def _convert_to_lowercase(string): + return string.lower() + + +def _get_common_parser(parser): + parser.add_argument( + '--description', + metavar='', + help=_('Description of the IKE policy')) + parser.add_argument( + '--auth-algorithm', + choices=_auth_algorithms, + type=_convert_to_lowercase, + help=_('Authentication algorithm')) + parser.add_argument( + '--encryption-algorithm', + choices=_encryption_algorithms, + type=_convert_to_lowercase, + help=_('Encryption algorithm')) + parser.add_argument( + '--phase1-negotiation-mode', + choices=['main', 'aggressive'], + type=_convert_to_lowercase, + help=_('IKE Phase1 negotiation mode')) + parser.add_argument( + '--ike-version', + choices=['v1', 'v2'], + type=_convert_to_lowercase, + help=_('IKE version for the policy')) + parser.add_argument( + '--pfs', + choices=_pfs_groups, + type=_convert_to_lowercase, + help=_('Perfect Forward Secrecy')) + parser.add_argument( + '--lifetime', + metavar="units=UNITS,value=VALUE", + type=nc_utils.str2dict_type(optional_keys=['units', 'value']), + help=vpn_utils.lifetime_help("IKE")) + return parser + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + if is_create: + if 'project' in parsed_args and parsed_args.project is not None: + attrs['project_id'] = osc_utils.find_project( + client_manager.identity, + parsed_args.project, + parsed_args.project_domain, + ).id + if parsed_args.description: + attrs['description'] = parsed_args.description + if parsed_args.auth_algorithm: + attrs['auth_algorithm'] = parsed_args.auth_algorithm + if parsed_args.encryption_algorithm: + attrs['encryption_algorithm'] = parsed_args.encryption_algorithm + if parsed_args.phase1_negotiation_mode: + attrs['phase1_negotiation_mode'] = parsed_args.phase1_negotiation_mode + if parsed_args.ike_version: + attrs['ike_version'] = parsed_args.ike_version + if parsed_args.pfs: + attrs['pfs'] = parsed_args.pfs + if parsed_args.lifetime: + vpn_utils.validate_lifetime_dict(parsed_args.lifetime) + attrs['lifetime'] = parsed_args.lifetime + return attrs + + +class CreateIKEPolicy(command.ShowOne): + _description = _("Create an IKE policy") + + def get_parser(self, prog_name): + parser = super(CreateIKEPolicy, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the IKE policy')) + osc_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + obj = client.create_vpn_ike_policy(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id', 'units', 'value']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteIKEPolicy(command.Command): + _description = _("Delete IKE policy (policies)") + + def get_parser(self, prog_name): + parser = super(DeleteIKEPolicy, self).get_parser(prog_name) + parser.add_argument( + 'ikepolicy', + metavar='', + nargs='+', + help=_('IKE policy to delete (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + for ike in parsed_args.ikepolicy: + try: + ike_id = client.find_vpn_ike_policy(ike, + ignore_missing=False)['id'] + client.delete_vpn_ike_policy(ike_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete IKE policy with " + "name or ID '%(ikepolicy)s': %(e)s"), + {'ikepolicy': ike, 'e': e}) + + if result > 0: + total = len(parsed_args.ikepolicy) + msg = (_("%(result)s of %(total)s IKE policy failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListIKEPolicy(command.Lister): + _description = _("List IKE policies that belong to a given project") + + def get_parser(self, prog_name): + parser = super(ListIKEPolicy, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.vpn_ike_policies() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, (utils.get_dict_properties(s, columns) for s in obj)) + + +class SetIKEPolicy(command.Command): + _description = _("Set IKE policy properties") + + def get_parser(self, prog_name): + parser = super(SetIKEPolicy, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the IKE policy')) + parser.add_argument( + 'ikepolicy', + metavar='', + help=_('IKE policy to set (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, + parsed_args, is_create=False) + if parsed_args.name: + attrs['name'] = parsed_args.name + ike_id = client.find_vpn_ike_policy(parsed_args.ikepolicy, + ignore_missing=False)['id'] + try: + client.update_vpn_ike_policy(ike_id, **attrs) + except Exception as e: + msg = (_("Failed to set IKE policy '%(ike)s': %(e)s") + % {'ike': parsed_args.ikepolicy, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowIKEPolicy(command.ShowOne): + _description = _("Display IKE policy details") + + def get_parser(self, prog_name): + parser = super(ShowIKEPolicy, self).get_parser(prog_name) + parser.add_argument( + 'ikepolicy', + metavar='', + help=_('IKE policy to display (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + ike_id = client.find_vpn_ike_policy(parsed_args.ikepolicy, + ignore_missing=False)['id'] + obj = client.get_vpn_ike_policy(ike_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id', 'units', 'value']) + data = utils.get_dict_properties(obj, columns) + return (display_columns, data) diff --git a/neutronclient/osc/v2/vpnaas/ipsec_site_connection.py b/neutronclient/osc/v2/vpnaas/ipsec_site_connection.py new file mode 100644 index 000000000..1497793b0 --- /dev/null +++ b/neutronclient/osc/v2/vpnaas/ipsec_site_connection.py @@ -0,0 +1,385 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 osc_lib.cli import format_columns +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util +from oslo_log import log as logging + +from neutronclient._i18n import _ +from neutronclient.common import utils as nc_utils +from neutronclient.osc import utils as osc_utils +from neutronclient.osc.v2.vpnaas import utils as vpn_utils + + +LOG = logging.getLogger(__name__) + + +_formatters = { + 'peer_cidrs': format_columns.ListColumn +} + + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('peer_address', 'Peer Address', column_util.LIST_BOTH), + ('auth_mode', 'Authentication Algorithm', column_util.LIST_BOTH), + ('status', 'Status', column_util.LIST_BOTH), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('peer_cidrs', 'Peer CIDRs', column_util.LIST_LONG_ONLY), + ('vpnservice_id', 'VPN Service', column_util.LIST_LONG_ONLY), + ('ipsecpolicy_id', 'IPSec Policy', column_util.LIST_LONG_ONLY), + ('ikepolicy_id', 'IKE Policy', column_util.LIST_LONG_ONLY), + ('mtu', 'MTU', column_util.LIST_LONG_ONLY), + ('initiator', 'Initiator', column_util.LIST_LONG_ONLY), + ('is_admin_state_up', 'State', column_util.LIST_LONG_ONLY), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('psk', 'Pre-shared Key', column_util.LIST_LONG_ONLY), + ('route_mode', 'Route Mode', column_util.LIST_LONG_ONLY), + ('local_id', 'Local ID', column_util.LIST_LONG_ONLY), + ('peer_id', 'Peer ID', column_util.LIST_LONG_ONLY), + ('local_ep_group_id', 'Local Endpoint Group ID', + column_util.LIST_LONG_ONLY), + ('peer_ep_group_id', 'Peer Endpoint Group ID', column_util.LIST_LONG_ONLY), + ('dpd', 'DPD', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'peer_address': 'Peer Address', + 'auth_mode': 'Authentication Algorithm', + 'status': 'Status', + 'peer_cidrs': 'Peer CIDRs', + 'vpnservice_id': 'VPN Service', + 'ipsecpolicy_id': 'IPSec Policy', + 'ikepolicy_id': 'IKE Policy', + 'mtu': 'MTU', + 'initiator': 'Initiator', + 'is_admin_state_up': 'State', + 'psk': 'Pre-shared Key', + 'route_mode': 'Route Mode', + 'local_id': 'Local ID', + 'peer_id': 'Peer ID', + 'local_ep_group_id': 'Local Endpoint Group ID', + 'peer_ep_group_id': 'Peer Endpoint Group ID', + 'description': 'Description', + 'project_id': 'Project', + 'dpd': 'DPD', +} + + +def _convert_to_lowercase(string): + return string.lower() + + +def _get_common_parser(parser, is_create=True): + parser.add_argument( + '--description', + metavar='', + help=_('Description for the connection')) + parser.add_argument( + '--dpd', + metavar="action=ACTION,interval=INTERVAL,timeout=TIMEOUT", + type=nc_utils.str2dict_type( + optional_keys=['action', 'interval', 'timeout']), + help=vpn_utils.dpd_help("IPsec connection")) + parser.add_argument( + '--mtu', + help=_('MTU size for the connection')) + parser.add_argument( + '--initiator', + choices=['bi-directional', 'response-only'], + type=_convert_to_lowercase, + help=_('Initiator state')) + peer_group = parser.add_mutually_exclusive_group() + peer_group.add_argument( + '--peer-cidr', + dest='peer_cidrs', + help=_('Remote subnet(s) in CIDR format. ' + 'Cannot be specified when using endpoint groups. Only ' + 'applicable, if subnet provided for VPN service.') + ) + peer_group.add_argument( + '--local-endpoint-group', + help=_('Local endpoint group (name or ID) with subnet(s) ' + 'for IPsec connection') + ) + parser.add_argument( + '--peer-endpoint-group', + help=_('Peer endpoint group (name or ID) with CIDR(s) for ' + 'IPSec connection')) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + action='store_true', + help=_("Enable IPSec site connection") + ) + admin_group.add_argument( + '--disable', + action='store_true', + help=_("Disable IPSec site connection") + ) + parser.add_argument( + '--local-id', + help=_('An ID to be used instead of the external IP ' + 'address for a virtual router')) + return parser + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + if is_create: + if 'project' in parsed_args and parsed_args.project is not None: + attrs['project_id'] = osc_utils.find_project( + client_manager.identity, + parsed_args.project, + parsed_args.project_domain, + ).id + if parsed_args.description: + attrs['description'] = str(parsed_args.description) + if parsed_args.mtu: + attrs['mtu'] = parsed_args.mtu + if parsed_args.enable: + attrs['admin_state_up'] = True + if parsed_args.disable: + attrs['admin_state_up'] = False + if parsed_args.initiator: + attrs['initiator'] = parsed_args.initiator + if parsed_args.dpd: + vpn_utils.validate_dpd_dict(parsed_args.dpd) + attrs['dpd'] = parsed_args.dpd + if parsed_args.local_endpoint_group: + _local_epg = client_manager.network.find_vpn_endpoint_group( + parsed_args.local_endpoint_group)['id'] + attrs['local_ep_group_id'] = _local_epg + if parsed_args.peer_endpoint_group: + _peer_epg = client_manager.network.find_vpn_endpoint_group( + parsed_args.peer_endpoint_group)['id'] + attrs['peer_ep_group_id'] = _peer_epg + if parsed_args.peer_cidrs: + attrs['peer_cidrs'] = parsed_args.peer_cidrs + if parsed_args.local_id: + attrs['local_id'] = parsed_args.local_id + return attrs + + +class CreateIPsecSiteConnection(command.ShowOne): + _description = _("Create an IPsec site connection") + + def get_parser(self, prog_name): + parser = super(CreateIPsecSiteConnection, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--peer-id', + required=True, + help=_('Peer router identity for authentication. Can be ' + 'IPv4/IPv6 address, e-mail address, key id, or FQDN')) + parser.add_argument( + '--peer-address', + required=True, + help=_('Peer gateway public IPv4/IPv6 address or FQDN')) + parser.add_argument( + '--psk', + required=True, + help=_('Pre-shared key string.')) + parser.add_argument( + '--vpnservice', + metavar='VPNSERVICE', + required=True, + help=_('VPN service instance associated with this ' + 'connection (name or ID)')) + parser.add_argument( + '--ikepolicy', + metavar='IKEPOLICY', + required=True, + help=_('IKE policy associated with this connection (name or ID)')) + parser.add_argument( + '--ipsecpolicy', + metavar='IPSECPOLICY', + required=True, + help=_('IPsec policy associated with this connection ' + '(name or ID)')) + parser.add_argument( + 'name', + metavar='', + help=_('Set friendly name for the connection')) + osc_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + if parsed_args.vpnservice: + _vpnservice_id = client.find_vpn_service( + parsed_args.vpnservice, ignore_missing=False)['id'] + attrs['vpnservice_id'] = _vpnservice_id + if parsed_args.ikepolicy: + _ikepolicy_id = client.find_vpn_ike_policy( + parsed_args.ikepolicy, ignore_missing=False)['id'] + attrs['ikepolicy_id'] = _ikepolicy_id + if parsed_args.ipsecpolicy: + _ipsecpolicy_id = client.find_vpn_ipsec_policy( + parsed_args.ipsecpolicy, ignore_missing=False)['id'] + attrs['ipsecpolicy_id'] = _ipsecpolicy_id + if parsed_args.peer_id: + attrs['peer_id'] = parsed_args.peer_id + if parsed_args.peer_address: + attrs['peer_address'] = parsed_args.peer_address + if parsed_args.psk: + attrs['psk'] = parsed_args.psk + if parsed_args.name: + attrs['name'] = parsed_args.name + if (bool(parsed_args.local_endpoint_group) != + bool(parsed_args.peer_endpoint_group)): + message = _("You must specify both local and peer endpoint " + "groups") + raise exceptions.CommandError(message) + if not parsed_args.peer_cidrs and not parsed_args.local_endpoint_group: + message = _("You must specify endpoint groups or peer CIDR(s)") + raise exceptions.CommandError(message) + obj = client.create_vpn_ipsec_site_connection(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id', 'action', + 'timeout', 'interval']) + data = utils.get_dict_properties(obj, columns, formatters=_formatters) + return display_columns, data + + +class DeleteIPsecSiteConnection(command.Command): + _description = _("Delete IPsec site connection(s)") + + def get_parser(self, prog_name): + parser = super(DeleteIPsecSiteConnection, self).get_parser(prog_name) + parser.add_argument( + 'ipsec_site_connection', + metavar='', + nargs='+', + help=_('IPsec site connection to delete (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + for ipsec_conn in parsed_args.ipsec_site_connection: + try: + ipsec_con_id = client.find_vpn_ipsec_site_connection( + ipsec_conn, ignore_missing=False)['id'] + client.delete_vpn_ipsec_site_connection(ipsec_con_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete IPsec site connection with " + "name or ID '%(ipsec_site_conn)s': %(e)s"), + {'ipsec_site_conn': ipsec_conn, 'e': e}) + + if result > 0: + total = len(parsed_args.ipsec_site_connection) + msg = (_("%(result)s of %(total)s IPsec site connection failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListIPsecSiteConnection(command.Lister): + _description = _("List IPsec site connections " + "that belong to a given project") + + def get_parser(self, prog_name): + parser = super(ListIPsecSiteConnection, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.vpn_ipsec_site_connections() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, (utils.get_dict_properties( + s, columns, formatters=_formatters) for s in obj)) + + +class SetIPsecSiteConnection(command.Command): + _description = _("Set IPsec site connection properties") + + def get_parser(self, prog_name): + parser = super(SetIPsecSiteConnection, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--peer-id', + help=_('Peer router identity for authentication. Can be ' + 'IPv4/IPv6 address, e-mail address, key id, or FQDN')) + parser.add_argument( + '--peer-address', + help=_('Peer gateway public IPv4/IPv6 address or FQDN')) + parser.add_argument( + '--name', + metavar='', + help=_('Set friendly name for the connection')) + parser.add_argument( + 'ipsec_site_connection', + metavar='', + help=_('IPsec site connection to set (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, + parsed_args, is_create=False) + if parsed_args.peer_id: + attrs['peer_id'] = parsed_args.peer_id + if parsed_args.peer_address: + attrs['peer_address'] = parsed_args.peer_address + if parsed_args.name: + attrs['name'] = parsed_args.name + ipsec_conn_id = client.find_vpn_ipsec_site_connection( + parsed_args.ipsec_site_connection, ignore_missing=False)['id'] + try: + client.update_vpn_ipsec_site_connection(ipsec_conn_id, **attrs) + except Exception as e: + msg = (_("Failed to set IPsec site " + "connection '%(ipsec_conn)s': %(e)s") + % {'ipsec_conn': parsed_args.ipsec_site_connection, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowIPsecSiteConnection(command.ShowOne): + _description = _("Show information of a given IPsec site connection") + + def get_parser(self, prog_name): + parser = super(ShowIPsecSiteConnection, self).get_parser(prog_name) + parser.add_argument( + 'ipsec_site_connection', + metavar='', + help=_('IPsec site connection to display (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + ipsec_site_id = client.find_vpn_ipsec_site_connection( + parsed_args.ipsec_site_connection, ignore_missing=False)['id'] + obj = client.get_vpn_ipsec_site_connection(ipsec_site_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id', 'action', + 'timeout', 'interval']) + data = utils.get_dict_properties(obj, columns, formatters=_formatters) + return (display_columns, data) diff --git a/neutronclient/osc/v2/vpnaas/ipsecpolicy.py b/neutronclient/osc/v2/vpnaas/ipsecpolicy.py new file mode 100644 index 000000000..c6e42bd06 --- /dev/null +++ b/neutronclient/osc/v2/vpnaas/ipsecpolicy.py @@ -0,0 +1,318 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util +from oslo_log import log as logging + +from neutronclient._i18n import _ +from neutronclient.common import utils as nc_utils +from neutronclient.osc import utils as osc_utils +from neutronclient.osc.v2.vpnaas import utils as vpn_utils + + +LOG = logging.getLogger(__name__) + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('auth_algorithm', 'Authentication Algorithm', column_util.LIST_BOTH), + ('encapsulation_mode', 'Encapsulation Mode', column_util.LIST_BOTH), + ('transform_protocol', 'Transform Protocol', column_util.LIST_BOTH), + ('encryption_algorithm', 'Encryption Algorithm', column_util.LIST_BOTH), + ('pfs', 'Perfect Forward Secrecy (PFS)', column_util.LIST_LONG_ONLY), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('lifetime', 'Lifetime', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'auth_algorithm': 'Authentication Algorithm', + 'encapsulation_mode': 'Encapsulation Mode', + 'transform_protocol': 'Transform Protocol', + 'encryption_algorithm': 'Encryption Algorithm', + 'pfs': 'Perfect Forward Secrecy (PFS)', + 'lifetime': 'Lifetime', + 'description': 'Description', + 'project_id': 'Project', +} + +_auth_algorithms = [ + 'sha1', + 'sha256', + 'sha384', + 'sha512', + 'aes-xcbc', + 'aes-cmac', +] + +_encryption_algorithms = [ + '3des', + 'aes-128', + 'aes-192', + 'aes-256', + 'aes-128-ccm-8', + 'aes-192-ccm-8', + 'aes-256-ccm-8', + 'aes-128-ccm-12', + 'aes-192-ccm-12', + 'aes-256-ccm-12', + 'aes-128-ccm-16', + 'aes-192-ccm-16', + 'aes-256-ccm-16', + 'aes-128-gcm-8', + 'aes-192-gcm-8', + 'aes-256-gcm-8', + 'aes-128-gcm-12', + 'aes-192-gcm-12', + 'aes-256-gcm-12', + 'aes-128-gcm-16', + 'aes-192-gcm-16', + 'aes-256-gcm-16', + 'aes-128-ctr', + 'aes-192-ctr', + 'aes-256-ctr', +] + +_pfs_groups = [ + 'group2', + 'group5', + 'group14', + 'group15', + 'group16', + 'group17', + 'group18', + 'group19', + 'group20', + 'group21', + 'group22', + 'group23', + 'group24', + 'group25', + 'group26', + 'group27', + 'group28', + 'group29', + 'group30', + 'group31', +] + + +def _convert_to_lowercase(string): + return string.lower() + + +def _get_common_parser(parser): + parser.add_argument( + '--description', + metavar='', + help=_('Description of the IPsec policy')) + parser.add_argument( + '--auth-algorithm', + choices=_auth_algorithms, + type=_convert_to_lowercase, + help=_('Authentication algorithm for IPsec policy')) + parser.add_argument( + '--encapsulation-mode', + choices=['tunnel', 'transport'], + type=_convert_to_lowercase, + help=_('Encapsulation mode for IPsec policy')) + parser.add_argument( + '--encryption-algorithm', + choices=_encryption_algorithms, + type=_convert_to_lowercase, + help=_('Encryption algorithm for IPsec policy')) + parser.add_argument( + '--lifetime', + metavar="units=UNITS,value=VALUE", + type=nc_utils.str2dict_type(optional_keys=['units', 'value']), + help=vpn_utils.lifetime_help("IPsec")) + parser.add_argument( + '--pfs', + choices=_pfs_groups, + type=_convert_to_lowercase, + help=_('Perfect Forward Secrecy for IPsec policy')) + parser.add_argument( + '--transform-protocol', + type=_convert_to_lowercase, + choices=['esp', 'ah', 'ah-esp'], + help=_('Transform protocol for IPsec policy')) + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + if is_create: + if 'project' in parsed_args and parsed_args.project is not None: + attrs['project_id'] = osc_utils.find_project( + client_manager.identity, + parsed_args.project, + parsed_args.project_domain, + ).id + if parsed_args.description: + attrs['description'] = str(parsed_args.description) + if parsed_args.auth_algorithm: + attrs['auth_algorithm'] = parsed_args.auth_algorithm + if parsed_args.encapsulation_mode: + attrs['encapsulation_mode'] = parsed_args.encapsulation_mode + if parsed_args.transform_protocol: + attrs['transform_protocol'] = parsed_args.transform_protocol + if parsed_args.encryption_algorithm: + attrs['encryption_algorithm'] = parsed_args.encryption_algorithm + if parsed_args.pfs: + attrs['pfs'] = parsed_args.pfs + if parsed_args.lifetime: + vpn_utils.validate_lifetime_dict(parsed_args.lifetime) + attrs['lifetime'] = parsed_args.lifetime + return attrs + + +class CreateIPsecPolicy(command.ShowOne): + _description = _("Create an IPsec policy") + + def get_parser(self, prog_name): + parser = super(CreateIPsecPolicy, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the IPsec policy')) + osc_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + obj = client.create_vpn_ipsec_policy(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id', + 'phase1_negotiation_mode', 'units', 'value']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteIPsecPolicy(command.Command): + _description = _("Delete IPsec policy(policies)") + + def get_parser(self, prog_name): + parser = super(DeleteIPsecPolicy, self).get_parser(prog_name) + parser.add_argument( + 'ipsecpolicy', + metavar='', + nargs='+', + help=_('ipsec policy to delete (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + for ipsec in parsed_args.ipsecpolicy: + try: + ipsec_id = client.find_vpn_ipsec_policy( + ipsec, ignore_missing=False)['id'] + client.delete_vpn_ipsec_policy(ipsec_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete IPsec policy with " + "name or ID '%(ipsecpolicy)s': %(e)s"), + {'ipsecpolicy': ipsec, 'e': e}) + + if result > 0: + total = len(parsed_args.ipsecpolicy) + msg = (_("%(result)s of %(total)s IPsec policy failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListIPsecPolicy(command.Lister): + _description = _("List IPsec policies that belong to a given project") + + def get_parser(self, prog_name): + parser = super(ListIPsecPolicy, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.vpn_ipsec_policies() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, (utils.get_dict_properties(s, columns) for s in obj)) + + +class SetIPsecPolicy(command.Command): + _description = _("Set IPsec policy properties") + + def get_parser(self, prog_name): + parser = super(SetIPsecPolicy, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the IPsec policy')) + parser.add_argument( + 'ipsecpolicy', + metavar='', + help=_('IPsec policy to set (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, + parsed_args, is_create=False) + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + ipsec_id = client.find_vpn_ipsec_policy( + parsed_args.ipsecpolicy, ignore_missing=False)['id'] + try: + client.update_vpn_ipsec_policy(ipsec_id, **attrs) + except Exception as e: + msg = (_("Failed to set IPsec policy '%(ipsec)s': %(e)s") + % {'ipsec': parsed_args.ipsecpolicy, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowIPsecPolicy(command.ShowOne): + _description = _("Display IPsec policy details") + + def get_parser(self, prog_name): + parser = super(ShowIPsecPolicy, self).get_parser(prog_name) + parser.add_argument( + 'ipsecpolicy', + metavar='', + help=_('IPsec policy to display (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + ipsec_id = client.find_vpn_ipsec_policy( + parsed_args.ipsecpolicy, ignore_missing=False)['id'] + obj = client.get_vpn_ipsec_policy(ipsec_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id', + 'phase1_negotiation_mode', 'units', 'value']) + data = utils.get_dict_properties(obj, columns) + return (display_columns, data) diff --git a/neutronclient/osc/v2/vpnaas/utils.py b/neutronclient/osc/v2/vpnaas/utils.py new file mode 100644 index 000000000..2de5cc35e --- /dev/null +++ b/neutronclient/osc/v2/vpnaas/utils.py @@ -0,0 +1,112 @@ +# Copyright 2017 FUJITSU LIMITED +# 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. +# + + +"""VPN Utilities and helper functions.""" + + +from neutronclient._i18n import _ +from neutronclient.common import exceptions + +DPD_SUPPORTED_ACTIONS = ['hold', 'clear', 'restart', + 'restart-by-peer', 'disabled'] +DPD_SUPPORTED_KEYS = ['action', 'interval', 'timeout'] + +lifetime_keys = ['units', 'value'] +lifetime_units = ['seconds'] + + +def validate_dpd_dict(dpd_dict): + for key, value in dpd_dict.items(): + if key not in DPD_SUPPORTED_KEYS: + message = _( + "DPD Dictionary KeyError: " + "Reason-Invalid DPD key : " + "'%(key)s' not in %(supported_key)s") % { + 'key': key, 'supported_key': DPD_SUPPORTED_KEYS} + raise exceptions.CommandError(message) + if key == 'action' and value not in DPD_SUPPORTED_ACTIONS: + message = _( + "DPD Dictionary ValueError: " + "Reason-Invalid DPD action : " + "'%(key_value)s' not in %(supported_action)s") % { + 'key_value': value, + 'supported_action': DPD_SUPPORTED_ACTIONS} + raise exceptions.CommandError(message) + if key in ('interval', 'timeout'): + try: + if int(value) <= 0: + raise ValueError() + except ValueError: + message = _( + "DPD Dictionary ValueError: " + "Reason-Invalid positive integer value: " + "'%(key)s' = %(value)s") % { + 'key': key, 'value': value} + raise exceptions.CommandError(message) + else: + dpd_dict[key] = int(value) + return + + +def validate_lifetime_dict(lifetime_dict): + + for key, value in lifetime_dict.items(): + if key not in lifetime_keys: + message = _( + "Lifetime Dictionary KeyError: " + "Reason-Invalid unit key : " + "'%(key)s' not in %(supported_key)s") % { + 'key': key, 'supported_key': lifetime_keys} + raise exceptions.CommandError(message) + if key == 'units' and value not in lifetime_units: + message = _( + "Lifetime Dictionary ValueError: " + "Reason-Invalid units : " + "'%(key_value)s' not in %(supported_units)s") % { + 'key_value': key, 'supported_units': lifetime_units} + raise exceptions.CommandError(message) + if key == 'value': + try: + if int(value) < 60: + raise ValueError() + except ValueError: + message = _( + "Lifetime Dictionary ValueError: " + "Reason-Invalid value should be at least 60:" + "'%(key_value)s' = %(value)s") % { + 'key_value': key, 'value': value} + raise exceptions.CommandError(message) + else: + lifetime_dict['value'] = int(value) + return + + +def lifetime_help(policy): + lifetime = _("%s lifetime attributes. " + "'units'-seconds, default:seconds. " + "'value'-non negative integer, default:3600.") % policy + return lifetime + + +def dpd_help(policy): + dpd = _(" %s Dead Peer Detection attributes." + " 'action'-hold,clear,disabled,restart,restart-by-peer." + " 'interval' and 'timeout' are non negative integers. " + " 'interval' should be less than 'timeout' value. " + " 'action', default:hold 'interval', default:30, " + " 'timeout', default:120.") % policy.capitalize() + return dpd diff --git a/neutronclient/osc/v2/vpnaas/vpnservice.py b/neutronclient/osc/v2/vpnaas/vpnservice.py new file mode 100644 index 000000000..9e1e489ca --- /dev/null +++ b/neutronclient/osc/v2/vpnaas/vpnservice.py @@ -0,0 +1,252 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util +from oslo_log import log as logging + +from neutronclient._i18n import _ +from neutronclient.osc import utils as osc_utils + + +LOG = logging.getLogger(__name__) + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('router_id', 'Router', column_util.LIST_BOTH), + ('subnet_id', 'Subnet', column_util.LIST_BOTH), + ('flavor_id', 'Flavor', column_util.LIST_BOTH), + ('is_admin_state_up', 'State', column_util.LIST_BOTH), + ('status', 'Status', column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('external_v4_ip', 'Ext v4 IP', column_util.LIST_LONG_ONLY), + ('external_v6_ip', 'Ext v6 IP', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'router_id': 'Router', + 'subnet_id': 'Subnet', + 'flavor_id': 'Flavor', + 'is_admin_state_up': 'State', + 'status': 'Status', + 'description': 'Description', + 'project_id': 'Project', + 'external_v4_ip': 'Ext v4 IP', + 'external_v6_ip': 'Ext v6 IP', +} + + +def _get_common_parser(parser): + parser.add_argument( + '--description', + metavar='', + help=_('Description for the VPN service')) + parser.add_argument( + '--subnet', + metavar='', + help=_('Local private subnet (name or ID)')) + parser.add_argument( + '--flavor', + metavar='', + help=_('Flavor for the VPN service (name or ID)')) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + action='store_true', + help=_("Enable VPN service") + ) + admin_group.add_argument( + '--disable', + action='store_true', + help=_("Disable VPN service") + ) + return parser + + +def _get_common_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + if is_create: + if 'project' in parsed_args and parsed_args.project is not None: + attrs['project_id'] = osc_utils.find_project( + client_manager.identity, + parsed_args.project, + parsed_args.project_domain, + ).id + if parsed_args.description: + attrs['description'] = str(parsed_args.description) + if parsed_args.subnet: + _subnet_id = client_manager.network.find_subnet( + parsed_args.subnet).id + attrs['subnet_id'] = _subnet_id + if parsed_args.flavor: + _flavor_id = client_manager.network.find_flavor( + parsed_args.flavor, + ignore_missing=False + ).id + attrs['flavor_id'] = _flavor_id + if parsed_args.enable: + attrs['admin_state_up'] = True + if parsed_args.disable: + attrs['admin_state_up'] = False + return attrs + + +class CreateVPNService(command.ShowOne): + _description = _("Create an VPN service") + + def get_parser(self, prog_name): + parser = super(CreateVPNService, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'name', + metavar='', + help=_('Name for the VPN service')) + parser.add_argument( + '--router', + metavar='ROUTER', + required=True, + help=_('Router for the VPN service (name or ID)')) + osc_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + if parsed_args.router: + _router_id = client.find_router(parsed_args.router, + ignore_missing=False).id + attrs['router_id'] = _router_id + obj = client.create_vpn_service(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteVPNService(command.Command): + _description = _("Delete VPN service(s)") + + def get_parser(self, prog_name): + parser = super(DeleteVPNService, self).get_parser(prog_name) + parser.add_argument( + 'vpnservice', + metavar='', + nargs='+', + help=_('VPN service to delete (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + for vpn in parsed_args.vpnservice: + try: + vpn_id = client.find_vpn_service(vpn, + ignore_missing=False)['id'] + client.delete_vpn_service(vpn_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete VPN service with " + "name or ID '%(vpnservice)s': %(e)s"), + {'vpnservice': vpn, 'e': e}) + + if result > 0: + total = len(parsed_args.vpnservice) + msg = (_("%(result)s of %(total)s vpn service failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListVPNService(command.Lister): + _description = _("List VPN services that belong to a given project") + + def get_parser(self, prog_name): + parser = super(ListVPNService, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.vpn_services() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long) + return (headers, (utils.get_dict_properties(s, columns) for s in obj)) + + +class SetVPNSercice(command.Command): + _description = _("Set VPN service properties") + + def get_parser(self, prog_name): + parser = super(SetVPNSercice, self).get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--name', + metavar='', + help=_('Name for the VPN service')) + parser.add_argument( + 'vpnservice', + metavar='', + help=_('VPN service to modify (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, + parsed_args, is_create=False) + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + vpn_id = client.find_vpn_service(parsed_args.vpnservice, + ignore_missing=False)['id'] + try: + client.update_vpn_service(vpn_id, **attrs) + except Exception as e: + msg = (_("Failed to set vpn service '%(vpn)s': %(e)s") + % {'vpn': parsed_args.vpnservice, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowVPNService(command.ShowOne): + _description = _("Display VPN service details") + + def get_parser(self, prog_name): + parser = super(ShowVPNService, self).get_parser(prog_name) + parser.add_argument( + 'vpnservice', + metavar='', + help=_('VPN service to display (name or ID)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + vpn_id = client.find_vpn_service(parsed_args.vpnservice, + ignore_missing=False)['id'] + obj = client.get_vpn_service(vpn_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id']) + data = utils.get_dict_properties(obj, columns) + return (display_columns, data) diff --git a/neutronclient/tests/__init__.py b/neutronclient/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/tests/unit/__init__.py b/neutronclient/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/tests/unit/osc/__init__.py b/neutronclient/tests/unit/osc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/tests/unit/osc/v2/__init__.py b/neutronclient/tests/unit/osc/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/tests/unit/osc/v2/dynamic_routing/__init__.py b/neutronclient/tests/unit/osc/v2/dynamic_routing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/tests/unit/osc/v2/dynamic_routing/fakes.py b/neutronclient/tests/unit/osc/v2/dynamic_routing/fakes.py new file mode 100644 index 000000000..188f06871 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/dynamic_routing/fakes.py @@ -0,0 +1,145 @@ +# 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 unittest import mock +import uuid + +from openstack.network.v2 import agent as _agent +from openstack.network.v2 import bgp_peer as _bgp_peer +from openstack.network.v2 import bgp_speaker as _bgp_speaker + +from neutronclient.tests.unit.osc.v2 import fakes + + +class TestNeutronDynamicRoutingOSCV2(fakes.TestNeutronClientOSCV2): + def setUp(self): + super(TestNeutronDynamicRoutingOSCV2, self).setUp() + self.neutronclient.find_resource = mock.Mock( + side_effect=lambda resource, name_or_id, project_id=None, + cmd_resource=None, parent_id=None, fields=None: + {'id': name_or_id}) + + self.networkclient.find_bgp_speaker = mock.Mock( + side_effect=lambda name_or_id, project_id=None, + cmd_resource=None, parent_id=None, fields=None, + ignore_missing=False: + _bgp_speaker.BgpSpeaker(id=name_or_id)) + self.networkclient.find_bgp_peer = mock.Mock( + side_effect=lambda name_or_id, project_id=None, + cmd_resource=None, parent_id=None, fields=None, + ignore_missing=False: + _bgp_peer.BgpPeer(id=name_or_id)) + + +class FakeBgpSpeaker(object): + """Fake one or more bgp speakers.""" + + @staticmethod + def create_one_bgp_speaker(attrs=None): + attrs = attrs or {} + # Set default attributes. + bgp_speaker_attrs = { + 'peers': [], + 'local_as': 200, + 'advertise_tenant_networks': True, + 'networks': [], + 'ip_version': 4, + 'advertise_floating_ip_host_routes': True, + 'id': uuid.uuid4().hex, + 'name': 'bgp-speaker-' + uuid.uuid4().hex, + 'tenant_id': uuid.uuid4().hex, + } + + # Overwrite default attributes. + bgp_speaker_attrs.update(attrs) + ret_bgp_speaker = _bgp_speaker.BgpSpeaker(**bgp_speaker_attrs) + + return ret_bgp_speaker + + @staticmethod + def create_bgp_speakers(attrs=None, count=1): + """Create multiple fake bgp speakers. + + """ + bgp_speakers = [] + for i in range(count): + bgp_speaker = FakeBgpSpeaker.create_one_bgp_speaker(attrs) + bgp_speakers.append(bgp_speaker) + + return bgp_speakers + + +class FakeBgpPeer(object): + """Fake one or more bgp peers.""" + + @staticmethod + def create_one_bgp_peer(attrs=None): + attrs = attrs or {} + # Set default attributes. + bgp_peer_attrs = { + 'auth_type': None, + 'peer_ip': '1.1.1.1', + 'remote_as': 100, + 'id': uuid.uuid4().hex, + 'name': 'bgp-peer-' + uuid.uuid4().hex, + 'tenant_id': uuid.uuid4().hex, + } + + # Overwrite default attributes. + bgp_peer_attrs.update(attrs) + ret_bgp_peer = _bgp_peer.BgpPeer(**bgp_peer_attrs) + + return ret_bgp_peer + + @staticmethod + def create_bgp_peers(attrs=None, count=1): + """Create one or multiple fake bgp peers.""" + bgp_peers = [] + for i in range(count): + bgp_peer = FakeBgpPeer.create_one_bgp_peer(attrs) + bgp_peers.append(bgp_peer) + + return bgp_peers + + +class FakeDRAgent(object): + """Fake one or more dynamic routing agents.""" + + @staticmethod + def create_one_dragent(attrs=None): + attrs = attrs or {} + # Set default attributes. + dragent_attrs = { + 'binary': 'neutron-bgp-dragent', + 'admin_state_up': True, + 'availability_zone': None, + 'alive': True, + 'topic': 'bgp_dragent', + 'host': 'network-' + uuid.uuid4().hex, + 'name': 'bgp-dragent-' + uuid.uuid4().hex, + 'agent_type': 'BGP dynamic routing agent', + 'id': uuid.uuid4().hex, + } + + # Overwrite default attributes. + dragent_attrs.update(attrs) + return _agent.Agent(**dragent_attrs) + + @staticmethod + def create_dragents(attrs=None, count=1): + """Create one or multiple fake dynamic routing agents.""" + agents = [] + for i in range(count): + agent = FakeDRAgent.create_one_dragent(attrs) + agents.append(agent) + + return agents diff --git a/neutronclient/tests/unit/osc/v2/dynamic_routing/test_bgp_dragent.py b/neutronclient/tests/unit/osc/v2/dynamic_routing/test_bgp_dragent.py new file mode 100644 index 000000000..6842cbbd1 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/dynamic_routing/test_bgp_dragent.py @@ -0,0 +1,84 @@ +# 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 unittest import mock + +from neutronclient.osc.v2.dynamic_routing import bgp_dragent +from neutronclient.tests.unit.osc.v2.dynamic_routing import fakes + + +class TestAddBgpSpeakerToDRAgent(fakes.TestNeutronDynamicRoutingOSCV2): + _bgp_speaker = fakes.FakeBgpSpeaker.create_one_bgp_speaker() + _bgp_dragent = fakes.FakeDRAgent.create_one_dragent() + _bgp_speaker_id = _bgp_speaker['id'] + _bgp_dragent_id = _bgp_dragent['id'] + + def setUp(self): + super(TestAddBgpSpeakerToDRAgent, self).setUp() + + # Get the command object to test + self.cmd = bgp_dragent.AddBgpSpeakerToDRAgent(self.app, self.namespace) + + def test_add_bgp_speaker_to_dragent(self): + arglist = [ + self._bgp_dragent_id, + self._bgp_speaker_id, + ] + verifylist = [ + ('dragent_id', self._bgp_dragent_id), + ('bgp_speaker', self._bgp_speaker_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(self.networkclient, + "add_bgp_speaker_to_dragent", + return_value=None): + + result = self.cmd.take_action(parsed_args) + self.networkclient.add_bgp_speaker_to_dragent.\ + assert_called_once_with( + self._bgp_dragent_id, self._bgp_speaker_id) + self.assertIsNone(result) + + +class TestRemoveBgpSpeakerFromDRAgent(fakes.TestNeutronDynamicRoutingOSCV2): + _bgp_speaker = fakes.FakeBgpSpeaker.create_one_bgp_speaker() + _bgp_dragent = fakes.FakeDRAgent.create_one_dragent() + _bgp_speaker_id = _bgp_speaker['id'] + _bgp_dragent_id = _bgp_dragent['id'] + + def setUp(self): + super(TestRemoveBgpSpeakerFromDRAgent, self).setUp() + + # Get the command object to test + self.cmd = bgp_dragent.RemoveBgpSpeakerFromDRAgent( + self.app, self.namespace) + + def test_remove_bgp_speaker_from_dragent(self): + arglist = [ + self._bgp_dragent_id, + self._bgp_speaker_id, + ] + verifylist = [ + ('dragent_id', self._bgp_dragent_id), + ('bgp_speaker', self._bgp_speaker_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(self.networkclient, + "remove_bgp_speaker_from_dragent", + return_value=None): + result = self.cmd.take_action(parsed_args) + self.networkclient.remove_bgp_speaker_from_dragent.\ + assert_called_once_with(self._bgp_dragent_id, + self._bgp_speaker_id) + self.assertIsNone(result) diff --git a/neutronclient/tests/unit/osc/v2/dynamic_routing/test_bgp_peer.py b/neutronclient/tests/unit/osc/v2/dynamic_routing/test_bgp_peer.py new file mode 100644 index 000000000..73b6bf7ab --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/dynamic_routing/test_bgp_peer.py @@ -0,0 +1,150 @@ +# 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 unittest import mock + +from neutronclient.osc.v2.dynamic_routing import bgp_peer +from neutronclient.tests.unit.osc.v2.dynamic_routing import fakes + + +class TestListBgpPeer(fakes.TestNeutronDynamicRoutingOSCV2): + _bgp_peers = fakes.FakeBgpPeer.create_bgp_peers(count=1) + columns = ('ID', 'Name', 'Peer IP', 'Remote AS') + data = [] + for _bgp_peer in _bgp_peers: + data.append(( + _bgp_peer['id'], + _bgp_peer['name'], + _bgp_peer['peer_ip'], + _bgp_peer['remote_as'])) + + def setUp(self): + super(TestListBgpPeer, self).setUp() + + self.networkclient.bgp_peers = mock.Mock( + return_value=self._bgp_peers + ) + + # Get the command object to test + self.cmd = bgp_peer.ListBgpPeer(self.app, self.namespace) + + def test_bgp_peer_list(self): + parsed_args = self.check_parser(self.cmd, [], []) + + columns, data = self.cmd.take_action(parsed_args) + self.networkclient.bgp_peers.assert_called_once_with( + retrieve_all=True) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestDeleteBgpPeer(fakes.TestNeutronDynamicRoutingOSCV2): + + _bgp_peer = fakes.FakeBgpPeer.create_one_bgp_peer() + + def setUp(self): + super(TestDeleteBgpPeer, self).setUp() + + self.networkclient.delete_bgp_peer = mock.Mock(return_value=None) + + self.cmd = bgp_peer.DeleteBgpPeer(self.app, self.namespace) + + def test_delete_bgp_peer(self): + arglist = [ + self._bgp_peer['name'], + ] + verifylist = [ + ('bgp_peer', self._bgp_peer['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.networkclient.delete_bgp_peer.assert_called_once_with( + self._bgp_peer['name']) + self.assertIsNone(result) + + +class TestShowBgpPeer(fakes.TestNeutronDynamicRoutingOSCV2): + _one_bgp_peer = fakes.FakeBgpPeer.create_one_bgp_peer() + data = ( + _one_bgp_peer['auth_type'], + _one_bgp_peer['id'], + _one_bgp_peer['name'], + _one_bgp_peer['peer_ip'], + _one_bgp_peer['tenant_id'], + _one_bgp_peer['remote_as'], + ) + _bgp_peer = _one_bgp_peer + _bgp_peer_name = _one_bgp_peer['name'] + columns = ( + 'auth_type', + 'id', + 'name', + 'peer_ip', + 'project_id', + 'remote_as', + ) + + def setUp(self): + super(TestShowBgpPeer, self).setUp() + + self.networkclient.get_bgp_peer = mock.Mock( + return_value=self._bgp_peer + ) + # Get the command object to test + self.cmd = bgp_peer.ShowBgpPeer(self.app, self.namespace) + + def test_bgp_peer_show(self): + arglist = [ + self._bgp_peer_name, + ] + verifylist = [ + ('bgp_peer', self._bgp_peer_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + data = self.cmd.take_action(parsed_args) + self.networkclient.get_bgp_peer.assert_called_once_with( + self._bgp_peer_name) + self.assertEqual(self.columns, data[0]) + self.assertEqual(self.data, data[1]) + + +class TestSetBgpPeer(fakes.TestNeutronDynamicRoutingOSCV2): + _one_bgp_peer = fakes.FakeBgpPeer.create_one_bgp_peer() + _bgp_peer_name = _one_bgp_peer['name'] + + def setUp(self): + super(TestSetBgpPeer, self).setUp() + self.networkclient.update_bgp_peer = mock.Mock(return_value=None) + bgp_peer.get_bgp_peer_id = mock.Mock(return_value=self._bgp_peer_name) + + self.cmd = bgp_peer.SetBgpPeer(self.app, self.namespace) + + def test_set_bgp_peer(self): + arglist = [ + self._bgp_peer_name, + '--name', 'noob', + ] + verifylist = [ + ('bgp_peer', self._bgp_peer_name), + ('name', 'noob'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = {'name': 'noob', 'password': None} + self.networkclient.update_bgp_peer.assert_called_once_with( + self._bgp_peer_name, **attrs) + self.assertIsNone(result) diff --git a/neutronclient/tests/unit/osc/v2/dynamic_routing/test_bgp_speaker.py b/neutronclient/tests/unit/osc/v2/dynamic_routing/test_bgp_speaker.py new file mode 100644 index 000000000..c5e42e588 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/dynamic_routing/test_bgp_speaker.py @@ -0,0 +1,157 @@ +# 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 unittest import mock + +from neutronclient.osc.v2.dynamic_routing import bgp_speaker +from neutronclient.tests.unit.osc.v2.dynamic_routing import fakes + + +class TestListBgpSpeaker(fakes.TestNeutronDynamicRoutingOSCV2): + _bgp_speakers = fakes.FakeBgpSpeaker.create_bgp_speakers() + columns = ('ID', 'Name', 'Local AS', 'IP Version') + data = [] + for _bgp_speaker in _bgp_speakers: + data.append(( + _bgp_speaker['id'], + _bgp_speaker['name'], + _bgp_speaker['local_as'], + _bgp_speaker['ip_version'])) + + def setUp(self): + super(TestListBgpSpeaker, self).setUp() + + self.networkclient.bgp_speakers = mock.Mock( + return_value=self._bgp_speakers + ) + + # Get the command object to test + self.cmd = bgp_speaker.ListBgpSpeaker(self.app, self.namespace) + + def test_bgp_speaker_list(self): + parsed_args = self.check_parser(self.cmd, [], []) + + columns, data = self.cmd.take_action(parsed_args) + self.networkclient.bgp_speakers.assert_called_once_with( + retrieve_all=True) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestDeleteBgpSpeaker(fakes.TestNeutronDynamicRoutingOSCV2): + + _bgp_speaker = fakes.FakeBgpSpeaker.create_one_bgp_speaker() + + def setUp(self): + super(TestDeleteBgpSpeaker, self).setUp() + + self.networkclient.delete_bgp_speaker = mock.Mock(return_value=None) + + self.cmd = bgp_speaker.DeleteBgpSpeaker(self.app, self.namespace) + + def test_delete_bgp_speaker(self): + arglist = [ + self._bgp_speaker['name'], + ] + verifylist = [ + ('bgp_speaker', self._bgp_speaker['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.networkclient.delete_bgp_speaker.assert_called_once_with( + self._bgp_speaker['name']) + self.assertIsNone(result) + + +class TestShowBgpSpeaker(fakes.TestNeutronDynamicRoutingOSCV2): + _one_bgp_speaker = fakes.FakeBgpSpeaker.create_one_bgp_speaker() + data = ( + _one_bgp_speaker['advertise_floating_ip_host_routes'], + _one_bgp_speaker['advertise_tenant_networks'], + _one_bgp_speaker['id'], + _one_bgp_speaker['ip_version'], + _one_bgp_speaker['local_as'], + _one_bgp_speaker['name'], + _one_bgp_speaker['networks'], + _one_bgp_speaker['peers'], + _one_bgp_speaker['tenant_id'] + ) + _bgp_speaker = _one_bgp_speaker + _bgp_speaker_name = _one_bgp_speaker['name'] + columns = ( + 'advertise_floating_ip_host_routes', + 'advertise_tenant_networks', + 'id', + 'ip_version', + 'local_as', + 'name', + 'networks', + 'peers', + 'project_id' + ) + + def setUp(self): + super(TestShowBgpSpeaker, self).setUp() + + self.networkclient.get_bgp_speaker = mock.Mock( + return_value=self._bgp_speaker + ) + # Get the command object to test + self.cmd = bgp_speaker.ShowBgpSpeaker(self.app, self.namespace) + + def test_bgp_speaker_show(self): + arglist = [ + self._bgp_speaker_name, + ] + verifylist = [ + ('bgp_speaker', self._bgp_speaker_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + data = self.cmd.take_action(parsed_args) + self.networkclient.get_bgp_speaker.assert_called_once_with( + self._bgp_speaker_name) + self.assertEqual(self.columns, data[0]) + self.assertEqual(self.data, data[1]) + + +class TestSetBgpSpeaker(fakes.TestNeutronDynamicRoutingOSCV2): + _one_bgp_speaker = fakes.FakeBgpSpeaker.create_one_bgp_speaker() + _bgp_speaker_name = _one_bgp_speaker['name'] + + def setUp(self): + super(TestSetBgpSpeaker, self).setUp() + self.networkclient.update_bgp_speaker = mock.Mock( + return_value=None) + + self.cmd = bgp_speaker.SetBgpSpeaker(self.app, self.namespace) + + def test_set_bgp_speaker(self): + arglist = [ + self._bgp_speaker_name, + '--name', 'noob', + ] + verifylist = [ + ('bgp_speaker', self._bgp_speaker_name), + ('name', 'noob'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = {'name': 'noob'} + self.networkclient.update_bgp_speaker.assert_called_once_with( + self._bgp_speaker_name, **attrs) + self.assertIsNone(result) diff --git a/neutronclient/tests/unit/osc/v2/fakes.py b/neutronclient/tests/unit/osc/v2/fakes.py new file mode 100644 index 000000000..7fec7347c --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/fakes.py @@ -0,0 +1,50 @@ +# 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 +from unittest import mock + +from cliff import columns as cliff_columns +from osc_lib.tests import utils + + +class TestNeutronClientOSCV2(utils.TestCommand): + + def setUp(self): + super(TestNeutronClientOSCV2, self).setUp() + self.namespace = argparse.Namespace() + self.app.client_manager.session = mock.Mock() + self.app.client_manager.neutronclient = mock.Mock() + self.neutronclient = self.app.client_manager.neutronclient + + self.app.client_manager.network = mock.Mock() + self.networkclient = self.app.client_manager.network + + self.addCleanup(mock.patch.stopall) + + # TODO(amotoki): Move this to osc_lib + def assertListItemEqual(self, expected, actual): + self.assertEqual(len(expected), len(actual)) + for item_expected, item_actual in zip(expected, actual): + self.assertItemEqual(item_expected, item_actual) + + # TODO(amotoki): Move this to osc_lib + def assertItemEqual(self, expected, actual): + self.assertEqual(len(expected), len(actual)) + for col_expected, col_actual in zip(expected, actual): + if isinstance(col_expected, cliff_columns.FormattableColumn): + self.assertIsInstance(col_actual, col_expected.__class__) + self.assertEqual(col_expected.human_readable(), + col_actual.human_readable()) + else: + self.assertEqual(col_expected, col_actual) diff --git a/neutronclient/tests/unit/osc/v2/lbaas/__init__.py b/neutronclient/tests/unit/osc/v2/lbaas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/tests/unit/osc/v2/logging/__init__.py b/neutronclient/tests/unit/osc/v2/logging/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/tests/unit/osc/v2/logging/fakes.py b/neutronclient/tests/unit/osc/v2/logging/fakes.py new file mode 100644 index 000000000..a20e3712e --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/logging/fakes.py @@ -0,0 +1,78 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 collections +import copy +from unittest import mock +import uuid + + +class FakeLogging(object): + + def create(self, attrs={}): + """Create a fake network logs + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A OrderedDict faking the network log + """ + self.ordered.update(attrs) + return copy.deepcopy(self.ordered) + + def bulk_create(self, attrs=None, count=2): + """Create multiple fake network logs + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of network logs to fake + :return: + A list of dictionaries faking the network logs + """ + return [self.create(attrs=attrs) for i in range(0, count)] + + def get(self, attrs=None, count=2): + """Create multiple fake network logs + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of network logs to fake + :return: + A list of dictionaries faking the network log + """ + if attrs is None: + self.attrs = self.bulk_create(count=count) + return mock.Mock(side_effect=attrs) + + +class NetworkLog(FakeLogging): + """Fake one or more network log""" + + def __init__(self): + super(NetworkLog, self).__init__() + self.ordered = collections.OrderedDict(( + ('id', 'log-id-' + uuid.uuid4().hex), + ('description', 'my-desc-' + uuid.uuid4().hex), + ('enabled', False), + ('name', 'my-log-' + uuid.uuid4().hex), + ('target_id', None), + ('project_id', 'project-id-' + uuid.uuid4().hex), + ('resource_id', None), + ('resource_type', 'security_group'), + ('event', 'all'), + )) diff --git a/neutronclient/tests/unit/osc/v2/logging/test_network_log.py b/neutronclient/tests/unit/osc/v2/logging/test_network_log.py new file mode 100644 index 000000000..c2e0390ae --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/logging/test_network_log.py @@ -0,0 +1,749 @@ +# Copyright 2017-2018 FUJITSU LIMITED +# 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 +from unittest import mock + +from osc_lib import exceptions +from osc_lib.tests import utils +import testtools + +from neutronclient.osc import utils as osc_utils +from neutronclient.osc.v2.logging import network_log +from neutronclient.tests.unit.osc.v2 import fakes as test_fakes +from neutronclient.tests.unit.osc.v2.logging import fakes + + +_log = fakes.NetworkLog().create() +RES_TYPE_SG = 'security_group' +RES_TYPE_FWG = 'firewall_group' +CONVERT_MAP = { + 'project': 'project_id', + 'enable': 'enabled', + 'disable': 'enabled', + 'target': 'target_id', + 'resource': 'resource_id', + 'event': 'event', +} + + +def _generate_data(ordered_dict=None, data=None): + source = ordered_dict if ordered_dict else _log + if data: + source.update(data) + return tuple(source[key] for key in source) + + +def _generate_req_and_res(verifylist): + request = dict(verifylist) + response = copy.deepcopy(_log) + for key, val in verifylist: + converted = CONVERT_MAP.get(key, key) + del request[key] + if key == 'enable' and val: + new_value = True + elif key == 'disable' and val: + new_value = False + else: + new_value = val + request[converted] = new_value + response[converted] = new_value + return request, response + + +class TestNetworkLog(test_fakes.TestNeutronClientOSCV2): + + def check_results(self, headers, data, exp_req, is_list=False): + if is_list: + req_body = {'logs': [exp_req]} + else: + req_body = {'log': exp_req} + self.mocked.assert_called_once_with(req_body) + self.assertEqual(self.ordered_headers, headers) + self.assertEqual(self.ordered_data, data) + + def setUp(self): + super(TestNetworkLog, self).setUp() + self.neutronclient.find_resource = mock.Mock() + self.neutronclient.find_resource.side_effect = \ + lambda x, y, **k: {'id': y} + osc_utils.find_project = mock.Mock() + osc_utils.find_project.id = _log['project_id'] + self.res = _log + self.headers = ( + 'ID', + 'Description', + 'Enabled', + 'Name', + 'Target', + 'Project', + 'Resource', + 'Type', + 'Event', + ) + self.data = _generate_data() + self.ordered_headers = ( + 'Description', + 'Enabled', + 'Event', + 'ID', + 'Name', + 'Project', + 'Resource', + 'Target', + 'Type', + ) + self.ordered_data = ( + _log['description'], + _log['enabled'], + _log['event'], + _log['id'], + _log['name'], + _log['project_id'], + _log['resource_id'], + _log['target_id'], + _log['resource_type'], + ) + self.ordered_columns = ( + 'description', + 'enabled', + 'event', + 'id', + 'name', + 'project_id', + 'resource_id', + 'target_id', + 'resource_type', + ) + + +class TestCreateNetworkLog(TestNetworkLog): + + def setUp(self): + super(TestCreateNetworkLog, self).setUp() + self.neutronclient.create_network_log = mock.Mock( + return_value={'log': _log}) + self.mocked = self.neutronclient.create_network_log + self.cmd = network_log.CreateNetworkLog(self.app, self.namespace) + loggables = { + "loggable_resources": [{"type": RES_TYPE_SG}, + {"type": RES_TYPE_FWG}] + } + self.neutronclient.list_network_loggable_resources = mock.Mock( + return_value=loggables) + + def _update_expect_response(self, request, response): + """Set expected request and response + + :param request + A dictionary of request body(dict of verifylist) + :param response + A OrderedDict of request body + """ + # Update response body + self.neutronclient.create_network_log.return_value = \ + {'log': dict(response)} + osc_utils.find_project.return_value.id = response['project_id'] + # Update response(finally returns 'data') + self.data = _generate_data(ordered_dict=response) + self.ordered_data = tuple( + response[column] for column in self.ordered_columns + ) + + def _set_all_params(self, args={}): + name = args.get('name', 'my-log') + desc = args.get('description', 'my-description-for-log') + event = args.get('event', 'ACCEPT') + resource = args.get('resource', 'id-target-log') + target = args.get('target', 'id-target-log') + resource_type = args.get('resource_type', 'security_group') + project = args.get('project_id', 'id-my-project') + + arglist = [ + name, + '--description', desc, + '--enable', + '--target', target, + '--resource', resource, + '--event', event, + '--project', project, + '--resource-type', resource_type, + ] + verifylist = [ + ('description', desc), + ('enable', True), + ('event', event), + ('name', name), + ('target', target), + ('project', project), + ('resource', target), + ('resource_type', resource_type), + ] + return arglist, verifylist + + def _test_create_with_all_params(self, args={}): + arglist, verifylist = self._set_all_params(args) + request, response = _generate_req_and_res(verifylist) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.check_results(headers, data, request) + + def test_create_with_no_options_and_raise(self): + arglist = [] + verifylist = [] + + self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_create_with_mandatory_params(self): + name = self.res['name'] + arglist = [ + name, + '--resource-type', RES_TYPE_SG, + ] + verifylist = [ + ('name', name), + ('resource_type', RES_TYPE_SG), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + expect = { + 'name': self.res['name'], + 'resource_type': self.res['resource_type'], + } + self.mocked.assert_called_once_with({'log': expect}) + self.assertEqual(self.ordered_headers, headers) + self.assertEqual(self.ordered_data, data) + + def test_create_with_disable(self): + name = self.res['name'] + arglist = [ + name, + '--resource-type', RES_TYPE_SG, + '--disable', + ] + verifylist = [ + ('name', name), + ('resource_type', RES_TYPE_SG), + ('disable', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + expect = { + 'name': self.res['name'], + 'resource_type': self.res['resource_type'], + 'enabled': False, + } + self.mocked.assert_called_once_with({'log': expect}) + self.assertEqual(self.ordered_headers, headers) + self.assertEqual(self.ordered_data, data) + + def test_create_with_all_params(self): + self._test_create_with_all_params() + + def test_create_with_all_params_event_drop(self): + self._test_create_with_all_params({'event': 'DROP'}) + + def test_create_with_all_params_event_all(self): + self._test_create_with_all_params({'event': 'ALL'}) + + def test_create_with_all_params_except_event(self): + arglist, verifylist = self._set_all_params({'event': ''}) + self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_create_with_all_params_event_upper_capitalized(self): + for event in ('all', 'All', 'dROP', 'accePt', 'accept', 'drop'): + arglist, verifylist = self._set_all_params({'event': event}) + self.assertRaises( + testtools.matchers._impl.MismatchError, + self.check_parser, self.cmd, arglist, verifylist) + + def test_create_with_all_params_resource_type_upper_capitalized(self): + for res_type in ('SECURITY_GROUP', 'Security_group', 'security_Group'): + arglist, verifylist = self._set_all_params( + {'resource_type': res_type}) + self.assertRaises( + testtools.matchers._impl.MismatchError, + self.check_parser, self.cmd, arglist, verifylist) + + def test_create_with_valid_fwg_resource(self): + name = self.res['name'] + resource_id = 'valid_fwg_id' + resource_type = RES_TYPE_FWG + # Test with valid FWG ID + with mock.patch.object(self.neutronclient, 'find_resource', + return_value={'id': resource_id}): + arglist = [name, + '--resource-type', resource_type, + '--resource', resource_id + ] + verifylist = [ + ('name', name), + ('resource_type', resource_type), + ('resource', resource_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + expect = { + 'name': self.res['name'], + 'resource_type': RES_TYPE_FWG, + 'resource_id': 'valid_fwg_id', + } + self.neutronclient.find_resource.assert_called_with( + resource_type, + resource_id, + cmd_resource='fwaas_firewall_group') + self.mocked.assert_called_once_with({'log': expect}) + self.assertEqual(self.ordered_headers, headers) + self.assertEqual(self.ordered_data, data) + + def test_create_with_invalid_fwg_resource(self): + name = self.res['name'] + resource_id = 'invalid_fwg_id' + resource_type = RES_TYPE_FWG + # Test with invalid FWG ID + with mock.patch.object(self.neutronclient, 'find_resource', + side_effect=exceptions.NotFound(code=0)): + arglist = [name, + '--resource-type', resource_type, + '--resource', resource_id + ] + verifylist = [ + ('name', name), + ('resource_type', resource_type), + ('resource', resource_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.NotFound, + self.cmd.take_action, + parsed_args) + self.neutronclient.find_resource.assert_called_with( + resource_type, + resource_id, + cmd_resource='fwaas_firewall_group') + self.mocked.assert_not_called() + + def test_create_with_invalid_resource_type(self): + name = self.res['name'] + resource_type = 'invalid_resource_type' + resource_id = 'valid_fwg_id' + with mock.patch.object(self.neutronclient, 'find_resource', + side_effect=exceptions.NotFound(code=0)): + arglist = [name, + '--resource-type', resource_type, + '--resource', resource_id + ] + verifylist = [ + ('name', name), + ('resource_type', resource_type), + ('resource', resource_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.NotFound, + self.cmd.take_action, + parsed_args) + self.neutronclient.find_resource.assert_called_with( + resource_type, + resource_id, + cmd_resource=None) + self.mocked.assert_not_called() + + +class TestListNetworkLog(TestNetworkLog): + + def _setup_summary(self, expect=None): + event = 'Event: ' + self.res['event'].upper() + target = 'Logged: (None specified)' + if expect: + if expect.get('event'): + event = expect['event'] + if expect.get('resource'): + target = expect['resource'] + summary = ',\n'.join([event, target]) + self.short_data = ( + expect['id'] if expect else self.res['id'], + expect['enabled'] if expect else self.res['enabled'], + expect['name'] if expect else self.res['name'], + expect['resource_type'] if expect else self.res['resource_type'], + summary + ) + + def setUp(self): + super(TestListNetworkLog, self).setUp() + self.cmd = network_log.ListNetworkLog(self.app, self.namespace) + + self.short_header = ( + 'ID', + 'Enabled', + 'Name', + 'Type', + 'Summary', + ) + self._setup_summary() + self.neutronclient.list_network_logs = mock.Mock( + return_value={'logs': [self.res]}) + self.mocked = self.neutronclient.list_network_logs + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + self.assertEqual([self.data], list(data)) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + def test_list_with_target_and_resource(self): + arglist = [] + verifylist = [] + target_id = 'aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaaaaaaa' + resource_id = 'bbbbbbbb-bbbb-bbbb-bbbbbbbbbbbbbbbbb' + log = fakes.NetworkLog().create({ + 'target_id': target_id, + 'resource_id': resource_id}) + self.mocked.return_value = {'logs': [log]} + logged = 'Logged: (security_group) %(res_id)s on (port) %(t_id)s' % { + 'res_id': resource_id, 't_id': target_id} + expect_log = copy.deepcopy(log) + expect_log.update({ + 'resource': logged, + 'event': 'Event: ALL'}) + self._setup_summary(expect=expect_log) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + def test_list_with_resource(self): + arglist = [] + verifylist = [] + resource_id = 'bbbbbbbb-bbbb-bbbb-bbbbbbbbbbbbbbbbb' + log = fakes.NetworkLog().create({'resource_id': resource_id}) + self.mocked.return_value = {'logs': [log]} + logged = 'Logged: (security_group) %s' % resource_id + expect_log = copy.deepcopy(log) + expect_log.update({ + 'resource': logged, + 'event': 'Event: ALL'}) + self._setup_summary(expect=expect_log) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + def test_list_with_target(self): + arglist = [] + verifylist = [] + target_id = 'aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaaaaaaa' + log = fakes.NetworkLog().create({'target_id': target_id}) + self.mocked.return_value = {'logs': [log]} + logged = 'Logged: (port) %s' % target_id + expect_log = copy.deepcopy(log) + expect_log.update({ + 'resource': logged, + 'event': 'Event: ALL'}) + self._setup_summary(expect=expect_log) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestShowNetworkLog(TestNetworkLog): + + def setUp(self): + super(TestShowNetworkLog, self).setUp() + self.neutronclient.show_network_log = mock.Mock( + return_value={'log': self.res}) + self.mocked = self.neutronclient.show_network_log + self.cmd = network_log.ShowNetworkLog(self.app, self.namespace) + + def test_show_filtered_by_id_or_name(self): + target = self.res['id'] + arglist = [target] + verifylist = [('network_log', target)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target) + self.assertEqual(self.ordered_headers, headers) + self.assertEqual(self.ordered_data, data) + + +class TestSetNetworkLog(TestNetworkLog): + + def setUp(self): + super(TestSetNetworkLog, self).setUp() + self.neutronclient.update_network_log = mock.Mock( + return_value={'log': self.res}) + self.mocked = self.neutronclient.update_network_log + self.cmd = network_log.SetNetworkLog(self.app, self.namespace) + + def test_set_name(self): + target = self.res['id'] + update = 'change' + arglist = [target, '--name', update] + verifylist = [ + ('network_log', target), + ('name', update), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, {'log': {'name': update}}) + self.assertIsNone(result) + + def test_set_description(self): + target = self.res['id'] + update = 'change-desc' + arglist = [target, '--description', update] + verifylist = [ + ('network_log', target), + ('description', update), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, {'log': {'description': update}}) + self.assertIsNone(result) + + def test_set_enable(self): + target = self.res['id'] + arglist = [target, '--enable'] + verifylist = [ + ('network_log', target), + ('enable', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, {'log': {'enabled': True}}) + self.assertIsNone(result) + + def test_set_disable(self): + target = self.res['id'] + arglist = [target, '--disable'] + verifylist = [ + ('network_log', target), + ('disable', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, {'log': {'enabled': False}}) + self.assertIsNone(result) + + # Illegal tests + def test_illegal_set_resource_type(self): + target = self.res['id'] + resource_type = 'security_group' + arglist = [target, '--resource-type', resource_type] + verifylist = [ + ('network_log', target), + ('resource_type', resource_type), + ] + + self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_illegal_set_event(self): + target = self.res['id'] + for event in ['all', 'accept', 'drop']: + arglist = [target, '--event', event] + verifylist = [ + ('network_log', target), + ('event', event), + ] + self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_illegal_set_resource_id(self): + target = self.res['id'] + resource_id = 'resource-id-for-logged-target' + arglist = [target, '--resource', resource_id] + verifylist = [ + ('network_log', target), + ('resource', resource_id), + ] + + self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_illegal_set_project(self): + target = self.res['id'] + arglist = [ + target, + '--project', + ] + verifylist = [ + ('network_log', target), + ('project', 'other-project'), + ] + self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_illegal_set_project_domain(self): + target = self.res['id'] + arglist = [ + target, + '--project-domain', + ] + verifylist = [ + ('network_log', target), + ('project_domain', 'other-project-domain'), + ] + self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_illegal_set_and_raises(self): + self.neutronclient.update_network_log = mock.Mock( + side_effect=Exception) + target = self.res['id'] + arglist = [target, '--name', 'my-name'] + verifylist = [('network_log', target), ('name', 'my-name')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args) + + +class TestDeleteNetworkLog(TestNetworkLog): + + def setUp(self): + super(TestDeleteNetworkLog, self).setUp() + self.neutronclient.delete_network_log = mock.Mock( + return_value={'log': self.res}) + self.mocked = self.neutronclient.delete_network_log + self.cmd = network_log.DeleteNetworkLog(self.app, self.namespace) + + def test_delete_with_one_resource(self): + target = self.res['id'] + arglist = [target] + verifylist = [('network_log', [target])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target) + self.assertIsNone(result) + + def test_delete_with_multiple_resources(self): + target1 = 'target1' + target2 = 'target2' + arglist = [target1, target2] + verifylist = [('network_log', [target1, target2])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertEqual(2, self.mocked.call_count) + for idx, reference in enumerate([target1, target2]): + actual = ''.join(self.mocked.call_args_list[idx][0]) + self.assertEqual(reference, actual) + + def test_delete_with_no_exist_id(self): + self.neutronclient.find_resource.side_effect = Exception + target = 'not_exist' + arglist = [target] + verifylist = [('network_log', [target])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args) + + +class TestLoggableResource(test_fakes.TestNeutronClientOSCV2): + + def check_results(self, headers, data, exp_req, is_list=False): + if is_list: + req_body = {'logs': [exp_req]} + else: + req_body = {'log': exp_req} + self.mocked.assert_called_once_with(req_body) + self.assertEqual(self.ordered_headers, headers) + self.assertEqual(self.ordered_data, data) + + def setUp(self): + super(TestLoggableResource, self).setUp() + self.headers = ('Supported types',) + self.data = ('security_group', ) + + +class TestListLoggableResource(TestLoggableResource): + + def setUp(self): + super(TestListLoggableResource, self).setUp() + self.cmd = network_log.ListLoggableResource(self.app, self.namespace) + + loggables = { + "loggable_resources": [{"type": "security_group"}] + } + self.neutronclient.list_network_loggable_resources = mock.Mock( + return_value=loggables) + self.mocked = self.neutronclient.list_network_loggable_resources + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + self.assertEqual([self.data], list(data)) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + self.assertEqual([self.data], list(data)) diff --git a/neutronclient/tests/unit/osc/v2/sfc/__init__.py b/neutronclient/tests/unit/osc/v2/sfc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/tests/unit/osc/v2/sfc/fakes.py b/neutronclient/tests/unit/osc/v2/sfc/fakes.py new file mode 100755 index 000000000..390c67e4e --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/sfc/fakes.py @@ -0,0 +1,302 @@ +# Copyright (c) 2017 Huawei Technologies India Pvt.Limited. +# 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 +from unittest import mock + + +from osc_lib.tests import utils +from oslo_utils import uuidutils + +from openstack.network.v2 import sfc_flow_classifier as flow_classifier +from openstack.network.v2 import sfc_port_chain as port_chain +from openstack.network.v2 import sfc_port_pair as port_pair +from openstack.network.v2 import sfc_port_pair_group as port_pair_group +from openstack.network.v2 import sfc_service_graph as service_graph + + +class TestNeutronClientOSCV2(utils.TestCommand): + + def setUp(self): + super(TestNeutronClientOSCV2, self).setUp() + self.namespace = argparse.Namespace() + self.app.client_manager.session = mock.Mock() + self.app.client_manager.network = mock.Mock() + self.network = self.app.client_manager.network + self.network.find_sfc_flow_classifier = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id} + ) + self.network.find_sfc_port_chain = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id} + ) + self.network.find_sfc_port_pair = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id} + ) + self.network.find_sfc_port_pair_group = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id} + ) + self.network.find_sfc_service_graph = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id} + ) + self.network.find_port = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id} + ) + + +class FakeSfcPortPair(object): + """Fake port pair attributes.""" + + @staticmethod + def create_port_pair(attrs=None): + """Create a fake port pair. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A Dictionary with id, name, description, ingress, egress, + service-function-parameter, project_id + """ + attrs = attrs or {} + + # Set default attributes. + port_pair_attrs = { + 'description': 'description', + 'egress': uuidutils.generate_uuid(), + 'id': uuidutils.generate_uuid(), + 'ingress': uuidutils.generate_uuid(), + 'name': 'port-pair-name', + 'service_function_parameters': [('correlation', None), + ('weight', 1)], + 'project_id': uuidutils.generate_uuid(), + } + + # Overwrite default attributes. + port_pair_attrs.update(attrs) + return port_pair.SfcPortPair(**port_pair_attrs) + + @staticmethod + def create_port_pairs(attrs=None, count=1): + """Create multiple port_pairs. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of port_pairs to fake + :return: + A list of dictionaries faking the port_pairs + """ + port_pairs = [] + for _ in range(count): + port_pairs.append(FakeSfcPortPair.create_port_pair(attrs)) + + return port_pairs + + +class FakeSfcPortPairGroup(object): + """Fake port pair group attributes.""" + + @staticmethod + def create_port_pair_group(attrs=None): + """Create a fake port pair group. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A Dictionary with id, name, description, port_pairs, group_id + port_pair_group_parameters, project_id + """ + attrs = attrs or {} + + # Set default attributes. + port_pair_group_attrs = { + 'id': uuidutils.generate_uuid(), + 'name': 'port-pair-group-name', + 'description': 'description', + 'port_pairs': uuidutils.generate_uuid(), + 'port_pair_group_parameters': {"lb_fields": []}, + 'project_id': uuidutils.generate_uuid(), + 'tap_enabled': False + } + + port_pair_group_attrs.update(attrs) + return port_pair_group.SfcPortPairGroup(**port_pair_group_attrs) + + @staticmethod + def create_port_pair_groups(attrs=None, count=1): + """Create multiple port pair groups. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of port_pair_groups to fake + :return: + A list of dictionaries faking the port pair groups + """ + port_pair_groups = [] + for _ in range(count): + port_pair_groups.append( + FakeSfcPortPairGroup.create_port_pair_group(attrs)) + + return port_pair_groups + + +class FakeSfcFlowClassifier(object): + """Fake flow classifier attributes.""" + + @staticmethod + def create_flow_classifier(attrs=None): + """Create a fake flow classifier. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A Dictionary with faking port chain attributes + """ + attrs = attrs or {} + + # Set default attributes. + flow_classifier_attrs = { + 'id': uuidutils.generate_uuid(), + 'destination_ip_prefix': '2.2.2.2/32', + 'destination_port_range_max': '90', + 'destination_port_range_min': '80', + 'ethertype': 'IPv4', + 'logical_destination_port': uuidutils.generate_uuid(), + 'logical_source_port': uuidutils.generate_uuid(), + 'name': 'flow-classifier-name', + 'description': 'fc_description', + 'protocol': 'tcp', + 'source_ip_prefix': '1.1.1.1/32', + 'source_port_range_max': '20', + 'source_port_range_min': '10', + 'project_id': uuidutils.generate_uuid(), + 'l7_parameters': {} + } + flow_classifier_attrs.update(attrs) + return flow_classifier.SfcFlowClassifier(**flow_classifier_attrs) + + @staticmethod + def create_flow_classifiers(attrs=None, count=1): + """Create multiple flow classifiers. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of flow classifiers to fake + :return: + A list of dictionaries faking the flow classifiers + """ + flow_classifiers = [] + for _ in range(count): + flow_classifiers.append( + FakeSfcFlowClassifier.create_flow_classifier(attrs)) + + return flow_classifiers + + +class FakeSfcPortChain(object): + """Fake port chain attributes.""" + + @staticmethod + def create_port_chain(attrs=None): + """Create a fake port chain. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A Dictionary with faking port chain attributes + """ + attrs = attrs or {} + + # Set default attributes. + port_chain_attrs = { + 'id': uuidutils.generate_uuid(), + 'name': 'port-chain-name', + 'description': 'description', + 'port_pair_groups': uuidutils.generate_uuid(), + 'flow_classifiers': uuidutils.generate_uuid(), + 'chain_parameters': {"correlation": "mpls", "symmetric": False}, + 'project_id': uuidutils.generate_uuid(), + } + + port_chain_attrs.update(attrs) + return port_chain.SfcPortChain(**port_chain_attrs) + + @staticmethod + def create_port_chains(attrs=None, count=1): + """Create multiple port chains. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of port chains to fake + :return: + A list of dictionaries faking the port chains. + """ + port_chains = [] + for _ in range(count): + port_chains.append(FakeSfcPortChain.create_port_chain(attrs)) + return port_chains + + +class FakeSfcServiceGraph(object): + """Fake service graph attributes.""" + + @staticmethod + def create_sfc_service_graph(attrs=None): + """Create a fake service graph. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A Dictionary with faking service graph attributes + """ + attrs = attrs or {} + + # Set default attributes. + service_graph_attrs = { + 'id': uuidutils.generate_uuid(), + 'name': 'port-pair-group-name', + 'description': 'description', + 'port_chains': {uuidutils.generate_uuid(): [ + uuidutils.generate_uuid()]}, + 'project_id': uuidutils.generate_uuid(), + } + + service_graph_attrs.update(attrs) + return service_graph.SfcServiceGraph(**service_graph_attrs) + + @staticmethod + def create_sfc_service_graphs(attrs=None, count=1): + """Create multiple service graphs. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of service graphs to fake + :return: + A list of dictionaries faking the service graphs. + """ + service_graphs = [] + for _ in range(count): + service_graphs.append( + FakeSfcServiceGraph.create_sfc_service_graph(attrs)) + return service_graphs diff --git a/neutronclient/tests/unit/osc/v2/sfc/test_flow_classifier.py b/neutronclient/tests/unit/osc/v2/sfc/test_flow_classifier.py new file mode 100755 index 000000000..ea1c4f3ab --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/sfc/test_flow_classifier.py @@ -0,0 +1,385 @@ +# Copyright (c) 2017 Huawei Technologies India Pvt.Limited. +# 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 unittest import mock + +from osc_lib import exceptions +import testtools + +from neutronclient.osc.v2.sfc import sfc_flow_classifier +from neutronclient.tests.unit.osc.v2.sfc import fakes + + +class TestCreateSfcFlowClassifier(fakes.TestNeutronClientOSCV2): + + _fc = fakes.FakeSfcFlowClassifier.create_flow_classifier() + + columns = ('Description', + 'Destination IP', + 'Destination Port Range Max', + 'Destination Port Range Min', + 'Ethertype', + 'ID', + 'L7 Parameters', + 'Logical Destination Port', + 'Logical Source Port', + 'Name', + 'Project', + 'Protocol', + 'Source IP', + 'Source Port Range Max', + 'Source Port Range Min', + 'Summary',) + + def get_data(self): + return ( + self._fc['description'], + self._fc['destination_ip_prefix'], + self._fc['destination_port_range_max'], + self._fc['destination_port_range_min'], + self._fc['ethertype'], + self._fc['id'], + self._fc['l7_parameters'], + self._fc['logical_destination_port'], + self._fc['logical_source_port'], + self._fc['name'], + self._fc['project_id'], + self._fc['protocol'], + self._fc['source_ip_prefix'], + self._fc['source_port_range_max'], + self._fc['source_port_range_min'] + ) + + def setUp(self): + super(TestCreateSfcFlowClassifier, self).setUp() + self.network.create_sfc_flow_classifier = mock.Mock( + return_value=self._fc) + self.data = self.get_data() + + # Get the command object to test + self.cmd = sfc_flow_classifier.CreateSfcFlowClassifier(self.app, + self.namespace) + + def test_create_flow_classifier_default_options(self): + arglist = [ + "--logical-source-port", self._fc['logical_source_port'], + "--ethertype", self._fc['ethertype'], + self._fc['name'], + ] + verifylist = [ + ('logical_source_port', self._fc['logical_source_port']), + ('ethertype', self._fc['ethertype']), + ('name', self._fc['name']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_sfc_flow_classifier.assert_called_once_with( + **{'name': self._fc['name'], + 'logical_source_port': self._fc['logical_source_port'], + 'ethertype': self._fc['ethertype'] + } + ) + self.assertEqual(self.columns, columns) + + def test_create_flow_classifier(self): + arglist = [ + "--description", self._fc['description'], + "--ethertype", self._fc['ethertype'], + "--protocol", self._fc['protocol'], + "--source-ip-prefix", self._fc['source_ip_prefix'], + "--destination-ip-prefix", self._fc['destination_ip_prefix'], + "--logical-source-port", self._fc['logical_source_port'], + "--logical-destination-port", self._fc['logical_destination_port'], + self._fc['name'], + "--l7-parameters", 'url=path', + ] + + param = 'url=path' + + verifylist = [ + ('description', self._fc['description']), + ('name', self._fc['name']), + ('ethertype', self._fc['ethertype']), + ('protocol', self._fc['protocol']), + ('source_ip_prefix', self._fc['source_ip_prefix']), + ('destination_ip_prefix', self._fc['destination_ip_prefix']), + ('logical_source_port', self._fc['logical_source_port']), + ('logical_destination_port', + self._fc['logical_destination_port']), + ('l7_parameters', param) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + self.network.create_sfc_flow_classifier.assert_called_once_with( + **{ + 'name': self._fc['name'], + 'description': self._fc['description'], + 'ethertype': self._fc['ethertype'], + 'protocol': self._fc['protocol'], + 'source_ip_prefix': self._fc['source_ip_prefix'], + 'destination_ip_prefix': self._fc['destination_ip_prefix'], + 'logical_source_port': self._fc['logical_source_port'], + 'logical_destination_port': + self._fc['logical_destination_port'], + 'l7_parameters': param + } + ) + self.assertEqual(self.columns, columns) + + +class TestDeleteSfcFlowClassifier(fakes.TestNeutronClientOSCV2): + + _flow_classifier = \ + fakes.FakeSfcFlowClassifier.create_flow_classifiers(count=1) + + def setUp(self): + super(TestDeleteSfcFlowClassifier, self).setUp() + self.network.delete_sfc_flow_classifier = mock.Mock( + return_value=None) + self.cmd = sfc_flow_classifier.DeleteSfcFlowClassifier(self.app, + self.namespace) + + def test_delete_flow_classifier(self): + client = self.app.client_manager.network + mock_flow_classifier_delete = client.delete_sfc_flow_classifier + arglist = [ + self._flow_classifier[0]['id'], + ] + verifylist = [ + ('flow_classifier', [self._flow_classifier[0]['id']]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + mock_flow_classifier_delete.assert_called_once_with( + self._flow_classifier[0]['id']) + self.assertIsNone(result) + + def test_delete_multiple_flow_classifiers_with_exception(self): + client = self.app.client_manager.network + target1 = self._flow_classifier[0]['id'] + arglist = [target1] + verifylist = [('flow_classifier', [target1])] + + client.find_sfc_flow_classifier.side_effect = [ + target1, exceptions.CommandError + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + msg = "1 of 2 flow classifier(s) failed to delete." + with testtools.ExpectedException(exceptions.CommandError) as e: + self.cmd.take_action(parsed_args) + self.assertEqual(msg, str(e)) + + +class TestSetSfcFlowClassifier(fakes.TestNeutronClientOSCV2): + _flow_classifier = fakes.FakeSfcFlowClassifier.create_flow_classifier() + _flow_classifier_name = _flow_classifier['name'] + _flow_classifier_id = _flow_classifier['id'] + + def setUp(self): + super(TestSetSfcFlowClassifier, self).setUp() + self.network.update_sfc_flow_classifier = mock.Mock( + return_value=None) + self.cmd = sfc_flow_classifier.SetSfcFlowClassifier(self.app, + self.namespace) + + def test_set_flow_classifier(self): + client = self.app.client_manager.network + mock_flow_classifier_update = client.update_sfc_flow_classifier + arglist = [ + self._flow_classifier_name, + '--name', 'name_updated', + '--description', 'desc_updated' + ] + verifylist = [ + ('flow_classifier', self._flow_classifier_name), + ('name', 'name_updated'), + ('description', 'desc_updated'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'name': 'name_updated', + 'description': 'desc_updated'} + mock_flow_classifier_update.assert_called_once_with( + self._flow_classifier_name, **attrs) + self.assertIsNone(result) + + +class TestShowSfcFlowClassifier(fakes.TestNeutronClientOSCV2): + + _fc = fakes.FakeSfcFlowClassifier.create_flow_classifier() + data = ( + _fc['description'], + _fc['destination_ip_prefix'], + _fc['destination_port_range_max'], + _fc['destination_port_range_min'], + _fc['ethertype'], + _fc['id'], + _fc['l7_parameters'], + _fc['logical_destination_port'], + _fc['logical_source_port'], + _fc['name'], + _fc['project_id'], + _fc['protocol'], + _fc['source_ip_prefix'], + _fc['source_port_range_max'], + _fc['source_port_range_min'] + ) + _flow_classifier = _fc + _flow_classifier_id = _fc['id'] + columns = ('Description', + 'Destination IP', + 'Destination Port Range Max', + 'Destination Port Range Min', + 'Ethertype', + 'ID', + 'L7 Parameters', + 'Logical Destination Port', + 'Logical Source Port', + 'Name', + 'Project', + 'Protocol', + 'Source IP', + 'Source Port Range Max', + 'Source Port Range Min', + 'Summary',) + + def setUp(self): + super(TestShowSfcFlowClassifier, self).setUp() + self.network.get_sfc_flow_classifier = mock.Mock( + return_value=self._flow_classifier + ) + # Get the command object to test + self.cmd = sfc_flow_classifier.ShowSfcFlowClassifier(self.app, + self.namespace) + + def test_show_flow_classifier(self): + client = self.app.client_manager.network + mock_flow_classifier_show = client.get_sfc_flow_classifier + arglist = [ + self._flow_classifier_id, + ] + verifylist = [ + ('flow_classifier', self._flow_classifier_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + mock_flow_classifier_show.assert_called_once_with( + self._flow_classifier_id) + self.assertEqual(self.columns, columns) + + +class TestListSfcFlowClassifier(fakes.TestNeutronClientOSCV2): + + _fc = fakes.FakeSfcFlowClassifier.create_flow_classifiers(count=1) + + columns = ('ID', 'Name', 'Summary') + + columns_long = ('ID', 'Name', 'Protocol', 'Ethertype', 'Source IP', + 'Destination IP', 'Logical Source Port', + 'Logical Destination Port', 'Source Port Range Min', + 'Source Port Range Max', 'Destination Port Range Min', + 'Destination Port Range Max', 'L7 Parameters', + 'Description', 'Project') + _flow_classifier = _fc[0] + data = [ + _flow_classifier['id'], + _flow_classifier['name'], + _flow_classifier['protocol'], + _flow_classifier['source_ip_prefix'], + _flow_classifier['destination_ip_prefix'], + _flow_classifier['logical_source_port'], + _flow_classifier['logical_destination_port'] + ] + data_long = [ + _flow_classifier['id'], + _flow_classifier['name'], + _flow_classifier['protocol'], + _flow_classifier['ethertype'], + _flow_classifier['source_ip_prefix'], + _flow_classifier['destination_ip_prefix'], + _flow_classifier['logical_source_port'], + _flow_classifier['logical_destination_port'], + _flow_classifier['source_port_range_min'], + _flow_classifier['source_port_range_max'], + _flow_classifier['destination_port_range_min'], + _flow_classifier['destination_port_range_max'], + _flow_classifier['l7_parameters'], + _flow_classifier['description'] + ] + + _flow_classifier1 = {'flow_classifiers': _flow_classifier} + _flow_classifier_id = _flow_classifier['id'] + + def setUp(self): + super(TestListSfcFlowClassifier, self).setUp() + self.network.sfc_flow_classifiers = mock.Mock( + return_value=self._fc + ) + # Get the command object to test + self.cmd = sfc_flow_classifier.ListSfcFlowClassifier(self.app, + self.namespace) + + def test_list_flow_classifiers(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns = self.cmd.take_action(parsed_args) + fcs = self.network.sfc_flow_classifiers() + fc = fcs[0] + data = [ + fc['id'], + fc['name'], + fc['protocol'], + fc['source_ip_prefix'], + fc['destination_ip_prefix'], + fc['logical_source_port'], + fc['logical_destination_port'] + ] + self.assertEqual(list(self.columns), columns[0]) + self.assertEqual(self.data, data) + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + fcs = self.network.sfc_flow_classifiers() + fc = fcs[0] + data = [ + fc['id'], + fc['name'], + fc['protocol'], + fc['ethertype'], + fc['source_ip_prefix'], + fc['destination_ip_prefix'], + fc['logical_source_port'], + fc['logical_destination_port'], + fc['source_port_range_min'], + fc['source_port_range_max'], + fc['destination_port_range_min'], + fc['destination_port_range_max'], + fc['l7_parameters'], + fc['description'] + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns_long = self.cmd.take_action(parsed_args)[0] + self.assertEqual(list(self.columns_long), columns_long) + self.assertEqual(self.data_long, data) diff --git a/neutronclient/tests/unit/osc/v2/sfc/test_port_chain.py b/neutronclient/tests/unit/osc/v2/sfc/test_port_chain.py new file mode 100755 index 000000000..15f7a431e --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/sfc/test_port_chain.py @@ -0,0 +1,499 @@ +# Copyright (c) 2017 Huawei Technologies India Pvt.Limited. +# 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 unittest import mock + +from osc_lib import exceptions +import testtools + +from neutronclient.osc.v2.sfc import sfc_port_chain +from neutronclient.tests.unit.osc.v2.sfc import fakes + + +def _get_id(client, id_or_name, resource): + return id_or_name + + +class TestCreateSfcPortChain(fakes.TestNeutronClientOSCV2): + # The new port_chain created + _port_chain = fakes.FakeSfcPortChain.create_port_chain() + + columns = ('Chain Parameters', + 'Description', + 'Flow Classifiers', + 'ID', + 'Name', + 'Port Pair Groups', + 'Project') + + def get_data(self): + return ( + self._port_chain['chain_parameters'], + self._port_chain['description'], + self._port_chain['flow_classifiers'], + self._port_chain['id'], + self._port_chain['name'], + self._port_chain['port_pair_groups'], + self._port_chain['project_id'], + ) + + def setUp(self): + super(TestCreateSfcPortChain, self).setUp() + self.network.create_sfc_port_chain = mock.Mock( + return_value=self._port_chain) + self.data = self.get_data() + + # Get the command object to test + self.cmd = sfc_port_chain.CreateSfcPortChain(self.app, self.namespace) + + def test_create_port_chain_default_options(self): + arglist = [ + self._port_chain['name'], + "--port-pair-group", self._port_chain['port_pair_groups'] + ] + verifylist = [ + ('name', self._port_chain['name']), + ('port_pair_groups', [self._port_chain['port_pair_groups']]), + ('flow_classifiers', []), + ('chain_parameters', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_sfc_port_chain.assert_called_once_with( + **{ + 'name': self._port_chain['name'], + 'port_pair_groups': [self._port_chain['port_pair_groups']] + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_port_chain_all_options(self): + arglist = [ + "--description", self._port_chain['description'], + "--port-pair-group", self._port_chain['port_pair_groups'], + self._port_chain['name'], + "--flow-classifier", self._port_chain['flow_classifiers'], + "--chain-parameters", 'correlation=mpls,symmetric=true', + ] + + cp = {'correlation': 'mpls', 'symmetric': 'true'} + + verifylist = [ + ('port_pair_groups', [self._port_chain['port_pair_groups']]), + ('name', self._port_chain['name']), + ('description', self._port_chain['description']), + ('flow_classifiers', [self._port_chain['flow_classifiers']]), + ('chain_parameters', [cp]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_sfc_port_chain.assert_called_once_with( + **{ + 'name': self._port_chain['name'], + 'port_pair_groups': [self._port_chain['port_pair_groups']], + 'description': self._port_chain['description'], + 'flow_classifiers': [self._port_chain['flow_classifiers']], + 'chain_parameters': cp + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteSfcPortChain(fakes.TestNeutronClientOSCV2): + + _port_chain = fakes.FakeSfcPortChain.create_port_chains(count=1) + + def setUp(self): + super(TestDeleteSfcPortChain, self).setUp() + self.network.delete_sfc_port_chain = mock.Mock(return_value=None) + self.cmd = sfc_port_chain.DeleteSfcPortChain(self.app, self.namespace) + + def test_delete_port_chain(self): + client = self.app.client_manager.network + mock_port_chain_delete = client.delete_sfc_port_chain + arglist = [ + self._port_chain[0]['id'], + ] + verifylist = [ + ('port_chain', [self._port_chain[0]['id']]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + mock_port_chain_delete.assert_called_once_with( + self._port_chain[0]['id']) + self.assertIsNone(result) + + def test_delete_multiple_port_chains_with_exception(self): + client = self.app.client_manager.network + target1 = self._port_chain[0]['id'] + arglist = [target1] + verifylist = [('port_chain', [target1])] + + client.find_sfc_port_chain.side_effect = [ + target1, exceptions.CommandError + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + msg = "1 of 2 port chain(s) failed to delete." + with testtools.ExpectedException(exceptions.CommandError) as e: + self.cmd.take_action(parsed_args) + self.assertEqual(msg, str(e)) + + +class TestListSfcPortChain(fakes.TestNeutronClientOSCV2): + _port_chains = fakes.FakeSfcPortChain.create_port_chains(count=1) + columns = ('ID', 'Name', 'Port Pair Groups', 'Flow Classifiers', + 'Chain Parameters') + columns_long = ('ID', 'Name', 'Port Pair Groups', 'Flow Classifiers', + 'Chain Parameters', 'Description', 'Project') + _port_chain = _port_chains[0] + data = [ + _port_chain['id'], + _port_chain['name'], + _port_chain['port_pair_groups'], + _port_chain['flow_classifiers'], + _port_chain['chain_parameters'], + ] + data_long = [ + _port_chain['id'], + _port_chain['name'], + _port_chain['project_id'], + _port_chain['port_pair_groups'], + _port_chain['flow_classifiers'], + _port_chain['chain_parameters'], + _port_chain['description'] + ] + _port_chain1 = {'port_chains': _port_chain} + _port_chain_id = _port_chain['id'] + + def setUp(self): + super(TestListSfcPortChain, self).setUp() + self.network.sfc_port_chains = mock.Mock( + return_value=self._port_chains + ) + # Get the command object to test + self.cmd = sfc_port_chain.ListSfcPortChain(self.app, self.namespace) + + def test_list_port_chains(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns = self.cmd.take_action(parsed_args)[0] + pcs = self.network.sfc_port_chains() + pc = pcs[0] + data = [ + pc['id'], + pc['name'], + pc['port_pair_groups'], + pc['flow_classifiers'], + pc['chain_parameters'], + ] + self.assertEqual(list(self.columns), columns) + self.assertEqual(self.data, data) + + def test_list_port_chain_with_long_opion(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns = self.cmd.take_action(parsed_args)[0] + pcs = self.network.sfc_port_chains() + pc = pcs[0] + data = [ + pc['id'], + pc['name'], + pc['project_id'], + pc['port_pair_groups'], + pc['flow_classifiers'], + pc['chain_parameters'], + pc['description'] + ] + self.assertEqual(list(self.columns_long), columns) + self.assertEqual(self.data_long, data) + + +class TestSetSfcPortChain(fakes.TestNeutronClientOSCV2): + _port_chain = fakes.FakeSfcPortChain.create_port_chain() + resource = _port_chain + res = 'port_chain' + _port_chain_name = _port_chain['name'] + _port_chain_id = _port_chain['id'] + pc_ppg = _port_chain['port_pair_groups'] + pc_fc = _port_chain['flow_classifiers'] + + def setUp(self): + super(TestSetSfcPortChain, self).setUp() + self.mocked = self.network.update_sfc_port_chain + self.cmd = sfc_port_chain.SetSfcPortChain(self.app, self.namespace) + + def test_set_port_chain(self): + client = self.app.client_manager.network + mock_port_chain_update = client.update_sfc_port_chain + arglist = [ + self._port_chain_name, + '--name', 'name_updated', + '--description', 'desc_updated', + ] + verifylist = [ + ('port_chain', self._port_chain_name), + ('name', 'name_updated'), + ('description', 'desc_updated'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = {'name': 'name_updated', 'description': 'desc_updated'} + mock_port_chain_update.assert_called_once_with(self._port_chain_name, + **attrs) + self.assertIsNone(result) + + def test_set_flow_classifiers(self): + target = self.resource['id'] + fc1 = 'flow_classifier1' + fc2 = 'flow_classifier2' + + self.network.find_sfc_port_chain = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id, 'flow_classifiers': [self.pc_fc]} + ) + self.network.find_sfc_flow_classifier.side_effect = \ + lambda name_or_id, ignore_missing=False: {'id': name_or_id} + arglist = [ + target, + '--flow-classifier', fc1, + '--flow-classifier', fc2, + ] + verifylist = [ + (self.res, target), + ('flow_classifiers', [fc1, fc2]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + expect = {'flow_classifiers': [self.pc_fc, fc1, fc2]} + self.mocked.assert_called_once_with(target, **expect) + self.assertIsNone(result) + + def test_set_no_flow_classifier(self): + client = self.app.client_manager.network + mock_port_chain_update = client.update_sfc_port_chain + arglist = [ + self._port_chain_name, + '--no-flow-classifier', + ] + verifylist = [ + ('port_chain', self._port_chain_name), + ('no_flow_classifier', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = {'flow_classifiers': []} + mock_port_chain_update.assert_called_once_with(self._port_chain_name, + **attrs) + self.assertIsNone(result) + + def test_set_port_pair_groups(self): + target = self.resource['id'] + existing_ppg = self.pc_ppg + ppg1 = 'port_pair_group1' + ppg2 = 'port_pair_group2' + + self.network.find_sfc_port_chain = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id, 'port_pair_groups': [self.pc_ppg]} + ) + arglist = [ + target, + '--port-pair-group', ppg1, + '--port-pair-group', ppg2, + ] + verifylist = [ + (self.res, target), + ('port_pair_groups', [ppg1, ppg2]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + expect = {'port_pair_groups': [existing_ppg, ppg1, ppg2]} + self.mocked.assert_called_once_with(target, **expect) + self.assertIsNone(result) + + def test_set_no_port_pair_group(self): + target = self.resource['id'] + ppg1 = 'port_pair_group1' + + arglist = [ + target, + '--no-port-pair-group', + '--port-pair-group', ppg1, + ] + verifylist = [ + (self.res, target), + ('no_port_pair_group', True), + ('port_pair_groups', [ppg1]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + expect = {'port_pair_groups': [ppg1]} + self.mocked.assert_called_once_with(target, **expect) + self.assertIsNone(result) + + def test_set_only_no_port_pair_group(self): + target = self.resource['id'] + arglist = [ + target, + '--no-port-pair-group', + ] + verifylist = [ + (self.res, target), + ('no_port_pair_group', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args) + + +class TestShowSfcPortChain(fakes.TestNeutronClientOSCV2): + + _pc = fakes.FakeSfcPortChain.create_port_chain() + data = ( + _pc['chain_parameters'], + _pc['description'], + _pc['flow_classifiers'], + _pc['id'], + _pc['name'], + _pc['port_pair_groups'], + _pc['project_id'] + ) + _port_chain = _pc + _port_chain_id = _pc['id'] + columns = ('Chain Parameters', + 'Description', + 'Flow Classifiers', + 'ID', + 'Name', + 'Port Pair Groups', + 'Project') + + def setUp(self): + super(TestShowSfcPortChain, self).setUp() + self.network.get_sfc_port_chain = mock.Mock( + return_value=self._port_chain + ) + # Get the command object to test + self.cmd = sfc_port_chain.ShowSfcPortChain(self.app, self.namespace) + + def test_show_port_chain(self): + client = self.app.client_manager.network + mock_port_chain_show = client.get_sfc_port_chain + arglist = [ + self._port_chain_id, + ] + verifylist = [ + ('port_chain', self._port_chain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + mock_port_chain_show.assert_called_once_with(self._port_chain_id) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestUnsetSfcPortChain(fakes.TestNeutronClientOSCV2): + _port_chain = fakes.FakeSfcPortChain.create_port_chain() + resource = _port_chain + res = 'port_chain' + _port_chain_name = _port_chain['name'] + _port_chain_id = _port_chain['id'] + pc_ppg = _port_chain['port_pair_groups'] + pc_fc = _port_chain['flow_classifiers'] + + def setUp(self): + super(TestUnsetSfcPortChain, self).setUp() + self.network.update_sfc_port_chain = mock.Mock( + return_value=None) + self.mocked = self.network.update_sfc_port_chain + self.cmd = sfc_port_chain.UnsetSfcPortChain(self.app, self.namespace) + + def test_unset_port_pair_group(self): + target = self.resource['id'] + ppg1 = 'port_pair_group1' + + self.network.find_sfc_port_chain = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id, 'port_pair_groups': [self.pc_ppg]} + ) + self.network.find_sfc_port_pair_group.side_effect = \ + lambda name_or_id, ignore_missing=False: {'id': name_or_id} + + arglist = [ + target, + '--port-pair-group', ppg1, + ] + verifylist = [ + (self.res, target), + ('port_pair_groups', [ppg1]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + expect = {'port_pair_groups': [self.pc_ppg]} + self.mocked.assert_called_once_with(target, **expect) + self.assertIsNone(result) + + def test_unset_flow_classifier(self): + target = self.resource['id'] + fc1 = 'flow_classifier1' + self.network.find_sfc_port_chain = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id, 'flow_classifiers': [self.pc_fc]} + ) + self.network.find_sfc_flow_classifier.side_effect = \ + lambda name_or_id, ignore_missing=False: {'id': name_or_id} + + arglist = [ + target, + '--flow-classifier', fc1, + ] + verifylist = [ + (self.res, target), + ('flow_classifiers', [fc1]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + expect = {'flow_classifiers': [self.pc_fc]} + self.mocked.assert_called_once_with(target, **expect) + self.assertIsNone(result) + + def test_unset_all_flow_classifier(self): + client = self.app.client_manager.network + target = self.resource['id'] + mock_port_chain_update = client.update_sfc_port_chain + arglist = [ + target, + '--all-flow-classifier', + ] + verifylist = [ + (self.res, target), + ('all_flow_classifier', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + expect = {'flow_classifiers': []} + mock_port_chain_update.assert_called_once_with(target, + **expect) + self.assertIsNone(result) diff --git a/neutronclient/tests/unit/osc/v2/sfc/test_port_pair.py b/neutronclient/tests/unit/osc/v2/sfc/test_port_pair.py new file mode 100755 index 000000000..e0610a360 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/sfc/test_port_pair.py @@ -0,0 +1,313 @@ +# Copyright (c) 2017 Huawei Technologies India Pvt.Limited. +# 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 unittest import mock + +from osc_lib import exceptions +import testtools + +from neutronclient.osc.v2.sfc import sfc_port_pair +from neutronclient.tests.unit.osc.v2.sfc import fakes + + +class TestCreateSfcPortPair(fakes.TestNeutronClientOSCV2): + # The new port_pair created + _port_pair = fakes.FakeSfcPortPair.create_port_pair() + + columns = ('Description', + 'Egress Logical Port', + 'ID', + 'Ingress Logical Port', + 'Name', + 'Project', + 'Service Function Parameters') + + def get_data(self): + return ( + self._port_pair['description'], + self._port_pair['egress'], + self._port_pair['id'], + self._port_pair['ingress'], + self._port_pair['name'], + self._port_pair['project_id'], + self._port_pair['service_function_parameters'] + ) + + def setUp(self): + super(TestCreateSfcPortPair, self).setUp() + self.network.create_sfc_port_pair = mock.Mock( + return_value=self._port_pair) + self.data = self.get_data() + + # Get the command object to test + self.cmd = sfc_port_pair.CreateSfcPortPair(self.app, self.namespace) + + def test_create_port_pair_default_options(self): + arglist = [ + "--ingress", self._port_pair['ingress'], + "--egress", self._port_pair['egress'], + self._port_pair['name'], + ] + verifylist = [ + ('ingress', self._port_pair['ingress']), + ('egress', self._port_pair['egress']), + ('name', self._port_pair['name']) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_sfc_port_pair.assert_called_once_with( + **{'name': self._port_pair['name'], + 'ingress': self._port_pair['ingress'], + 'egress': self._port_pair['egress']} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def _test_create_port_pair_all_options(self, correlation): + arglist = [ + "--description", self._port_pair['description'], + "--egress", self._port_pair['egress'], + "--ingress", self._port_pair['ingress'], + self._port_pair['name'], + "--service-function-parameters", + 'correlation=%s,weight=1' % correlation, + ] + + verifylist = [ + ('ingress', self._port_pair['ingress']), + ('egress', self._port_pair['egress']), + ('name', self._port_pair['name']), + ('description', self._port_pair['description']), + ('service_function_parameters', + [{'correlation': correlation, 'weight': '1'}]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + if correlation == "None": + correlation_param = None + else: + correlation_param = correlation + self.network.create_sfc_port_pair.assert_called_once_with( + **{'name': self._port_pair['name'], + 'ingress': self._port_pair['ingress'], + 'egress': self._port_pair['egress'], + 'description': self._port_pair['description'], + 'service_function_parameters': + {'correlation': correlation_param, 'weight': '1'}} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_port_pair_all_options(self): + self._test_create_port_pair_all_options('None') + + def test_create_port_pair_all_options_mpls(self): + self._test_create_port_pair_all_options('mpls') + + +class TestDeleteSfcPortPair(fakes.TestNeutronClientOSCV2): + + _port_pair = fakes.FakeSfcPortPair.create_port_pairs(count=1) + + def setUp(self): + super(TestDeleteSfcPortPair, self).setUp() + self.network.delete_sfc_port_pair = mock.Mock(return_value=None) + self.cmd = sfc_port_pair.DeleteSfcPortPair(self.app, self.namespace) + + def test_delete_port_pair(self): + client = self.app.client_manager.network + mock_port_pair_delete = client.delete_sfc_port_pair + arglist = [ + self._port_pair[0]['id'], + ] + verifylist = [ + ('port_pair', [self._port_pair[0]['id']]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + mock_port_pair_delete.assert_called_once_with( + self._port_pair[0]['id']) + self.assertIsNone(result) + + def test_delete_multiple_port_pairs_with_exception(self): + client = self.app.client_manager.network + target1 = self._port_pair[0]['id'] + arglist = [target1] + verifylist = [('port_pair', [target1])] + + client.find_sfc_port_pair.side_effect = [ + target1, exceptions.CommandError + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + msg = "1 of 2 port pair(s) failed to delete." + with testtools.ExpectedException(exceptions.CommandError) as e: + self.cmd.take_action(parsed_args) + self.assertEqual(msg, str(e)) + + +class TestListSfcPortPair(fakes.TestNeutronClientOSCV2): + _port_pairs = fakes.FakeSfcPortPair.create_port_pairs() + columns = ('ID', 'Name', 'Ingress Logical Port', 'Egress Logical Port') + columns_long = ('ID', 'Name', 'Ingress Logical Port', + 'Egress Logical Port', 'Service Function Parameters', + 'Description', 'Project') + _port_pair = _port_pairs[0] + data = [ + _port_pair['id'], + _port_pair['name'], + _port_pair['ingress'], + _port_pair['egress'] + ] + data_long = [ + _port_pair['id'], + _port_pair['name'], + _port_pair['ingress'], + _port_pair['egress'], + _port_pair['service_function_parameters'], + _port_pair['description'] + ] + _port_pair1 = {'port_pairs': _port_pair} + _port_pair_id = _port_pair['id'], + + def setUp(self): + super(TestListSfcPortPair, self).setUp() + self.network.sfc_port_pairs = mock.Mock( + return_value=self._port_pairs) + # Get the command object to test + self.cmd = sfc_port_pair.ListSfcPortPair(self.app, self.namespace) + + def test_list_port_pairs(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns = self.cmd.take_action(parsed_args)[0] + port_pairs = self.network.sfc_port_pairs() + port_pair = port_pairs[0] + data = [ + port_pair['id'], + port_pair['name'], + port_pair['ingress'], + port_pair['egress'] + ] + self.assertEqual(list(self.columns), columns) + self.assertEqual(self.data, data) + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + port_pairs = self.network.sfc_port_pairs() + port_pair = port_pairs[0] + data = [ + port_pair['id'], + port_pair['name'], + port_pair['ingress'], + port_pair['egress'], + port_pair['service_function_parameters'], + port_pair['description'] + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns_long = self.cmd.take_action(parsed_args)[0] + self.assertEqual(list(self.columns_long), columns_long) + self.assertEqual(self.data_long, data) + + +class TestSetSfcPortPair(fakes.TestNeutronClientOSCV2): + _port_pair = fakes.FakeSfcPortPair.create_port_pair() + _port_pair_name = _port_pair['name'] + _port_pair_id = _port_pair['id'] + + def setUp(self): + super(TestSetSfcPortPair, self).setUp() + self.network.update_sfc_port_pair = mock.Mock(return_value=None) + self.cmd = sfc_port_pair.SetSfcPortPair(self.app, self.namespace) + + def test_set_port_pair(self): + client = self.app.client_manager.network + mock_port_pair_update = client.update_sfc_port_pair + arglist = [ + self._port_pair_name, + '--name', 'name_updated', + '--description', 'desc_updated' + ] + verifylist = [ + ('port_pair', self._port_pair_name), + ('name', 'name_updated'), + ('description', 'desc_updated'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'name': 'name_updated', + 'description': 'desc_updated' + } + mock_port_pair_update.assert_called_once_with(self._port_pair_name, + **attrs) + self.assertIsNone(result) + + +class TestShowSfcPortPair(fakes.TestNeutronClientOSCV2): + + _pp = fakes.FakeSfcPortPair.create_port_pair() + + data = ( + _pp['description'], + _pp['egress'], + _pp['id'], + _pp['ingress'], + _pp['name'], + _pp['project_id'], + _pp['service_function_parameters'], + ) + _port_pair = _pp + _port_pair_id = _pp['id'] + columns = ( + 'Description', + 'Egress Logical Port', + 'ID', + 'Ingress Logical Port', + 'Name', + 'Project', + 'Service Function Parameters' + ) + + def setUp(self): + super(TestShowSfcPortPair, self).setUp() + + self.network.get_sfc_port_pair = mock.Mock( + return_value=self._port_pair + ) + + # Get the command object to test + self.cmd = sfc_port_pair.ShowSfcPortPair(self.app, self.namespace) + + def test_show_port_pair(self): + client = self.app.client_manager.network + mock_port_pair_show = client.get_sfc_port_pair + arglist = [ + self._port_pair_id, + ] + verifylist = [ + ('port_pair', self._port_pair_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + mock_port_pair_show.assert_called_once_with(self._port_pair_id) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/neutronclient/tests/unit/osc/v2/sfc/test_port_pair_group.py b/neutronclient/tests/unit/osc/v2/sfc/test_port_pair_group.py new file mode 100755 index 000000000..49f3fcf06 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/sfc/test_port_pair_group.py @@ -0,0 +1,433 @@ +# Copyright (c) 2017 Huawei Technologies India Pvt.Limited. +# 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 unittest import mock + +from osc_lib import exceptions +import testtools + +from neutronclient.osc.v2.sfc import sfc_port_pair_group +from neutronclient.tests.unit.osc.v2.sfc import fakes + + +class TestCreateSfcPortPairGroup(fakes.TestNeutronClientOSCV2): + + _port_pair_group = fakes.FakeSfcPortPairGroup.create_port_pair_group() + + columns = ('Description', + 'ID', + 'Name', + 'Port Pair', + 'Port Pair Group Parameters', + 'Project', + 'Tap Enabled') + + def get_data(self, ppg): + return ( + ppg['description'], + ppg['id'], + ppg['name'], + ppg['port_pairs'], + ppg['port_pair_group_parameters'], + ppg['project_id'], + ppg['tap_enabled'] + ) + + def setUp(self): + super(TestCreateSfcPortPairGroup, self).setUp() + self.network.create_sfc_port_pair_group = mock.Mock( + return_value=self._port_pair_group) + self.data = self.get_data(self._port_pair_group) + + # Get the command object to test + self.cmd = sfc_port_pair_group.CreateSfcPortPairGroup(self.app, + self.namespace) + + def test_create_port_pair_group_default_options(self): + arglist = [ + "--port-pair", self._port_pair_group['port_pairs'], + self._port_pair_group['name'], + ] + verifylist = [ + ('port_pairs', [self._port_pair_group['port_pairs']]), + ('name', self._port_pair_group['name']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + self.network.create_sfc_port_pair_group.assert_called_once_with( + **{'name': self._port_pair_group['name'], + 'port_pairs': [self._port_pair_group['port_pairs']]}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_port_pair_group(self): + arglist = [ + "--description", self._port_pair_group['description'], + "--port-pair", self._port_pair_group['port_pairs'], + self._port_pair_group['name'], + ] + verifylist = [ + ('port_pairs', [self._port_pair_group['port_pairs']]), + ('name', self._port_pair_group['name']), + ('description', self._port_pair_group['description']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_sfc_port_pair_group.assert_called_once_with( + **{ + 'name': self._port_pair_group['name'], + 'port_pairs': [self._port_pair_group['port_pairs']], + 'description': self._port_pair_group['description']}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_tap_enabled_port_pair_group(self): + arglist = [ + "--description", self._port_pair_group['description'], + "--port-pair", self._port_pair_group['port_pairs'], + self._port_pair_group['name'], + "--enable-tap" + ] + verifylist = [ + ('port_pairs', [self._port_pair_group['port_pairs']]), + ('name', self._port_pair_group['name']), + ('description', self._port_pair_group['description']), + ('enable_tap', True) + ] + + expected_data = self._update_expected_response_data( + data={ + 'tap_enabled': True + } + ) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_sfc_port_pair_group.assert_called_once_with( + **{ + 'name': self._port_pair_group['name'], + 'port_pairs': [self._port_pair_group['port_pairs']], + 'description': self._port_pair_group['description'], + 'tap_enabled': True}) + self.assertEqual(self.columns, columns) + self.assertEqual(expected_data, data) + + def _update_expected_response_data(self, data): + # REVISIT(vks1) - This method can be common for other test functions. + ppg = fakes.FakeSfcPortPairGroup.create_port_pair_group(data) + self.network.create_sfc_port_pair_group.return_value = ppg + return self.get_data(ppg) + + +class TestDeleteSfcPortPairGroup(fakes.TestNeutronClientOSCV2): + + _port_pair_group = (fakes.FakeSfcPortPairGroup.create_port_pair_groups + (count=1)) + + def setUp(self): + super(TestDeleteSfcPortPairGroup, self).setUp() + self.network.delete_sfc_port_pair_group = mock.Mock( + return_value=None) + self.cmd = sfc_port_pair_group.DeleteSfcPortPairGroup(self.app, + self.namespace) + + def test_delete_port_pair_group(self): + client = self.app.client_manager.network + mock_port_pair_group_delete = client.delete_sfc_port_pair_group + arglist = [ + self._port_pair_group[0]['id'], + ] + verifylist = [ + ('port_pair_group', [self._port_pair_group[0]['id']]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + mock_port_pair_group_delete.assert_called_once_with( + self._port_pair_group[0]['id']) + self.assertIsNone(result) + + def test_delete_multiple_port_pair_groups_with_exception(self): + client = self.app.client_manager.network + target1 = 'target' + arglist = [target1] + verifylist = [('port_pair_group', [target1])] + + client.find_sfc_port_pair_group.side_effect = [ + target1, exceptions.CommandError + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + msg = "1 of 2 port pair group(s) failed to delete." + with testtools.ExpectedException(exceptions.CommandError) as e: + self.cmd.take_action(parsed_args) + self.assertEqual(msg, str(e)) + + +class TestListSfcPortPairGroup(fakes.TestNeutronClientOSCV2): + _ppgs = fakes.FakeSfcPortPairGroup.create_port_pair_groups(count=1) + columns = ('ID', 'Name', 'Port Pair', 'Port Pair Group Parameters', + 'Tap Enabled') + columns_long = ('ID', 'Name', 'Port Pair', 'Port Pair Group Parameters', + 'Description', 'Project', 'Tap Enabled') + _port_pair_group = _ppgs[0] + data = [ + _port_pair_group['id'], + _port_pair_group['name'], + _port_pair_group['port_pairs'], + _port_pair_group['port_pair_group_parameters'], + _port_pair_group['tap_enabled'] + ] + data_long = [ + _port_pair_group['id'], + _port_pair_group['name'], + _port_pair_group['port_pairs'], + _port_pair_group['port_pair_group_parameters'], + _port_pair_group['description'], + _port_pair_group['tap_enabled'] + ] + _port_pair_group1 = {'port_pair_groups': _port_pair_group} + _port_pair_id = _port_pair_group['id'] + + def setUp(self): + super(TestListSfcPortPairGroup, self).setUp() + + self.network.sfc_port_pair_groups = mock.Mock( + return_value=self._ppgs + ) + # Get the command object to test + self.cmd = sfc_port_pair_group.ListSfcPortPairGroup(self.app, + self.namespace) + + def test_list_port_pair_groups(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns = self.cmd.take_action(parsed_args)[0] + ppgs = self.network.sfc_port_pair_groups() + ppg = ppgs[0] + data = [ + ppg['id'], + ppg['name'], + ppg['port_pairs'], + ppg['port_pair_group_parameters'], + ppg['tap_enabled'] + ] + self.assertEqual(list(self.columns), columns) + self.assertEqual(self.data, data) + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + ppgs = self.network.sfc_port_pair_groups() + ppg = ppgs[0] + data = [ + ppg['id'], + ppg['name'], + ppg['port_pairs'], + ppg['port_pair_group_parameters'], + ppg['description'], + ppg['tap_enabled'] + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns_long = self.cmd.take_action(parsed_args)[0] + self.assertEqual(list(self.columns_long), columns_long) + self.assertEqual(self.data_long, data) + + +class TestSetSfcPortPairGroup(fakes.TestNeutronClientOSCV2): + _port_pair_group = fakes.FakeSfcPortPairGroup.create_port_pair_group() + resource = _port_pair_group + res = 'port_pair_group' + _port_pair_group_name = _port_pair_group['name'] + ppg_pp = _port_pair_group['port_pairs'] + _port_pair_group_id = _port_pair_group['id'] + + def setUp(self): + super(TestSetSfcPortPairGroup, self).setUp() + + self.network.update_sfc_port_pair_group = mock.Mock( + return_value=None) + self.mocked = self.network.update_sfc_port_pair_group + self.cmd = sfc_port_pair_group.SetSfcPortPairGroup(self.app, + self.namespace) + + def test_set_port_pair_group(self): + target = self.resource['id'] + port_pair1 = 'additional_port1' + port_pair2 = 'additional_port2' + + self.network.find_sfc_port_pair = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id} + ) + self.network.find_sfc_port_pair_group = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id, 'port_pairs': self.ppg_pp} + ) + + arglist = [ + target, + '--port-pair', port_pair1, + '--port-pair', port_pair2, + ] + verifylist = [ + (self.res, target), + ('port_pairs', [port_pair1, port_pair2]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + expect = {'port_pairs': sorted([*self.ppg_pp, port_pair1, port_pair2])} + self.mocked.assert_called_once_with(target, **expect) + self.assertIsNone(result) + + def test_set_no_port_pair(self): + client = self.app.client_manager.network + mock_port_pair_group_update = client.update_sfc_port_pair_group + arglist = [ + self._port_pair_group_name, + '--name', 'name_updated', + '--description', 'desc_updated', + '--no-port-pair', + ] + verifylist = [ + ('port_pair_group', self._port_pair_group_name), + ('name', 'name_updated'), + ('description', 'desc_updated'), + ('no_port_pair', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = {'name': 'name_updated', 'description': 'desc_updated', + 'port_pairs': []} + mock_port_pair_group_update.assert_called_once_with( + self._port_pair_group_name, **attrs) + self.assertIsNone(result) + + +class TestShowSfcPortPairGroup(fakes.TestNeutronClientOSCV2): + + _ppg = fakes.FakeSfcPortPairGroup.create_port_pair_group() + data = ( + _ppg['description'], + _ppg['id'], + _ppg['name'], + _ppg['port_pairs'], + _ppg['port_pair_group_parameters'], + _ppg['project_id'], + _ppg['tap_enabled']) + _port_pair_group = _ppg + _port_pair_group_id = _ppg['id'] + columns = ( + 'Description', + 'ID', + 'Name', + 'Port Pair', + 'Port Pair Group Parameters', + 'Project', + 'Tap Enabled' + ) + + def setUp(self): + super(TestShowSfcPortPairGroup, self).setUp() + + self.network.get_sfc_port_pair_group = mock.Mock( + return_value=self._port_pair_group + ) + self.cmd = sfc_port_pair_group.ShowSfcPortPairGroup(self.app, + self.namespace) + + def test_show_port_pair_group(self): + client = self.app.client_manager.network + mock_port_pair_group_show = client.get_sfc_port_pair_group + arglist = [ + self._port_pair_group_id, + ] + verifylist = [ + ('port_pair_group', self._port_pair_group_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + mock_port_pair_group_show.assert_called_once_with( + self._port_pair_group_id) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestUnsetSfcPortPairGroup(fakes.TestNeutronClientOSCV2): + _port_pair_group = fakes.FakeSfcPortPairGroup.create_port_pair_group() + resource = _port_pair_group + res = 'port_pair_group' + _port_pair_group_name = _port_pair_group['name'] + _port_pair_group_id = _port_pair_group['id'] + ppg_pp = _port_pair_group['port_pairs'] + + def setUp(self): + super(TestUnsetSfcPortPairGroup, self).setUp() + self.network.update_sfc_port_pair_group = mock.Mock( + return_value=None) + self.mocked = self.network.update_sfc_port_pair_group + self.cmd = sfc_port_pair_group.UnsetSfcPortPairGroup( + self.app, self.namespace) + + def test_unset_port_pair(self): + target = self.resource['id'] + port_pair1 = 'additional_port1' + port_pair2 = 'additional_port2' + + self.network.find_sfc_port_pair = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id} + ) + self.network.find_sfc_port_pair_group = mock.Mock( + side_effect=lambda name_or_id, ignore_missing=False: + {'id': name_or_id, 'port_pairs': self.ppg_pp} + ) + arglist = [ + target, + '--port-pair', port_pair1, + '--port-pair', port_pair2, + ] + verifylist = [ + (self.res, target), + ('port_pairs', [port_pair1, port_pair2]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + expect = {'port_pairs': sorted([*self.ppg_pp])} + self.mocked.assert_called_once_with(target, **expect) + self.assertIsNone(result) + + def test_unset_all_port_pair(self): + client = self.app.client_manager.network + mock_port_pair_group_update = client.update_sfc_port_pair_group + arglist = [ + self._port_pair_group_name, + '--all-port-pair', + ] + verifylist = [ + ('port_pair_group', self._port_pair_group_name), + ('all_port_pair', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = {'port_pairs': []} + mock_port_pair_group_update.assert_called_once_with( + self._port_pair_group_name, **attrs) + self.assertIsNone(result) diff --git a/neutronclient/tests/unit/osc/v2/sfc/test_service_graph.py b/neutronclient/tests/unit/osc/v2/sfc/test_service_graph.py new file mode 100644 index 000000000..167f1fcf8 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/sfc/test_service_graph.py @@ -0,0 +1,338 @@ +# Copyright 2017 Intel Corporation. +# +# 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 unittest import mock + +from osc_lib import exceptions +from osc_lib.tests import utils as tests_utils +import testtools + +from neutronclient.osc.v2.sfc import sfc_service_graph +from neutronclient.tests.unit.osc.v2.sfc import fakes + + +class TestListSfcServiceGraph(fakes.TestNeutronClientOSCV2): + _service_graphs = fakes.FakeSfcServiceGraph.create_sfc_service_graphs( + count=1) + columns = ('ID', 'Name', 'Branching Points') + columns_long = ('ID', 'Name', 'Branching Points', 'Description', 'Project') + _service_graph = _service_graphs[0] + data = [ + _service_graph['id'], + _service_graph['name'], + _service_graph['port_chains'] + ] + data_long = [ + _service_graph['id'], + _service_graph['name'], + _service_graph['port_chains'], + _service_graph['description'], + _service_graph['project_id'] + ] + _service_graph1 = {'service_graphs': _service_graph} + _service_graph_id = _service_graph['id'] + + def setUp(self): + super(TestListSfcServiceGraph, self).setUp() + self.network.sfc_service_graphs = mock.Mock( + return_value=self._service_graphs + ) + # Get the command object to test + self.cmd = sfc_service_graph.ListSfcServiceGraph( + self.app, self.namespace) + + def test_list_sfc_service_graphs(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns = self.cmd.take_action(parsed_args)[0] + sgs = self.network.sfc_service_graphs() + sg = sgs[0] + data = [ + sg['id'], + sg['name'], + sg['port_chains'] + ] + self.assertEqual(list(self.columns), columns) + self.assertEqual(self.data, data) + + def test_list_sfc_service_graphs_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns = self.cmd.take_action(parsed_args)[0] + sgs = self.network.sfc_service_graphs() + sg = sgs[0] + data = [ + sg['id'], + sg['name'], + sg['port_chains'], + sg['description'], + sg['project_id'] + ] + self.assertEqual(list(self.columns_long), columns) + self.assertEqual(self.data_long, data) + + +class TestCreateSfcServiceGraph(fakes.TestNeutronClientOSCV2): + _service_graph = fakes.FakeSfcServiceGraph.create_sfc_service_graph() + + columns = ('ID', 'Name', 'Branching Points') + columns_long = ('Branching Points', 'Description', 'ID', 'Name', 'Project') + + def get_data(self): + return ( + self._service_graph['port_chains'], + self._service_graph['description'], + self._service_graph['id'], + self._service_graph['name'], + self._service_graph['project_id'], + ) + + def setUp(self): + super(TestCreateSfcServiceGraph, self).setUp() + self.network.create_sfc_service_graph = mock.Mock( + return_value=self._service_graph) + self.data = self.get_data() + self.cmd = sfc_service_graph.CreateSfcServiceGraph( + self.app, self.namespace) + + def test_create_sfc_service_graph(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_sfc_service_graph_without_loop(self): + bp1_str = 'pc1:pc2,pc3' + bp2_str = 'pc2:pc4' + self.cmd = sfc_service_graph.CreateSfcServiceGraph( + self.app, self.namespace) + + arglist = [ + "--description", self._service_graph['description'], + "--branching-point", bp1_str, + "--branching-point", bp2_str, + self._service_graph['name']] + + pcs = {'pc1': ['pc2', 'pc3'], 'pc2': ['pc4']} + + verifylist = [ + ("description", self._service_graph['description']), + ("branching_points", [bp1_str, bp2_str]), + ("name", self._service_graph['name']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_sfc_service_graph.assert_called_once_with(**{ + 'description': self._service_graph['description'], + 'name': self._service_graph['name'], + 'port_chains': pcs + }) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data, data) + + def test_create_sfc_service_graph_with_loop(self): + bp1_str = 'pc1:pc2,pc3;' + bp2_str = 'pc2:pc1' + self.cmd = sfc_service_graph.CreateSfcServiceGraph( + self.app, self.namespace) + + arglist = [ + "--description", self._service_graph['description'], + "--branching-point", bp1_str, + "--branching-point", bp2_str, + self._service_graph['name']] + + verifylist = [ + ("description", self._service_graph['description']), + ("branching_points", [bp1_str, bp2_str]), + ("name", self._service_graph['name']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args) + + def test_create_sfc_service_graph_invalid_port_chains(self): + bp1_str = 'pc1:pc2,pc3:' + self.cmd = sfc_service_graph.CreateSfcServiceGraph( + self.app, self.namespace) + + arglist = [ + "--description", self._service_graph['description'], + "--branching-point", bp1_str, + self._service_graph['name']] + + verifylist = [ + ("description", self._service_graph['description']), + ("branching_points", [bp1_str]), + ("name", self._service_graph['name']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args) + + def test_create_sfc_service_graph_duplicate_src_chains(self): + bp1_str = 'pc1:pc2,pc3;' + bp2_str = 'pc1:pc4' + self.cmd = sfc_service_graph.CreateSfcServiceGraph( + self.app, self.namespace) + + arglist = [ + "--description", self._service_graph['description'], + "--branching-point", bp1_str, + "--branching-point", bp2_str, + self._service_graph['name']] + + verifylist = [ + ("description", self._service_graph['description']), + ("branching_points", [bp1_str, bp2_str]), + ("name", self._service_graph['name']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args) + + +class TestDeleteSfcServiceGraph(fakes.TestNeutronClientOSCV2): + + _service_graph = fakes.FakeSfcServiceGraph.create_sfc_service_graphs( + count=1) + + def setUp(self): + super(TestDeleteSfcServiceGraph, self).setUp() + self.network.delete_sfc_service_graph = mock.Mock( + return_value=None) + self.cmd = sfc_service_graph.DeleteSfcServiceGraph( + self.app, self.namespace) + + def test_delete_sfc_service_graph(self): + client = self.app.client_manager.network + mock_service_graph_delete = client.delete_sfc_service_graph + arglist = [ + self._service_graph[0]['id'], + ] + verifylist = [ + ('service_graph', [self._service_graph[0]['id']]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + mock_service_graph_delete.assert_called_once_with( + self._service_graph[0]['id']) + self.assertIsNone(result) + + def test_delete_multiple_service_graphs_with_exception(self): + client = self.app.client_manager.network + target = self._service_graph[0]['id'] + arglist = [target] + verifylist = [('service_graph', [target])] + + client.find_sfc_service_graph.side_effect = [ + target, exceptions.CommandError + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + msg = "1 of 2 service graph(s) failed to delete." + with testtools.ExpectedException(exceptions.CommandError) as e: + self.cmd.take_action(parsed_args) + self.assertEqual(msg, str(e)) + + +class TestShowSfcServiceGraph(fakes.TestNeutronClientOSCV2): + + _sg = fakes.FakeSfcServiceGraph.create_sfc_service_graph() + columns = ('ID', 'Name', 'Branching Points') + columns_long = ('Branching Points', 'Description', 'ID', 'Name', 'Project') + data = ( + _sg['id'], + _sg['name'], + _sg['port_chains'] + ) + data_long = ( + _sg['port_chains'], + _sg['description'], + _sg['id'], + _sg['name'], + _sg['project_id'] + ) + + _service_graph = _sg + _service_graph_id = _sg['id'] + + def setUp(self): + super(TestShowSfcServiceGraph, self).setUp() + self.network.get_sfc_service_graph = mock.Mock( + return_value=self._service_graph + ) + # Get the command object to test + self.cmd = sfc_service_graph.ShowSfcServiceGraph( + self.app, self.namespace) + + def test_service_graph_show(self): + client = self.app.client_manager.network + mock_service_graph_show = client.get_sfc_service_graph + arglist = [ + self._service_graph_id, + ] + verifylist = [ + ('service_graph', self._service_graph_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + mock_service_graph_show.assert_called_once_with(self._service_graph_id) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, data) + + +class TestSetSfcServiceGraph(fakes.TestNeutronClientOSCV2): + _service_graph = fakes.FakeSfcServiceGraph.create_sfc_service_graph() + _service_graph_name = _service_graph['name'] + _service_graph_id = _service_graph['id'] + + def setUp(self): + super(TestSetSfcServiceGraph, self).setUp() + self.network.update_sfc_service_graph = mock.Mock( + return_value=None) + self.cmd = sfc_service_graph.SetSfcServiceGraph( + self.app, self.namespace) + + def test_set_service_graph(self): + client = self.app.client_manager.network + mock_service_graph_update = client.update_sfc_service_graph + arglist = [ + self._service_graph_name, + '--name', 'name_updated', + '--description', 'desc_updated' + ] + verifylist = [ + ('service_graph', self._service_graph_name), + ('name', 'name_updated'), + ('description', 'desc_updated'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'name': 'name_updated', + 'description': 'desc_updated' + } + mock_service_graph_update.assert_called_once_with( + self._service_graph_name, **attrs) + self.assertIsNone(result) diff --git a/neutronclient/tests/unit/osc/v2/subnet_onboard/__init__.py b/neutronclient/tests/unit/osc/v2/subnet_onboard/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/tests/unit/osc/v2/subnet_onboard/test_network_onboard_subnets.py b/neutronclient/tests/unit/osc/v2/subnet_onboard/test_network_onboard_subnets.py new file mode 100644 index 000000000..c2ca14044 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/subnet_onboard/test_network_onboard_subnets.py @@ -0,0 +1,54 @@ +# Copyright (c) 2019 SUSE Linux Products GmbH +# 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 unittest import mock + +from neutronclient.osc.v2.subnet_onboard import subnet_onboard +from neutronclient.tests.unit.osc.v2 import fakes as test_fakes + + +def _get_id(client, id_or_name, resource): + return id_or_name + + +class TestNetworkOnboardSubnets(test_fakes.TestNeutronClientOSCV2): + + def setUp(self): + super(TestNetworkOnboardSubnets, self).setUp() + mock.patch( + 'neutronclient.osc.v2.subnet_onboard.subnet_onboard._get_id', + new=_get_id).start() + + self.network_id = 'my_network_id' + self.subnetpool_id = 'my_subnetpool_id' + + # Get the command object to test + self.cmd = subnet_onboard.NetworkOnboardSubnets(self.app, + self.namespace) + + def test_options(self): + arglist = [ + self.network_id, + self.subnetpool_id + ] + verifylist = [ + ('network', self.network_id), + ('subnetpool', self.subnetpool_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.neutronclient.onboard_network_subnets.assert_called_once_with( + self.subnetpool_id, {'network_id': self.network_id}) diff --git a/neutronclient/tests/unit/osc/v2/vpnaas/__init__.py b/neutronclient/tests/unit/osc/v2/vpnaas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/tests/unit/osc/v2/vpnaas/common.py b/neutronclient/tests/unit/osc/v2/vpnaas/common.py new file mode 100644 index 000000000..7aaebdbe3 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/vpnaas/common.py @@ -0,0 +1,183 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 osc_lib import exceptions + +from neutronclient.tests.unit.osc.v2 import fakes as test_fakes + + +class TestCreateVPNaaS(test_fakes.TestNeutronClientOSCV2): + pass + + +class TestDeleteVPNaaS(test_fakes.TestNeutronClientOSCV2): + + def test_delete_with_one_resource(self): + target = self.resource['id'] + + def _mock_vpnaas(*args, **kwargs): + return {'id': args[0]} + + self.networkclient.find_vpn_endpoint_group.side_effect = _mock_vpnaas + self.networkclient.find_vpn_ipsec_site_connection.side_effect = \ + _mock_vpnaas + self.networkclient.find_vpn_ike_policy.side_effect = _mock_vpnaas + self.networkclient.find_vpn_ipsec_policy.side_effect = _mock_vpnaas + self.networkclient.find_vpn_service.side_effect = _mock_vpnaas + + arglist = [target] + verifylist = [(self.res, [target])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target) + self.assertIsNone(result) + + def test_delete_with_multiple_resources(self): + + def _mock_vpnaas(*args, **kwargs): + return {'id': args[0]} + + self.networkclient.find_vpn_endpoint_group.side_effect = _mock_vpnaas + self.networkclient.find_vpn_ipsec_site_connection.side_effect = \ + _mock_vpnaas + self.networkclient.find_vpn_ike_policy.side_effect = _mock_vpnaas + self.networkclient.find_vpn_ipsec_policy.side_effect = _mock_vpnaas + self.networkclient.find_vpn_service.side_effect = _mock_vpnaas + + target1 = 'target1' + target2 = 'target2' + arglist = [target1, target2] + verifylist = [(self.res, [target1, target2])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertEqual(2, self.mocked.call_count) + for idx, reference in enumerate([target1, target2]): + actual = ''.join(self.mocked.call_args_list[idx][0]) + self.assertEqual(reference, actual) + + def test_delete_multiple_with_exception(self): + target1 = 'target' + arglist = [target1] + verifylist = [(self.res, [target1])] + + self.networkclient.find_vpn_ipsec_site_connection.side_effect = [ + target1, exceptions.CommandError + ] + self.networkclient.find_vpn_endpoint_group.side_effect = [ + target1, exceptions.CommandError + ] + self.networkclient.find_vpn_ike_policy.side_effect = [ + target1, exceptions.CommandError + ] + self.networkclient.find_vpn_service.side_effect = [ + target1, exceptions.CommandError + ] + self.networkclient.find_vpn_ipsec_policy.side_effect = [ + target1, exceptions.CommandError + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + resource_name = self.res.replace('_', ' ') + msg = "1 of 2 %s(s) failed to delete." % resource_name + with testtools.ExpectedException(exceptions.CommandError) as e: + self.cmd.take_action(parsed_args) + self.assertEqual(msg, str(e)) + + +class TestListVPNaaS(test_fakes.TestNeutronClientOSCV2): + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.list_headers), headers) + self.assertEqual([self.list_data], list(data)) + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + self.assertEqual([self.data], list(data)) + + +class TestSetVPNaaS(test_fakes.TestNeutronClientOSCV2): + + def test_set_name(self): + target = self.resource['id'] + update = 'change' + arglist = [target, '--name', update] + verifylist = [ + (self.res, target), + ('name', update), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'name': update}) + self.assertIsNone(result) + + def test_set_description(self): + target = self.resource['id'] + update = 'change-desc' + arglist = [target, '--description', update] + verifylist = [ + (self.res, target), + ('description', update), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'description': update}) + self.assertIsNone(result) + + +class TestShowVPNaaS(test_fakes.TestNeutronClientOSCV2): + + def test_show_filtered_by_id_or_name(self): + target = self.resource['id'] + + def _mock_vpnaas(*args, **kwargs): + return {'id': args[0]} + + self.networkclient.find_vpn_endpoint_group.side_effect = _mock_vpnaas + self.networkclient.find_vpn_ipsec_site_connection.side_effect = \ + _mock_vpnaas + self.networkclient.find_vpn_ike_policy.side_effect = _mock_vpnaas + self.networkclient.find_vpn_ipsec_policy.side_effect = _mock_vpnaas + self.networkclient.find_vpn_service.side_effect = _mock_vpnaas + + arglist = [target] + verifylist = [(self.res, target)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target) + self.assertEqual(self.ordered_headers, headers) + self.assertItemEqual(self.ordered_data, data) diff --git a/neutronclient/tests/unit/osc/v2/vpnaas/fakes.py b/neutronclient/tests/unit/osc/v2/vpnaas/fakes.py new file mode 100644 index 000000000..24e6497fc --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/vpnaas/fakes.py @@ -0,0 +1,191 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 collections +from unittest import mock +import uuid + +from openstack.network.v2 import vpn_endpoint_group as vpn_epg +from openstack.network.v2 import vpn_ike_policy as vpn_ikep +from openstack.network.v2 import vpn_ipsec_policy as vpn_ipsecp +from openstack.network.v2 import vpn_ipsec_site_connection as vpn_sitec +from openstack.network.v2 import vpn_service + + +class FakeVPNaaS(object): + + def create(self, attrs={}): + """Create a fake vpnaas resources + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A OrderedDict faking the vpnaas resource + """ + self.ordered.update(attrs) + if 'IKEPolicy' == self.__class__.__name__: + return vpn_ikep.VpnIkePolicy(**self.ordered) + if 'IPSecPolicy' == self.__class__.__name__: + return vpn_ipsecp.VpnIpsecPolicy(**self.ordered) + if 'VPNService' == self.__class__.__name__: + return vpn_service.VpnService(**self.ordered) + if 'EndpointGroup' == self.__class__.__name__: + return vpn_epg.VpnEndpointGroup(**self.ordered) + + def bulk_create(self, attrs=None, count=2): + """Create multiple fake vpnaas resources + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of vpnaas resources to fake + :return: + A list of dictionaries faking the vpnaas resources + """ + return [self.create(attrs=attrs) for i in range(0, count)] + + def get(self, attrs=None, count=2): + """Get multiple fake vpnaas resources + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of vpnaas resources to fake + :return: + A list of dictionaries faking the vpnaas resource + """ + if attrs is None: + self.attrs = self.bulk_create(count=count) + return mock.Mock(side_effect=attrs) + + +class IKEPolicy(FakeVPNaaS): + """Fake one or more IKE policies""" + + def __init__(self): + super(IKEPolicy, self).__init__() + self.ordered = collections.OrderedDict(( + ('id', 'ikepolicy-id-' + uuid.uuid4().hex), + ('name', 'my-ikepolicy-' + uuid.uuid4().hex), + ('auth_algorithm', 'sha1'), + ('encryption_algorithm', 'aes-128'), + ('ike_version', 'v1'), + ('pfs', 'group5'), + ('description', 'my-desc-' + uuid.uuid4().hex), + ('phase1_negotiation_mode', 'main'), + ('project_id', 'project-id-' + uuid.uuid4().hex), + ('lifetime', {'units': 'seconds', 'value': 3600}), + )) + + +class IPSecPolicy(FakeVPNaaS): + """Fake one or more IPsec policies""" + + def __init__(self): + super(IPSecPolicy, self).__init__() + self.ordered = collections.OrderedDict(( + ('id', 'ikepolicy-id-' + uuid.uuid4().hex), + ('name', 'my-ikepolicy-' + uuid.uuid4().hex), + ('auth_algorithm', 'sha1'), + ('encapsulation_mode', 'tunnel'), + ('transform_protocol', 'esp'), + ('encryption_algorithm', 'aes-128'), + ('pfs', 'group5'), + ('description', 'my-desc-' + uuid.uuid4().hex), + ('project_id', 'project-id-' + uuid.uuid4().hex), + ('lifetime', {'units': 'seconds', 'value': 3600}), + )) + + +class VPNService(FakeVPNaaS): + """Fake one or more VPN services""" + + def __init__(self): + super(VPNService, self).__init__() + self.ordered = collections.OrderedDict(( + ('id', 'vpnservice-id-' + uuid.uuid4().hex), + ('name', 'my-vpnservice-' + uuid.uuid4().hex), + ('router_id', 'router-id-' + uuid.uuid4().hex), + ('subnet_id', 'subnet-id-' + uuid.uuid4().hex), + ('flavor_id', 'flavor-id-' + uuid.uuid4().hex), + ('admin_state_up', True), + ('status', 'ACTIVE'), + ('description', 'my-desc-' + uuid.uuid4().hex), + ('project_id', 'project-id-' + uuid.uuid4().hex), + ('external_v4_ip', '192.0.2.42'), + ('external_v6_ip', '2001:0db8:207a:4a3a:053b:6fab:7df9:1afd'), + )) + + +class EndpointGroup(FakeVPNaaS): + """Fake one or more Endpoint Groups""" + + def __init__(self): + super(EndpointGroup, self).__init__() + self.ordered = collections.OrderedDict(( + ('id', 'ep-group-id-' + uuid.uuid4().hex), + ('name', 'my-ep-group-' + uuid.uuid4().hex), + ('type', 'cidr'), + ('endpoints', ['10.0.0.0/24', '20.0.0.0/24']), + ('description', 'my-desc-' + uuid.uuid4().hex), + ('project_id', 'project-id-' + uuid.uuid4().hex), + )) + + +class IPsecSiteConnection(object): + """Fake one or more IPsec site connections""" + @staticmethod + def create_conn(attrs=None): + """Create a fake IPsec conn. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A Dictionary with id, name, peer_address, auth_mode, status, + project_id, peer_cidrs, vpnservice_id, ipsecpolicy_id, + ikepolicy_id, mtu, initiator, admin_state_up, description, + psk, route_mode, local_id, peer_id, local_ep_group_id, + peer_ep_group_id + """ + attrs = attrs or {} + + # Set default attributes. + conn_attrs = { + 'id': 'ipsec-site-conn-id-' + uuid.uuid4().hex, + 'name': 'my-ipsec-site-conn-' + uuid.uuid4().hex, + 'peer_address': '192.168.2.10', + 'auth_mode': '', + 'status': '', + 'project_id': 'project-id-' + uuid.uuid4().hex, + 'peer_cidrs': [], + 'vpnservice_id': 'vpnservice-id-' + uuid.uuid4().hex, + 'ipsecpolicy_id': 'ipsecpolicy-id-' + uuid.uuid4().hex, + 'ikepolicy_id': 'ikepolicy-id-' + uuid.uuid4().hex, + 'mtu': 1500, + 'initiator': 'bi-directional', + 'admin_state_up': True, + 'description': 'my-vpn-connection', + 'psk': 'abcd', + 'route_mode': '', + 'local_id': '', + 'peer_id': '192.168.2.10', + 'local_ep_group_id': 'local-ep-group-id-' + uuid.uuid4().hex, + 'peer_ep_group_id': 'peer-ep-group-id-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + conn_attrs.update(attrs) + return vpn_sitec.VpnIPSecSiteConnection(**conn_attrs) diff --git a/neutronclient/tests/unit/osc/v2/vpnaas/test_endpoint_group.py b/neutronclient/tests/unit/osc/v2/vpnaas/test_endpoint_group.py new file mode 100644 index 000000000..e9da939b1 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/vpnaas/test_endpoint_group.py @@ -0,0 +1,254 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 unittest import mock + +from osc_lib.tests import utils as tests_utils + +from neutronclient.osc import utils as osc_utils +from neutronclient.osc.v2.vpnaas import endpoint_group +from neutronclient.tests.unit.osc.v2 import fakes as test_fakes +from neutronclient.tests.unit.osc.v2.vpnaas import common +from neutronclient.tests.unit.osc.v2.vpnaas import fakes + + +_endpoint_group = fakes.EndpointGroup().create() +CONVERT_MAP = { + 'project': 'tenant_id', +} + + +def _generate_data(ordered_dict=None, data=None): + source = ordered_dict if ordered_dict else _endpoint_group + if data: + source.update(data) + return source + + +def _generate_req_and_res(verifylist): + request = dict(verifylist) + response = _endpoint_group + for key, val in verifylist: + converted = CONVERT_MAP.get(key, key) + del request[key] + new_value = val + request[converted] = new_value + response[converted] = new_value + return request, response + + +class TestEndpointGroup(test_fakes.TestNeutronClientOSCV2): + + def check_results(self, headers, data, exp_req, is_list=False): + if is_list: + req_body = {self.res_plural: list(exp_req)} + else: + req_body = exp_req + self.mocked.assert_called_once_with(**req_body) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + self.assertEqual(self.ordered_data, data) + + def setUp(self): + super(TestEndpointGroup, self).setUp() + + def _mock_endpoint_group(*args, **kwargs): + self.networkclient.find_vpn_endpoint_group.assert_called_once_with( + self.resource['id'], ignore_missing=False) + return {'id': args[0]} + + self.networkclient.find_vpn_endpoint_group.side_effect = mock.Mock( + side_effect=_mock_endpoint_group) + osc_utils.find_project = mock.Mock() + osc_utils.find_project.id = _endpoint_group['project_id'] + self.res = 'endpoint_group' + self.res_plural = 'endpoint_groups' + self.resource = _endpoint_group + self.headers = ( + 'ID', + 'Name', + 'Type', + 'Endpoints', + 'Description', + 'Project', + ) + self.data = _generate_data() + self.ordered_headers = ( + 'Description', + 'Endpoints', + 'ID', + 'Name', + 'Project', + 'Type', + ) + self.ordered_data = ( + _endpoint_group['description'], + _endpoint_group['endpoints'], + _endpoint_group['id'], + _endpoint_group['name'], + _endpoint_group['project_id'], + _endpoint_group['type'], + ) + self.ordered_columns = ( + 'description', + 'endpoints', + 'id', + 'name', + 'project_id', + 'type', + ) + + +class TestCreateEndpointGroup(TestEndpointGroup, common.TestCreateVPNaaS): + + def setUp(self): + super(TestCreateEndpointGroup, self).setUp() + self.networkclient.create_vpn_endpoint_group = mock.Mock( + return_value=_endpoint_group) + self.mocked = self.networkclient.create_vpn_endpoint_group + self.cmd = endpoint_group.CreateEndpointGroup(self.app, self.namespace) + + def _update_expect_response(self, request, response): + """Set expected request and response + + :param request + A dictionary of request body(dict of verifylist) + :param response + A OrderedDict of request body + """ + # Update response body + self.neutronclient.create_endpoint_group.return_value = \ + {self.res: dict(response)} + osc_utils.find_project.return_value.id = response['tenant_id'] + # Update response(finally returns 'data') + self.data = _generate_data(ordered_dict=response) + self.ordered_data = tuple( + response[column] for column in self.ordered_columns + ) + + def _set_all_params_cidr(self, args={}): + name = args.get('name') or 'my-name' + description = args.get('description') or 'my-desc' + endpoint_type = args.get('type') or 'cidr' + endpoints = args.get('endpoints') or ['10.0.0.0/24', '20.0.0.0/24'] + tenant_id = args.get('project_id') or 'my-tenant' + arglist = [ + '--description', description, + '--type', endpoint_type, + '--value', '10.0.0.0/24', + '--value', '20.0.0.0/24', + '--project', tenant_id, + name, + ] + verifylist = [ + ('description', description), + ('type', endpoint_type), + ('endpoints', endpoints), + ('project', tenant_id), + ('name', name), + ] + return arglist, verifylist + + def _test_create_with_all_params_cidr(self, args={}): + arglist, verifylist = self._set_all_params_cidr(args) + request, response = _generate_req_and_res(verifylist) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.check_results(headers, data, request) + + def test_create_with_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_with_all_params_cidr(self): + self._test_create_with_all_params_cidr() + + +class TestDeleteEndpointGroup(TestEndpointGroup, common.TestDeleteVPNaaS): + + def setUp(self): + super(TestDeleteEndpointGroup, self).setUp() + self.networkclient.delete_vpn_endpoint_group = mock.Mock() + self.mocked = self.networkclient.delete_vpn_endpoint_group + self.cmd = endpoint_group.DeleteEndpointGroup(self.app, self.namespace) + + +class TestListEndpointGroup(TestEndpointGroup): + + def setUp(self): + super(TestListEndpointGroup, self).setUp() + self.cmd = endpoint_group.ListEndpointGroup(self.app, self.namespace) + + self.short_header = ( + 'ID', + 'Name', + 'Type', + 'Endpoints', + ) + + self.short_data = ( + _endpoint_group['id'], + _endpoint_group['name'], + _endpoint_group['type'], + _endpoint_group['endpoints'], + ) + + self.networkclient.vpn_endpoint_groups = mock.Mock( + return_value=[_endpoint_group]) + self.mocked = self.networkclient.vpn_endpoint_groups + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestSetEndpointGroup(TestEndpointGroup, common.TestSetVPNaaS): + + def setUp(self): + super(TestSetEndpointGroup, self).setUp() + self.networkclient.update_vpn_endpoint_group = mock.Mock( + return_value=_endpoint_group) + self.mocked = self.networkclient.update_vpn_endpoint_group + self.cmd = endpoint_group.SetEndpointGroup(self.app, self.namespace) + + +class TestShowEndpointGroup(TestEndpointGroup, common.TestShowVPNaaS): + + def setUp(self): + super(TestShowEndpointGroup, self).setUp() + self.networkclient.get_vpn_endpoint_group = mock.Mock( + return_value=_endpoint_group) + self.mocked = self.networkclient.get_vpn_endpoint_group + self.cmd = endpoint_group.ShowEndpointGroup(self.app, self.namespace) diff --git a/neutronclient/tests/unit/osc/v2/vpnaas/test_ikepolicy.py b/neutronclient/tests/unit/osc/v2/vpnaas/test_ikepolicy.py new file mode 100644 index 000000000..bf68611f8 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/vpnaas/test_ikepolicy.py @@ -0,0 +1,319 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 unittest import mock + +from osc_lib.tests import utils as tests_utils + +from neutronclient.osc import utils as osc_utils +from neutronclient.osc.v2.vpnaas import ikepolicy +from neutronclient.tests.unit.osc.v2 import fakes as test_fakes +from neutronclient.tests.unit.osc.v2.vpnaas import common +from neutronclient.tests.unit.osc.v2.vpnaas import fakes + + +_ikepolicy = fakes.IKEPolicy().create() +CONVERT_MAP = { + 'project': 'project_id', +} + + +def _generate_data(ordered_dict=None, data=None): + source = ordered_dict if ordered_dict else _ikepolicy + if data: + source.update(data) + return source + + +def _generate_req_and_res(verifylist): + request = dict(verifylist) + response = _ikepolicy + for key, val in verifylist: + converted = CONVERT_MAP.get(key, key) + del request[key] + new_value = val + request[converted] = new_value + response[converted] = new_value + return request, response + + +class TestIKEPolicy(test_fakes.TestNeutronClientOSCV2): + + def check_results(self, headers, data, exp_req, is_list=False): + if is_list: + req_body = {self.res_plural: list(exp_req)} + else: + req_body = exp_req + self.mocked.assert_called_once_with(**req_body) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + self.assertEqual(self.ordered_data, data) + + def setUp(self): + super(TestIKEPolicy, self).setUp() + + def _mock_ikepolicy(*args, **kwargs): + self.networkclient.find_vpn_ike_policy.assert_called_once_with( + self.resource['id'], ignore_missing=False) + return {'id': args[0]} + + self.networkclient.find_vpn_ike_policy.side_effect = mock.Mock( + side_effect=_mock_ikepolicy) + osc_utils.find_project = mock.Mock() + osc_utils.find_project.id = _ikepolicy['project_id'] + self.res = 'ikepolicy' + self.res_plural = 'ikepolicies' + self.resource = _ikepolicy + self.headers = ( + 'ID', + 'Name', + 'Authentication Algorithm', + 'Encryption Algorithm', + 'IKE Version', + 'Perfect Forward Secrecy (PFS)', + 'Description', + 'Phase1 Negotiation Mode', + 'Project', + 'Lifetime', + ) + self.data = _generate_data() + self.ordered_headers = ( + 'Authentication Algorithm', + 'Description', + 'Encryption Algorithm', + 'ID', + 'IKE Version', + 'Lifetime', + 'Name', + 'Perfect Forward Secrecy (PFS)', + 'Phase1 Negotiation Mode', + 'Project', + ) + self.ordered_data = ( + _ikepolicy['auth_algorithm'], + _ikepolicy['description'], + _ikepolicy['encryption_algorithm'], + _ikepolicy['id'], + _ikepolicy['ike_version'], + _ikepolicy['lifetime'], + _ikepolicy['name'], + _ikepolicy['pfs'], + _ikepolicy['phase1_negotiation_mode'], + _ikepolicy['project_id'], + ) + self.ordered_columns = ( + 'auth_algorithm', + 'description', + 'encryption_algorithm', + 'id', + 'ike_version', + 'lifetime', + 'name', + 'pfs', + 'phase1_negotiation_mode', + 'project_id', + ) + + +class TestCreateIKEPolicy(TestIKEPolicy, common.TestCreateVPNaaS): + + def setUp(self): + super(TestCreateIKEPolicy, self).setUp() + self.networkclient.create_vpn_ike_policy = mock.Mock( + return_value=_ikepolicy) + self.mocked = self.networkclient.create_vpn_ike_policy + self.cmd = ikepolicy.CreateIKEPolicy(self.app, self.namespace) + + def _update_expect_response(self, request, response): + """Set expected request and response + + :param request + A dictionary of request body(dict of verifylist) + :param response + A OrderedDict of request body + """ + # Update response body + self.networkclient.create_vpn_ikepolicy.return_value = response + osc_utils.find_project.return_value.id = response['project_id'] + # Update response(finally returns 'data') + self.data = _generate_data(ordered_dict=response) + self.ordered_data = tuple( + response[column] for column in self.ordered_columns + ) + + def _set_all_params(self, args={}): + name = args.get('name') or 'my-name' + description = args.get('description') or 'my-desc' + auth_algorithm = args.get('auth_algorithm') or 'sha1' + encryption_algorithm = args.get('encryption_algorithm') or 'aes-128' + phase1_negotiation_mode = args.get('phase1_negotiation_mode') or 'main' + ike_version = args.get('ike_version') or 'v1' + pfs = args.get('pfs') or 'group5' + tenant_id = args.get('tenant_id') or 'my-tenant' + arglist = [ + '--description', description, + '--auth-algorithm', auth_algorithm, + '--encryption-algorithm', encryption_algorithm, + '--phase1-negotiation-mode', phase1_negotiation_mode, + '--ike-version', ike_version, + '--pfs', pfs, + '--project', tenant_id, + name, + ] + verifylist = [ + ('description', description), + ('auth_algorithm', auth_algorithm), + ('encryption_algorithm', encryption_algorithm), + ('phase1_negotiation_mode', phase1_negotiation_mode), + ('ike_version', ike_version), + ('pfs', pfs), + ('project', tenant_id), + ('name', name), + ] + return arglist, verifylist + + def _test_create_with_all_params(self, args={}): + arglist, verifylist = self._set_all_params(args) + request, response = _generate_req_and_res(verifylist) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.check_results(headers, data, request) + + def test_create_with_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_with_all_params(self): + self._test_create_with_all_params() + + def test_create_with_all_params_name(self): + self._test_create_with_all_params({'name': 'new_ikepolicy'}) + + def test_create_with_all_params_aggressive_mode(self): + self._test_create_with_all_params( + {'phase1_negotiation_mode': 'aggressive'}) + + +class TestDeleteIKEPolicy(TestIKEPolicy, common.TestDeleteVPNaaS): + + def setUp(self): + super(TestDeleteIKEPolicy, self).setUp() + self.networkclient.delete_vpn_ike_policy = mock.Mock() + self.mocked = self.networkclient.delete_vpn_ike_policy + self.cmd = ikepolicy.DeleteIKEPolicy(self.app, self.namespace) + + +class TestListIKEPolicy(TestIKEPolicy): + + def setUp(self): + super(TestListIKEPolicy, self).setUp() + self.cmd = ikepolicy.ListIKEPolicy(self.app, self.namespace) + + self.short_header = ( + 'ID', + 'Name', + 'Authentication Algorithm', + 'Encryption Algorithm', + 'IKE Version', + 'Perfect Forward Secrecy (PFS)', + ) + + self.short_data = ( + _ikepolicy['id'], + _ikepolicy['name'], + _ikepolicy['auth_algorithm'], + _ikepolicy['encryption_algorithm'], + _ikepolicy['ike_version'], + _ikepolicy['pfs'], + ) + + self.networkclient.vpn_ike_policies = mock.Mock( + return_value=[_ikepolicy]) + self.mocked = self.networkclient.vpn_ike_policies + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestSetIKEPolicy(TestIKEPolicy, common.TestSetVPNaaS): + + def setUp(self): + super(TestSetIKEPolicy, self).setUp() + self.networkclient.update_vpn_ike_policy = mock.Mock( + return_value=_ikepolicy) + self.mocked = self.networkclient.update_vpn_ike_policy + self.cmd = ikepolicy.SetIKEPolicy(self.app, self.namespace) + + def test_set_auth_algorithm_with_sha256(self): + target = self.resource['id'] + auth_algorithm = 'sha256' + arglist = [target, '--auth-algorithm', auth_algorithm] + verifylist = [ + (self.res, target), + ('auth_algorithm', auth_algorithm), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'auth_algorithm': 'sha256'}) + self.assertIsNone(result) + + def test_set_phase1_negotiation_mode_with_aggressive(self): + target = self.resource['id'] + phase1_negotiation_mode = 'aggressive' + arglist = [target, + '--phase1-negotiation-mode', phase1_negotiation_mode] + verifylist = [ + (self.res, target), + ('phase1_negotiation_mode', phase1_negotiation_mode), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'phase1_negotiation_mode': 'aggressive'}) + self.assertIsNone(result) + + +class TestShowIKEPolicy(TestIKEPolicy, common.TestShowVPNaaS): + + def setUp(self): + super(TestShowIKEPolicy, self).setUp() + self.networkclient.get_vpn_ike_policy = mock.Mock( + return_value=_ikepolicy) + self.mocked = self.networkclient.get_vpn_ike_policy + self.cmd = ikepolicy.ShowIKEPolicy(self.app, self.namespace) diff --git a/neutronclient/tests/unit/osc/v2/vpnaas/test_ipsec_site_connection.py b/neutronclient/tests/unit/osc/v2/vpnaas/test_ipsec_site_connection.py new file mode 100644 index 000000000..4929ea843 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/vpnaas/test_ipsec_site_connection.py @@ -0,0 +1,372 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 unittest import mock + +from osc_lib.cli import format_columns +from osc_lib.tests import utils as tests_utils + +from neutronclient.osc import utils as osc_utils +from neutronclient.osc.v2.vpnaas import ipsec_site_connection +from neutronclient.tests.unit.osc.v2 import fakes as test_fakes +from neutronclient.tests.unit.osc.v2.vpnaas import common +from neutronclient.tests.unit.osc.v2.vpnaas import fakes + + +_ipsec_site_conn = fakes.IPsecSiteConnection().create_conn() +CONVERT_MAP = { + 'project': 'project_id', + 'ikepolicy': 'ikepolicy_id', + 'ipsecpolicy': 'ipsecpolicy_id', + 'vpnservice': 'vpnservice_id', + 'peer_endpoint_group': 'peer_ep_group_id', + 'local_endpoint_group': 'local_ep_group_id', +} + + +def _generate_data(ordered_dict=None, data=None): + source = ordered_dict if ordered_dict else _ipsec_site_conn + if data: + source.update(data) + return source + + +def _generate_req_and_res(verifylist): + request = dict(verifylist) + response = _ipsec_site_conn + for key, val in verifylist: + converted = CONVERT_MAP.get(key, key) + del request[key] + new_value = val + request[converted] = new_value + response[converted] = new_value + return request, response + + +class TestIPsecSiteConn(test_fakes.TestNeutronClientOSCV2): + + def check_results(self, headers, data, exp_req, is_list=False): + if is_list: + req_body = {self.res_plural: list(exp_req)} + else: + req_body = exp_req + self.mocked.assert_called_once_with(**req_body) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + self.assertItemEqual(self.ordered_data, data) + + def setUp(self): + super(TestIPsecSiteConn, self).setUp() + + def _mock_ipsec_site_conn(*args, **kwargs): + return {'id': args[0]} + + self.networkclient.find_vpn_ipsec_site_connection.side_effect = \ + mock.Mock(side_effect=_mock_ipsec_site_conn) + osc_utils.find_project = mock.Mock() + osc_utils.find_project.id = _ipsec_site_conn['project_id'] + self.res = 'ipsec_site_connection' + self.res_plural = 'ipsec_site_connections' + self.resource = _ipsec_site_conn + self.headers = ( + 'ID', + 'Name', + 'Peer Address', + 'Authentication Algorithm', + 'Status', + 'Project', + 'Peer CIDRs', + 'VPN Service', + 'IPSec Policy', + 'IKE Policy', + 'MTU', + 'Initiator', + 'State', + 'Description', + 'Pre-shared Key', + 'Route Mode', + 'Local ID', + 'Peer ID', + 'Local Endpoint Group ID', + 'Peer Endpoint Group ID', + 'DPD', + ) + self.data = _generate_data() + self.ordered_headers = ( + 'Authentication Algorithm', + 'DPD', + 'Description', + 'ID', + 'IKE Policy', + 'IPSec Policy', + 'Initiator', + 'Local Endpoint Group ID', + 'Local ID', + 'MTU', + 'Name', + 'Peer Address', + 'Peer CIDRs', + 'Peer Endpoint Group ID', + 'Peer ID', + 'Pre-shared Key', + 'Project', + 'Route Mode', + 'State', + 'Status', + 'VPN Service', + ) + self.ordered_data = ( + _ipsec_site_conn['auth_mode'], + _ipsec_site_conn['dpd'], + _ipsec_site_conn['description'], + _ipsec_site_conn['id'], + _ipsec_site_conn['ikepolicy_id'], + _ipsec_site_conn['ipsecpolicy_id'], + _ipsec_site_conn['initiator'], + _ipsec_site_conn['local_ep_group_id'], + _ipsec_site_conn['local_id'], + _ipsec_site_conn['mtu'], + _ipsec_site_conn['name'], + _ipsec_site_conn['peer_address'], + format_columns.ListColumn(_ipsec_site_conn['peer_cidrs']), + _ipsec_site_conn['peer_ep_group_id'], + _ipsec_site_conn['peer_id'], + _ipsec_site_conn['psk'], + _ipsec_site_conn['project_id'], + _ipsec_site_conn['route_mode'], + _ipsec_site_conn['admin_state_up'], + _ipsec_site_conn['status'], + _ipsec_site_conn['vpnservice_id'], + ) + + +class TestCreateIPsecSiteConn(TestIPsecSiteConn, common.TestCreateVPNaaS): + + def setUp(self): + super(TestCreateIPsecSiteConn, self).setUp() + self.networkclient.create_vpn_ipsec_site_connection = mock.Mock( + return_value=_ipsec_site_conn) + self.mocked = self.networkclient.create_vpn_ipsec_site_connection + self.cmd = ipsec_site_connection.CreateIPsecSiteConnection( + self.app, self.namespace) + + def _update_expect_response(self, request, response): + """Set expected request and response + + :param request + A dictionary of request body(dict of verifylist) + :param response + A OrderedDict of request body + """ + # Update response body + self.networkclient.create_vpn_ipsec_site_connection.return_value = \ + response + osc_utils.find_project.return_value.id = response['project_id'] + # Update response(finally returns 'data') + self.data = _generate_data(ordered_dict=response) + self.ordered_data = ( + response['auth_mode'], + response['dpd'], + response['description'], + response['id'], + response['ikepolicy_id'], + response['ipsecpolicy_id'], + response['initiator'], + response['local_ep_group_id'], + response['local_id'], + response['mtu'], + response['name'], + response['peer_address'], + format_columns.ListColumn(response['peer_cidrs']), + response['peer_ep_group_id'], + response['peer_id'], + response['psk'], + response['project_id'], + response['route_mode'], + response['admin_state_up'], + response['status'], + response['vpnservice_id'], + ) + + def _set_all_params(self, args={}): + tenant_id = args.get('tenant_id') or 'my-tenant' + name = args.get('name') or 'connection1' + peer_address = args.get('peer_address') or '192.168.2.10' + peer_id = args.get('peer_id') or '192.168.2.10' + psk = args.get('psk') or 'abcd' + mtu = args.get('mtu') or '1500' + initiator = args.get('initiator') or 'bi-directional' + vpnservice_id = args.get('vpnservice') or 'vpnservice_id' + ikepolicy_id = args.get('ikepolicy') or 'ikepolicy_id' + ipsecpolicy_id = args.get('ipsecpolicy') or 'ipsecpolicy_id' + local_ep_group = args.get('local_ep_group_id') or 'local-epg' + peer_ep_group = args.get('peer_ep_group_id') or 'peer-epg' + description = args.get('description') or 'my-vpn-connection' + + arglist = [ + '--project', tenant_id, + '--peer-address', peer_address, + '--peer-id', peer_id, + '--psk', psk, + '--initiator', initiator, + '--vpnservice', vpnservice_id, + '--ikepolicy', ikepolicy_id, + '--ipsecpolicy', ipsecpolicy_id, + '--mtu', mtu, + '--description', description, + '--local-endpoint-group', local_ep_group, + '--peer-endpoint-group', peer_ep_group, + name, + ] + verifylist = [ + ('project', tenant_id), + ('peer_address', peer_address), + ('peer_id', peer_id), + ('psk', psk), + ('initiator', initiator), + ('vpnservice', vpnservice_id), + ('ikepolicy', ikepolicy_id), + ('ipsecpolicy', ipsecpolicy_id), + ('mtu', mtu), + ('description', description), + ('local_endpoint_group', local_ep_group), + ('peer_endpoint_group', peer_ep_group), + ('name', name), + ] + return arglist, verifylist + + def _test_create_with_all_params(self, args={}): + arglist, verifylist = self._set_all_params(args) + request, response = _generate_req_and_res(verifylist) + + def _mock_endpoint_group(*args, **kwargs): + return {'id': args[0]} + + self.networkclient.find_vpn_endpoint_group.side_effect = mock.Mock( + side_effect=_mock_endpoint_group) + self.networkclient.find_vpn_service.side_effect = mock.Mock( + side_effect=_mock_endpoint_group) + self.networkclient.find_vpn_ike_policy.side_effect = mock.Mock( + side_effect=_mock_endpoint_group) + self.networkclient.find_vpn_ipsec_policy.side_effect = mock.Mock( + side_effect=_mock_endpoint_group) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.check_results(headers, data, request) + + def test_create_with_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_with_all_params(self): + self._test_create_with_all_params() + + +class TestDeleteIPsecSiteConn(TestIPsecSiteConn, common.TestDeleteVPNaaS): + + def setUp(self): + super(TestDeleteIPsecSiteConn, self).setUp() + self.networkclient.delete_vpn_ipsec_site_connection = mock.Mock() + self.mocked = self.networkclient.delete_vpn_ipsec_site_connection + self.cmd = ipsec_site_connection.DeleteIPsecSiteConnection( + self.app, self.namespace) + + +class TestListIPsecSiteConn(TestIPsecSiteConn): + + def setUp(self): + super(TestListIPsecSiteConn, self).setUp() + self.cmd = ipsec_site_connection.ListIPsecSiteConnection( + self.app, self.namespace) + + self.short_header = ( + 'ID', + 'Name', + 'Peer Address', + 'Authentication Algorithm', + 'Status', + ) + + self.short_data = ( + _ipsec_site_conn['id'], + _ipsec_site_conn['name'], + _ipsec_site_conn['peer_address'], + _ipsec_site_conn['auth_mode'], + _ipsec_site_conn['status'], + ) + + self.networkclient.vpn_ipsec_site_connections = mock.Mock( + return_value=[_ipsec_site_conn]) + self.mocked = self.networkclient.vpn_ipsec_site_connections + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestSetIPsecSiteConn(TestIPsecSiteConn, common.TestSetVPNaaS): + + def setUp(self): + super(TestSetIPsecSiteConn, self).setUp() + self.networkclient.update_vpn_ipsec_site_connection = mock.Mock( + return_value=_ipsec_site_conn) + self.mocked = self.networkclient.update_vpn_ipsec_site_connection + self.cmd = ipsec_site_connection.SetIPsecSiteConnection( + self.app, self.namespace) + + def test_set_ipsec_site_conn_with_peer_id(self): + target = self.resource['id'] + peer_id = '192.168.3.10' + arglist = [target, '--peer-id', peer_id] + verifylist = [ + (self.res, target), + ('peer_id', peer_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'peer_id': peer_id}) + self.assertIsNone(result) + + +class TestShowIPsecSiteConn(TestIPsecSiteConn, common.TestShowVPNaaS): + + def setUp(self): + super(TestShowIPsecSiteConn, self).setUp() + self.networkclient.get_vpn_ipsec_site_connection = mock.Mock( + return_value=_ipsec_site_conn) + self.mocked = self.networkclient.get_vpn_ipsec_site_connection + self.cmd = ipsec_site_connection.ShowIPsecSiteConnection( + self.app, self.namespace) diff --git a/neutronclient/tests/unit/osc/v2/vpnaas/test_ipsecpolicy.py b/neutronclient/tests/unit/osc/v2/vpnaas/test_ipsecpolicy.py new file mode 100644 index 000000000..3729a11fe --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/vpnaas/test_ipsecpolicy.py @@ -0,0 +1,299 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 unittest import mock + +from osc_lib.tests import utils as tests_utils + +from neutronclient.osc import utils as osc_utils +from neutronclient.osc.v2.vpnaas import ipsecpolicy +from neutronclient.tests.unit.osc.v2 import fakes as test_fakes +from neutronclient.tests.unit.osc.v2.vpnaas import common +from neutronclient.tests.unit.osc.v2.vpnaas import fakes + + +_ipsecpolicy = fakes.IPSecPolicy().create() +CONVERT_MAP = { + 'project': 'project_id', +} + + +def _generate_data(ordered_dict=None, data=None): + source = ordered_dict if ordered_dict else _ipsecpolicy + if data: + source.update(data) + return source + + +def _generate_req_and_res(verifylist): + request = dict(verifylist) + response = _ipsecpolicy + for key, val in verifylist: + converted = CONVERT_MAP.get(key, key) + del request[key] + new_value = val + request[converted] = new_value + response[converted] = new_value + return request, response + + +class TestIPSecPolicy(test_fakes.TestNeutronClientOSCV2): + + def check_results(self, headers, data, exp_req, is_list=False): + if is_list: + req_body = {self.res_plural: list(exp_req)} + else: + req_body = exp_req + self.mocked.assert_called_once_with(**req_body) + self.assertEqual(self.ordered_headers, headers) + self.assertEqual(self.ordered_data, data) + + def setUp(self): + super(TestIPSecPolicy, self).setUp() + + def _mock_ipsecpolicy(*args, **kwargs): + self.networkclient.find_vpn_ipsec_policy.assert_called_once_with( + self.resource['id'], ignore_missing=False) + return {'id': args[0]} + + self.networkclient.find_vpn_ipsec_policy.side_effect = mock.Mock( + side_effect=_mock_ipsecpolicy) + osc_utils.find_project = mock.Mock() + osc_utils.find_project.id = _ipsecpolicy['project_id'] + self.res = 'ipsecpolicy' + self.res_plural = 'ipsecpolicies' + self.resource = _ipsecpolicy + self.headers = ( + 'ID', + 'Name', + 'Authentication Algorithm', + 'Encapsulation Mode', + 'Transform Protocol', + 'Encryption Algorithm', + 'Perfect Forward Secrecy (PFS)', + 'Description', + 'Project', + 'Lifetime', + ) + self.data = _generate_data() + self.ordered_headers = ( + 'Authentication Algorithm', + 'Description', + 'Encapsulation Mode', + 'Encryption Algorithm', + 'ID', + 'Lifetime', + 'Name', + 'Perfect Forward Secrecy (PFS)', + 'Project', + 'Transform Protocol', + ) + self.ordered_data = ( + _ipsecpolicy['auth_algorithm'], + _ipsecpolicy['description'], + _ipsecpolicy['encapsulation_mode'], + _ipsecpolicy['encryption_algorithm'], + _ipsecpolicy['id'], + _ipsecpolicy['lifetime'], + _ipsecpolicy['name'], + _ipsecpolicy['pfs'], + _ipsecpolicy['project_id'], + _ipsecpolicy['transform_protocol'], + ) + self.ordered_columns = ( + 'auth_algorithm', + 'description', + 'encapsulation_mode', + 'encryption_algorithm', + 'id', + 'lifetime', + 'name', + 'pfs', + 'project_id', + 'transform_protocol', + ) + + +class TestCreateIPSecPolicy(TestIPSecPolicy, common.TestCreateVPNaaS): + + def setUp(self): + super(TestCreateIPSecPolicy, self).setUp() + self.networkclient.create_vpn_ipsec_policy = mock.Mock( + return_value=_ipsecpolicy) + self.mocked = self.networkclient.create_vpn_ipsec_policy + self.cmd = ipsecpolicy.CreateIPsecPolicy(self.app, self.namespace) + + def _update_expect_response(self, request, response): + """Set expected request and response + + :param request + A dictionary of request body(dict of verifylist) + :param response + A OrderedDict of request body + """ + # Update response body + self.networkclient.create_vpn_ipsec_policy.return_value = \ + response + osc_utils.find_project.return_value.id = response['project_id'] + # Update response(finally returns 'data') + self.data = _generate_data(ordered_dict=response) + self.ordered_data = tuple( + response[column] for column in self.ordered_columns + ) + + def _set_all_params(self, args={}): + name = args.get('name') or 'my-name' + auth_algorithm = args.get('auth_algorithm') or 'sha1' + encapsulation_mode = args.get('encapsulation_mode') or 'tunnel' + transform_protocol = args.get('transform_protocol') or 'esp' + encryption_algorithm = args.get('encryption_algorithm') or 'aes-128' + pfs = args.get('pfs') or 'group5' + description = args.get('description') or 'my-desc' + project_id = args.get('project_id') or 'my-project' + arglist = [ + name, + '--auth-algorithm', auth_algorithm, + '--encapsulation-mode', encapsulation_mode, + '--transform-protocol', transform_protocol, + '--encryption-algorithm', encryption_algorithm, + '--pfs', pfs, + '--description', description, + '--project', project_id, + ] + verifylist = [ + ('name', name), + ('auth_algorithm', auth_algorithm), + ('encapsulation_mode', encapsulation_mode), + ('transform_protocol', transform_protocol), + ('encryption_algorithm', encryption_algorithm), + ('pfs', pfs), + ('description', description), + ('project', project_id), + ] + return arglist, verifylist + + def _test_create_with_all_params(self, args={}): + arglist, verifylist = self._set_all_params(args) + request, response = _generate_req_and_res(verifylist) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + self.check_results(headers, data, request) + + def test_create_with_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_with_all_params(self): + self._test_create_with_all_params() + + def test_create_with_all_params_name(self): + self._test_create_with_all_params({'name': 'new_ipsecpolicy'}) + + +class TestDeleteIPSecPolicy(TestIPSecPolicy, common.TestDeleteVPNaaS): + + def setUp(self): + super(TestDeleteIPSecPolicy, self).setUp() + self.networkclient.delete_vpn_ipsec_policy = mock.Mock() + self.mocked = self.networkclient.delete_vpn_ipsec_policy + self.cmd = ipsecpolicy.DeleteIPsecPolicy(self.app, self.namespace) + + +class TestListIPSecPolicy(TestIPSecPolicy): + + def setUp(self): + super(TestListIPSecPolicy, self).setUp() + self.cmd = ipsecpolicy.ListIPsecPolicy(self.app, self.namespace) + + self.short_header = ( + 'ID', + 'Name', + 'Authentication Algorithm', + 'Encapsulation Mode', + 'Transform Protocol', + 'Encryption Algorithm', + ) + + self.short_data = ( + _ipsecpolicy['id'], + _ipsecpolicy['name'], + _ipsecpolicy['auth_algorithm'], + _ipsecpolicy['encapsulation_mode'], + _ipsecpolicy['transform_protocol'], + _ipsecpolicy['encryption_algorithm'], + ) + + self.networkclient.vpn_ipsec_policies = mock.Mock( + return_value=[_ipsecpolicy]) + self.mocked = self.networkclient.vpn_ipsec_policies + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestSetIPSecPolicy(TestIPSecPolicy, common.TestSetVPNaaS): + + def setUp(self): + super(TestSetIPSecPolicy, self).setUp() + self.networkclient.update_vpn_ipsec_policy = mock.Mock( + return_value=_ipsecpolicy) + self.mocked = self.networkclient.update_vpn_ipsec_policy + self.cmd = ipsecpolicy.SetIPsecPolicy(self.app, self.namespace) + + def test_set_auth_algorithm_with_sha256(self): + target = self.resource['id'] + auth_algorithm = 'sha256' + arglist = [target, '--auth-algorithm', auth_algorithm] + verifylist = [ + (self.res, target), + ('auth_algorithm', auth_algorithm), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'auth_algorithm': 'sha256'}) + self.assertIsNone(result) + + +class TestShowIPSecPolicy(TestIPSecPolicy, common.TestShowVPNaaS): + + def setUp(self): + super(TestShowIPSecPolicy, self).setUp() + self.networkclient.get_vpn_ipsec_policy = mock.Mock( + return_value=_ipsecpolicy) + self.mocked = self.networkclient.get_vpn_ipsec_policy + self.cmd = ipsecpolicy.ShowIPsecPolicy(self.app, self.namespace) diff --git a/neutronclient/tests/unit/osc/v2/vpnaas/test_vpnservice.py b/neutronclient/tests/unit/osc/v2/vpnaas/test_vpnservice.py new file mode 100644 index 000000000..521fe741c --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/vpnaas/test_vpnservice.py @@ -0,0 +1,305 @@ +# Copyright 2017 FUJITSU LIMITED +# 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 unittest import mock +import uuid + + +from neutronclient.osc import utils as osc_utils +from neutronclient.osc.v2.vpnaas import vpnservice +from neutronclient.tests.unit.osc.v2 import fakes as test_fakes +from neutronclient.tests.unit.osc.v2.vpnaas import common +from neutronclient.tests.unit.osc.v2.vpnaas import fakes + + +_vpnservice = fakes.VPNService().create() +CONVERT_MAP = { + 'project': 'project_id', + 'router': 'router_id', + 'subnet': 'subnet_id' +} + + +def _generate_data(ordered_dict=None, data=None): + source = ordered_dict if ordered_dict else _vpnservice + if data: + source.update(data) + return source + + +def _generate_req_and_res(verifylist): + request = dict(verifylist) + response = _vpnservice + for key, val in verifylist: + converted = CONVERT_MAP.get(key, key) + del request[key] + new_value = val + request[converted] = new_value + response[converted] = new_value + return request, response + + +class TestVPNService(test_fakes.TestNeutronClientOSCV2): + + def _check_results(self, headers, data, exp_req, is_list=False): + if is_list: + req_body = {self.res_plural: list(exp_req)} + else: + req_body = exp_req + self.mocked.assert_called_once_with(**req_body) + self.assertEqual(self.ordered_headers, headers) + self.assertEqual(self.ordered_data, data) + + def setUp(self): + super(TestVPNService, self).setUp() + + def _mock_vpnservice(*args, **kwargs): + self.networkclient.find_vpn_service.assert_called_once_with( + self.resource['id'], ignore_missing=False) + return {'id': args[0]} + + self.networkclient.find_router = mock.Mock() + self.networkclient.find_subnet = mock.Mock() + self.fake_router = mock.Mock() + self.fake_subnet = mock.Mock() + self.networkclient.find_router.return_value = self.fake_router + self.networkclient.find_subnet.return_value = self.fake_subnet + self.args = { + 'name': 'my-name', + 'description': 'my-desc', + 'project_id': 'project-id-' + uuid.uuid4().hex, + 'router_id': 'router-id-' + uuid.uuid4().hex, + 'subnet_id': 'subnet-id-' + uuid.uuid4().hex, + + } + self.fake_subnet.id = self.args['subnet_id'] + self.fake_router.id = self.args['router_id'] + + self.networkclient.find_vpn_service.side_effect = mock.Mock( + side_effect=_mock_vpnservice) + osc_utils.find_project = mock.Mock() + osc_utils.find_project.id = _vpnservice['project_id'] + + self.res = 'vpnservice' + self.res_plural = 'vpnservices' + self.resource = _vpnservice + self.headers = ( + 'ID', + 'Name', + 'Router', + 'Subnet', + 'Flavor', + 'State', + 'Status', + 'Description', + 'Project', + 'Ext v4 IP', + 'Ext v6 IP', + ) + self.data = _generate_data() + self.ordered_headers = ( + 'Description', + 'Ext v4 IP', + 'Ext v6 IP', + 'Flavor', + 'ID', + 'Name', + 'Project', + 'Router', + 'State', + 'Status', + 'Subnet', + ) + self.ordered_data = ( + _vpnservice['description'], + _vpnservice['external_v4_ip'], + _vpnservice['external_v6_ip'], + _vpnservice['flavor_id'], + _vpnservice['id'], + _vpnservice['name'], + _vpnservice['project_id'], + _vpnservice['router_id'], + _vpnservice['admin_state_up'], + _vpnservice['status'], + _vpnservice['subnet_id'], + ) + self.ordered_columns = ( + 'description', + 'external_v4_ip', + 'external_v6_ip', + 'flavor_id', + 'id', + 'name', + 'project_id', + 'router_id', + 'admin_state_up', + 'status', + 'subnet_id', + ) + + +class TestCreateVPNService(TestVPNService, common.TestCreateVPNaaS): + + def setUp(self): + super(TestCreateVPNService, self).setUp() + self.networkclient.create_vpn_service = mock.Mock( + return_value=_vpnservice) + self.mocked = self.networkclient.create_vpn_service + self.cmd = vpnservice.CreateVPNService(self.app, self.namespace) + + def _update_expect_response(self, request, response): + """Set expected request and response + + :param request + A dictionary of request body(dict of verifylist) + :param response + A OrderedDict of request body + """ + # Update response body + self.networkclient.create_vpn_service.return_value = response + osc_utils.find_project.return_value.id = response['project_id'] + # Update response(finally returns 'data') + self.data = _generate_data(ordered_dict=response) + self.ordered_data = tuple( + response[column] for column in self.ordered_columns + ) + + def _set_all_params(self): + name = self.args.get('name') + description = self.args.get('description') + router_id = self.args.get('router_id') + subnet_id = self.args.get('subnet_id') + project_id = self.args.get('project_id') + arglist = [ + '--description', description, + '--project', project_id, + '--subnet', subnet_id, + '--router', router_id, + name, + ] + verifylist = [ + ('description', description), + ('project', project_id), + ('subnet', subnet_id), + ('router', router_id), + ('name', name), + ] + return arglist, verifylist + + def _test_create_with_all_params(self): + arglist, verifylist = self._set_all_params() + request, response = _generate_req_and_res(verifylist) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self._check_results(headers, data, request) + + def test_create_with_all_params(self): + self._test_create_with_all_params() + + +class TestDeleteVPNService(TestVPNService, common.TestDeleteVPNaaS): + + def setUp(self): + super(TestDeleteVPNService, self).setUp() + self.networkclient.delete_vpn_service = mock.Mock() + self.mocked = self.networkclient.delete_vpn_service + self.cmd = vpnservice.DeleteVPNService(self.app, self.namespace) + + +class TestListVPNService(TestVPNService): + + def setUp(self): + super(TestListVPNService, self).setUp() + self.cmd = vpnservice.ListVPNService(self.app, self.namespace) + + self.short_header = ( + 'ID', + 'Name', + 'Router', + 'Subnet', + 'Flavor', + 'State', + 'Status', + ) + + self.short_data = ( + _vpnservice['id'], + _vpnservice['name'], + _vpnservice['router_id'], + _vpnservice['subnet_id'], + _vpnservice['flavor_id'], + _vpnservice['admin_state_up'], + _vpnservice['status'], + ) + + self.networkclient.vpn_services = mock.Mock(return_value=[_vpnservice]) + self.mocked = self.networkclient.vpn_services + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestSetVPNService(TestVPNService, common.TestSetVPNaaS): + + def setUp(self): + super(TestSetVPNService, self).setUp() + self.networkclient.update_vpn_service = mock.Mock( + return_value=_vpnservice) + self.mocked = self.networkclient.update_vpn_service + self.cmd = vpnservice.SetVPNSercice(self.app, self.namespace) + + def test_set_name(self): + target = self.resource['id'] + update = 'change' + arglist = [target, '--name', update] + verifylist = [ + (self.res, target), + ('name', update), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'name': update}) + self.assertIsNone(result) + + +class TestShowVPNService(TestVPNService, common.TestShowVPNaaS): + + def setUp(self): + super(TestShowVPNService, self).setUp() + self.networkclient.get_vpn_service = mock.Mock( + return_value=_vpnservice) + self.mocked = self.networkclient.get_vpn_service + self.cmd = vpnservice.ShowVPNService(self.app, self.namespace) diff --git a/neutronclient/tests/unit/test_casual_args.py b/neutronclient/tests/unit/test_casual_args.py new file mode 100644 index 000000000..cbf061617 --- /dev/null +++ b/neutronclient/tests/unit/test_casual_args.py @@ -0,0 +1,141 @@ +# 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 neutronclient.common import exceptions +from neutronclient.neutron import v2_0 as neutronV20 + + +class CLITestArgs(testtools.TestCase): + + def test_empty(self): + _mydict = neutronV20.parse_args_to_dict([]) + self.assertEqual({}, _mydict) + + def test_default_bool(self): + _specs = ['--my_bool', '--arg1', 'value1'] + _mydict = neutronV20.parse_args_to_dict(_specs) + self.assertTrue(_mydict['my_bool']) + + def test_bool_true(self): + _specs = ['--my-bool', 'type=bool', 'true', '--arg1', 'value1'] + _mydict = neutronV20.parse_args_to_dict(_specs) + self.assertTrue(_mydict['my_bool']) + + def test_bool_false(self): + _specs = ['--my_bool', 'type=bool', 'false', '--arg1', 'value1'] + _mydict = neutronV20.parse_args_to_dict(_specs) + self.assertFalse(_mydict['my_bool']) + + def test_int_and_str(self): + _specs = ['--my-int', 'type=int', '10', + '--my-str', 'type=str', 'value1'] + _mydict = neutronV20.parse_args_to_dict(_specs) + self.assertEqual(10, _mydict['my_int']) + self.assertEqual('value1', _mydict['my_str']) + + def test_nargs(self): + _specs = ['--tag', 'x', 'y', '--arg1', 'value1'] + _mydict = neutronV20.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, + neutronV20.parse_args_to_dict, _specs) + + def test_badarg_with_minus(self): + _specs = ['--arg1', 'value1', '-D'] + self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + + def test_goodarg_with_minus_number(self): + _specs = ['--arg1', 'value1', '-1', '-1.0'] + _mydict = neutronV20.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, + neutronV20.parse_args_to_dict, _specs) + + def test_badarg_early_type_specification(self): + _specs = ['type=dict', 'key=value'] + self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + + def test_arg(self): + _specs = ['--tag=t', '--arg1', 'value1'] + self.assertEqual('value1', + neutronV20.parse_args_to_dict(_specs)['arg1']) + + def test_arg_invalid_syntax(self): + _specs = ['--tag=t', '---arg1', 'value1'] + self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + + def test_dict_arg(self): + _specs = ['--tag=t', '--arg1', 'type=dict', 'key1=value1,key2=value2'] + arg1 = neutronV20.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 = neutronV20.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 = neutronV20.parse_args_to_dict(_specs)['arg1'] + self.assertEqual('value1', arg1[0]['key1']) + self.assertEqual('value2', arg1[0]['key2']) + + def test_parse_args_to_dict_bad_type(self): + _specs = ['--badtypearg', 'type=badtype', 'val1'] + ex = self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + self.assertEqual('Invalid value_specs --badtypearg type=badtype val1: ' + 'type badtype is not supported', + str(ex)) + + def test_clear_action(self): + _specs = ['--anyarg', 'action=clear'] + args = neutronV20.parse_args_to_dict(_specs) + self.assertIsNone(args['anyarg']) + + def test_bad_values_str_without_value(self): + _specs = ['--strarg', 'type=str'] + ex = self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + self.assertEqual('Invalid values_specs --strarg type=str', + str(ex)) + + def test_bad_values_list(self): + _specs = ['--listarg', 'list=true', 'type=str'] + self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + _specs = ['--listarg', 'type=list'] + self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) + _specs = ['--listarg', 'type=list', 'action=clear'] + self.assertRaises(exceptions.CommandError, + neutronV20.parse_args_to_dict, _specs) diff --git a/neutronclient/tests/unit/test_command_meta.py b/neutronclient/tests/unit/test_command_meta.py new file mode 100644 index 000000000..dedc3dd1c --- /dev/null +++ b/neutronclient/tests/unit/test_command_meta.py @@ -0,0 +1,41 @@ +# Copyright 2013 Intel +# Copyright 2013 Isaku Yamahata +# +# 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 neutronclient.neutron import v2_0 as neutronV20 + + +class TestCommandMeta(testtools.TestCase): + def test_neutron_command_meta_defines_log(self): + class FakeCommand(neutronV20.NeutronCommand): + pass + + self.assertTrue(hasattr(FakeCommand, 'log')) + self.assertIsInstance(FakeCommand.log, logging.getLoggerClass()) + self.assertEqual(__name__ + ".FakeCommand", FakeCommand.log.name) + + def test_neutron_command_log_defined_explicitly(self): + class FakeCommand(neutronV20.NeutronCommand): + log = None + + self.assertTrue(hasattr(FakeCommand, 'log')) + self.assertIsNone(FakeCommand.log) diff --git a/neutronclient/tests/unit/test_exceptions.py b/neutronclient/tests/unit/test_exceptions.py new file mode 100644 index 000000000..6e063244c --- /dev/null +++ b/neutronclient/tests/unit/test_exceptions.py @@ -0,0 +1,48 @@ +# 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 unittest import mock + +from oslo_utils import encodeutils +import testtools + +from neutronclient._i18n import _ +from neutronclient.common import exceptions + + +class TestExceptions(testtools.TestCase): + + def test_exception_print_with_unicode(self): + class TestException(exceptions.NeutronException): + message = _('Exception with %(reason)s') + + multibyte_unicode_string = u'\uff21\uff22\uff23' + e = TestException(reason=multibyte_unicode_string) + + with mock.patch.object(sys, 'stdout') as mock_stdout: + print(e) + + exc_str = 'Exception with %s' % multibyte_unicode_string + mock_stdout.assert_has_calls([mock.call.write(exc_str)]) + + def test_exception_message_with_encoded_unicode(self): + class TestException(exceptions.NeutronException): + message = _('Exception with %(reason)s') + + multibyte_string = u'\uff21\uff22\uff23' + multibyte_binary = encodeutils.safe_encode(multibyte_string) + e = TestException(reason=multibyte_binary) + self.assertEqual('Exception with %s' % multibyte_string, + str(e)) diff --git a/neutronclient/tests/unit/test_http.py b/neutronclient/tests/unit/test_http.py new file mode 100644 index 000000000..5680d4375 --- /dev/null +++ b/neutronclient/tests/unit/test_http.py @@ -0,0 +1,157 @@ +# 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 abc + +from oslo_utils import uuidutils +import osprofiler.profiler +import osprofiler.web +from requests_mock.contrib import fixture as mock_fixture +import testtools + +from neutronclient import client +from neutronclient.common import exceptions + + +AUTH_TOKEN = 'test_token' +END_URL = 'test_url' +METHOD = 'GET' +URL = 'http://test.test:1234/v2.0/test' +BODY = 'IAMFAKE' + + +class TestHTTPClientMixin(object, metaclass=abc.ABCMeta): + + def setUp(self): + super(TestHTTPClientMixin, self).setUp() + + self.requests = self.useFixture(mock_fixture.Fixture()) + self.http = self.initialize() + + @abc.abstractmethod + def initialize(self): + """Return client class, instance.""" + + def _test_headers(self, expected_headers, **kwargs): + # Test headers. + self.requests.register_uri(METHOD, URL, + request_headers=expected_headers) + self.http.request(URL, METHOD, **kwargs) + self.assertEqual(kwargs.get('body'), self.requests.last_request.body) + + def test_headers_without_body(self): + self._test_headers({'Accept': 'application/json'}) + + def test_headers_with_body(self): + headers = {'Accept': 'application/json', + 'Content-Type': 'application/json'} + self._test_headers(headers, body=BODY) + + def test_headers_without_body_with_content_type(self): + headers = {'Accept': 'application/json'} + self._test_headers(headers, content_type='application/json') + + def test_headers_with_body_with_content_type(self): + headers = {'Accept': 'application/json', + 'Content-Type': 'application/json'} + self._test_headers(headers, body=BODY, content_type='application/json') + + def test_headers_defined_in_headers(self): + headers = {'Accept': 'application/json', + 'Content-Type': 'application/json'} + self._test_headers(headers, body=BODY, headers=headers) + + def test_osprofiler_headers_are_injected(self): + osprofiler.profiler.init('SWORDFISH') + self.addCleanup(osprofiler.profiler.clean) + + headers = {'Accept': 'application/json'} + headers.update(osprofiler.web.get_trace_id_headers()) + self._test_headers(headers) + + +class TestHTTPClient(TestHTTPClientMixin, testtools.TestCase): + + def initialize(self): + return client.HTTPClient(token=AUTH_TOKEN, endpoint_url=END_URL) + + def test_request_error(self): + def cb(*args, **kwargs): + raise Exception('error msg') + + self.requests.get(URL, body=cb) + self.assertRaises( + exceptions.ConnectionFailed, + self.http._cs_request, + URL, METHOD + ) + + def test_request_success(self): + text = 'test content' + self.requests.register_uri(METHOD, URL, text=text) + + resp, resp_text = self.http._cs_request(URL, METHOD) + self.assertEqual(200, resp.status_code) + self.assertEqual(text, resp_text) + + def test_request_unauthorized(self): + text = 'unauthorized message' + self.requests.register_uri(METHOD, URL, status_code=401, text=text) + e = self.assertRaises(exceptions.Unauthorized, + self.http._cs_request, URL, METHOD) + self.assertEqual(text, e.message) + + def test_request_forbidden_is_returned_to_caller(self): + text = 'forbidden message' + self.requests.register_uri(METHOD, URL, status_code=403, text=text) + + resp, resp_text = self.http._cs_request(URL, METHOD) + self.assertEqual(403, resp.status_code) + self.assertEqual(text, resp_text) + + def test_do_request_success(self): + text = 'test content' + self.requests.register_uri(METHOD, END_URL + URL, text=text) + + resp, resp_text = self.http.do_request(URL, METHOD) + self.assertEqual(200, resp.status_code) + self.assertEqual(text, resp_text) + + def test_do_request_with_headers_success(self): + text = 'test content' + self.requests.register_uri(METHOD, END_URL + URL, text=text, + request_headers={'key': 'value'}) + + resp, resp_text = self.http.do_request(URL, METHOD, + headers={'key': 'value'}) + self.assertEqual(200, resp.status_code) + self.assertEqual(text, resp_text) + + +class TestHTTPClientWithReqId(TestHTTPClientMixin, testtools.TestCase): + """Tests for when global_request_id is set.""" + + def initialize(self): + self.req_id = "req-%s" % uuidutils.generate_uuid() + return client.HTTPClient(token=AUTH_TOKEN, endpoint_url=END_URL, + global_request_id=self.req_id) + + def test_request_success(self): + headers = { + 'Accept': 'application/json', + 'X-OpenStack-Request-ID': self.req_id + } + self.requests.register_uri(METHOD, URL, request_headers=headers) + self.http.request(URL, METHOD) diff --git a/neutronclient/tests/unit/test_utils.py b/neutronclient/tests/unit/test_utils.py new file mode 100644 index 000000000..4806682b6 --- /dev/null +++ b/neutronclient/tests/unit/test_utils.py @@ -0,0 +1,161 @@ +# 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 + +from oslo_utils import netutils + +import testtools + +from neutronclient.common import exceptions +from neutronclient.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_invalid_string_to_dictionary(self): + input_str = 'invalid' + self.assertRaises(argparse.ArgumentTypeError, + utils.str2dict, input_str) + + def test_string_with_comma_value_to_dictionary(self): + input_str = ('opt_name=classless-static-route,' + 'opt_value=169.254.169.254/32,10.0.0.1') + expected = {'opt_name': 'classless-static-route', + 'opt_value': '169.254.169.254/32,10.0.0.1'} + self.assertEqual(expected, utils.str2dict(input_str)) + + def test_str2dict_optional_keys(self): + self.assertDictEqual({'key1': 'value1'}, + utils.str2dict('key1=value1', + optional_keys=['key1', 'key2'])) + self.assertDictEqual({'key1': 'value1', 'key2': 'value2'}, + utils.str2dict('key1=value1,key2=value2', + optional_keys=['key1', 'key2'])) + e = self.assertRaises(argparse.ArgumentTypeError, + utils.str2dict, + 'key1=value1,key2=value2,key3=value3', + optional_keys=['key1', 'key2']) + self.assertEqual("Invalid key(s) 'key3' specified. " + "Valid key(s): 'key1, key2'.", + str(e)) + + def test_str2dict_required_keys(self): + self.assertDictEqual( + {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}, + utils.str2dict('key1=value1,key2=value2,key3=value3', + required_keys=['key1', 'key2'], + optional_keys=['key3'])) + self.assertDictEqual( + {'key1': 'value1', 'key2': 'value2'}, + utils.str2dict('key1=value1,key2=value2', + required_keys=['key1', 'key2'])) + e = self.assertRaises(argparse.ArgumentTypeError, + utils.str2dict, 'key1=value1', + required_keys=['key1', 'key2']) + self.assertEqual("Required key(s) 'key2' not specified.", str(e)) + + 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) + + def test_is_cidr(self): + self.assertTrue(netutils.is_valid_cidr('10.10.10.0/24')) + self.assertFalse(netutils.is_valid_cidr('10.10.10..0/24')) + self.assertFalse(netutils.is_valid_cidr('wrong_cidr_format')) + + +class ImportClassTestCase(testtools.TestCase): + def test_get_client_class_invalid_version(self): + self.assertRaises( + exceptions.UnsupportedVersion, + utils.get_client_class, 'image', '2', {'image': '2'}) diff --git a/neutronclient/tests/unit/test_validators.py b/neutronclient/tests/unit/test_validators.py new file mode 100644 index 000000000..a92496baa --- /dev/null +++ b/neutronclient/tests/unit/test_validators.py @@ -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 neutronclient.common import exceptions +from neutronclient.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)) diff --git a/neutronclient/v2_0/__init__.py b/neutronclient/v2_0/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutronclient/v2_0/client.py b/neutronclient/v2_0/client.py new file mode 100644 index 000000000..82676b1c1 --- /dev/null +++ b/neutronclient/v2_0/client.py @@ -0,0 +1,2631 @@ +# Copyright 2012 OpenStack Foundation. +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# Copyright 2017 FUJITSU LIMITED +# 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 inspect +import itertools +import logging +import re +import time +import urllib.parse as urlparse + +import debtcollector.renames +from keystoneauth1 import exceptions as ksa_exc +import requests + +from neutronclient._i18n import _ +from neutronclient import client +from neutronclient.common import exceptions +from neutronclient.common import extension as client_extension +from neutronclient.common import serializer +from neutronclient.common import utils + + +_logger = logging.getLogger(__name__) + +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 exception_handler_v20(status_code, error_content): + """Exception handler for API v2.0 client. + + This routine generates the appropriate Neutron 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 + request_ids = error_content.request_ids + if isinstance(error_content, dict): + error_dict = error_content.get('NeutronError') + # Find real error type + client_exc = None + if error_dict: + # If Neutron 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'] + # If corresponding exception is defined, use it. + client_exc = getattr(exceptions, '%sClient' % error_type, None) + except Exception: + error_message = "%s" % error_dict + else: + error_message = None + if isinstance(error_content, dict): + error_message = error_content.get('message') + if not error_message: + # If we end up here the exception was not a neutron error + error_message = "%s-%s" % (status_code, error_content) + + # If an exception corresponding to the error type is not found, + # look up per status-code client exception. + if not client_exc: + client_exc = exceptions.HTTP_EXCEPTION_MAP.get(status_code) + # If there is no exception per status-code, + # Use NeutronClientException as fallback. + if not client_exc: + client_exc = exceptions.NeutronClientException + + raise client_exc(message=error_message, + status_code=status_code, + request_ids=request_ids) + + +class _RequestIdMixin(object): + """Wrapper class to expose x-openstack-request-id to the caller.""" + def _request_ids_setup(self): + self._request_ids = [] + + @property + def request_ids(self): + return self._request_ids + + def _append_request_ids(self, resp): + """Add request_ids as an attribute to the object + + :param resp: Response object or list of Response objects + """ + if isinstance(resp, list): + # Add list of request_ids if response is of type list. + for resp_obj in resp: + self._append_request_id(resp_obj) + elif resp is not None: + # Add request_ids if response contains single object. + self._append_request_id(resp) + + def _append_request_id(self, resp): + if isinstance(resp, requests.Response): + # Extract 'x-openstack-request-id' from headers if + # response is a Response object. + request_id = resp.headers.get('x-openstack-request-id') + else: + # If resp is of type string. + request_id = resp + if request_id: + self._request_ids.append(request_id) + + +class _DictWithMeta(dict, _RequestIdMixin): + def __init__(self, values, resp): + super(_DictWithMeta, self).__init__(values) + self._request_ids_setup() + self._append_request_ids(resp) + + +class _TupleWithMeta(tuple, _RequestIdMixin): + def __new__(cls, values, resp): + return super(_TupleWithMeta, cls).__new__(cls, values) + + def __init__(self, values, resp): + self._request_ids_setup() + self._append_request_ids(resp) + + +class _StrWithMeta(str, _RequestIdMixin): + def __new__(cls, value, resp): + return super(_StrWithMeta, cls).__new__(cls, value) + + def __init__(self, values, resp): + self._request_ids_setup() + self._append_request_ids(resp) + + +class _GeneratorWithMeta(_RequestIdMixin): + def __init__(self, paginate_func, collection, path, **params): + self.paginate_func = paginate_func + self.collection = collection + self.path = path + self.params = params + self.generator = None + self._request_ids_setup() + + def _paginate(self): + for r in self.paginate_func( + self.collection, self.path, **self.params): + yield r, r.request_ids + + def __iter__(self): + return self + + # Python 3 compatibility + def __next__(self): + return self.next() + + def next(self): + if not self.generator: + self.generator = self._paginate() + + try: + obj, req_id = next(self.generator) + self._append_request_ids(req_id) + except StopIteration: + raise StopIteration() + + return obj + + +class ClientBase(object): + """Client for the OpenStack Neutron v2.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: DEPRECATED! Use project_name instead. + :param string project_name: Project name. (optional) + :param string tenant_id: DEPRECATED! Use project_id instead. + :param string project_id: Project 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 neutron + 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 cert: A client certificate to pass to requests. These are of the + same form as requests expects. Either a single filename + containing both the certificate and key or a tuple containing + the path to the certificate then a path to the key. (optional) + :param integer retries: How many times idempotent (GET, PUT, DELETE) + requests to Neutron 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 neutronclient.v2_0 import client + neutron = client.Client(username=USER, + password=PASS, + project_name=PROJECT_NAME, + auth_url=KEYSTONE_URL) + + nets = neutron.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 = {} + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + def __init__(self, **kwargs): + """Initialize a new client for the Neutron v2.0 API.""" + super(ClientBase, self).__init__() + _logger.warning("The python binding code in neutronclient is " + "deprecated in favor of OpenstackSDK, please use " + "that as this will be removed in a future release.") + self.retries = kwargs.pop('retries', 0) + self.raise_errors = kwargs.pop('raise_errors', True) + self.httpclient = client.construct_http_client(**kwargs) + self.version = '2.0' + self.action_prefix = "/v%s" % (self.version) + self.retry_interval = 1 + + def _handle_fault_response(self, status_code, response_body, resp): + # 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 + # Neutron error + des_error_body = {'message': response_body} + error_body = self._convert_into_with_meta(des_error_body, resp) + # Raise the appropriate exception + exception_handler_v20(status_code, error_body) + + def do_request(self, method, action, body=None, headers=None, params=None): + # Add format and project_id + action = self.action_prefix + action + if isinstance(params, 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, + headers=headers) + + status_code = resp.status_code + if status_code in (requests.codes.ok, + requests.codes.created, + requests.codes.accepted, + requests.codes.no_content): + data = self.deserialize(replybody, status_code) + return self._convert_into_with_meta(data, resp) + else: + if not replybody: + replybody = resp.reason + self._handle_fault_response(status_code, replybody, resp) + + def get_auth_info(self): + return self.httpclient.get_auth_info() + + def serialize(self, data): + """Serializes a dictionary into JSON. + + A dictionary with a single key can be passed and it can contain any + structure. + """ + if data is None: + return None + elif isinstance(data, dict): + return serializer.Serializer().serialize(data) + else: + raise Exception(_("Unable to serialize object of type = '%s'") % + type(data)) + + def deserialize(self, data, status_code): + """Deserializes a JSON string into a dictionary.""" + if not data: + return data + return serializer.Serializer().deserialize( + data)['body'] + + 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, ksa_exc.ConnectionError): + # Exception has already been logged by do_request() + if i < self.retries: + _logger.debug('Retrying connection to Neutron service') + time.sleep(self.retry_interval) + elif self.raise_errors: + raise + + if self.retries: + msg = (_("Failed to connect to Neutron server after %d attempts") + % max_attempts) + else: + msg = _("Failed to connect Neutron 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 = [] + request_ids = [] + for r in self._pagination(collection, path, **params): + res.extend(r[collection]) + request_ids.extend(r.request_ids) + return _DictWithMeta({collection: res}, request_ids) + else: + return _GeneratorWithMeta(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 + + def _convert_into_with_meta(self, item, resp): + if item: + if isinstance(item, dict): + return _DictWithMeta(item, resp) + elif isinstance(item, str): + return _StrWithMeta(item, resp) + else: + return _TupleWithMeta((), resp) + + def get_resource_plural(self, resource): + for k in self.EXTED_PLURALS: + if self.EXTED_PLURALS[k] == resource: + return k + return resource + 's' + + def find_resource_by_id(self, resource, resource_id, cmd_resource=None, + parent_id=None, fields=None): + if not cmd_resource: + cmd_resource = resource + cmd_resource_plural = self.get_resource_plural(cmd_resource) + resource_plural = self.get_resource_plural(resource) + # TODO(amotoki): Use show_%s instead of list_%s + obj_lister = getattr(self, "list_%s" % cmd_resource_plural) + # perform search by id only if we are passing a valid UUID + match = re.match(UUID_PATTERN, resource_id) + collection = resource_plural + if match: + params = {'id': resource_id} + if fields: + params['fields'] = fields + if parent_id: + data = obj_lister(parent_id, **params) + else: + data = obj_lister(**params) + if data and data[collection]: + return data[collection][0] + not_found_message = (_("Unable to find %(resource)s with id " + "'%(id)s'") % + {'resource': resource, 'id': resource_id}) + # 404 is raised by exceptions.NotFound to simulate serverside behavior + raise exceptions.NotFound(message=not_found_message) + + def _find_resource_by_name(self, resource, name, project_id=None, + cmd_resource=None, parent_id=None, fields=None): + if not cmd_resource: + cmd_resource = resource + cmd_resource_plural = self.get_resource_plural(cmd_resource) + resource_plural = self.get_resource_plural(resource) + obj_lister = getattr(self, "list_%s" % cmd_resource_plural) + params = {'name': name} + if fields: + params['fields'] = fields + if project_id: + params['tenant_id'] = project_id + if parent_id: + data = obj_lister(parent_id, **params) + else: + data = obj_lister(**params) + collection = resource_plural + info = data[collection] + if len(info) > 1: + raise exceptions.NeutronClientNoUniqueMatch(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 raised by exceptions.NotFound + # to simulate serverside behavior + raise exceptions.NotFound(message=not_found_message) + else: + return info[0] + + def find_resource(self, resource, name_or_id, project_id=None, + cmd_resource=None, parent_id=None, fields=None): + try: + return self.find_resource_by_id(resource, name_or_id, + cmd_resource, parent_id, fields) + except exceptions.NotFound: + try: + return self._find_resource_by_name( + resource, name_or_id, project_id, + cmd_resource, parent_id, fields) + except exceptions.NotFound: + not_found_message = (_("Unable to find %(resource)s with name " + "or id '%(name_or_id)s'") % + {'resource': resource, + 'name_or_id': name_or_id}) + raise exceptions.NotFound( + message=not_found_message) + + +class Client(ClientBase): + + networks_path = "/networks" + network_path = "/networks/%s" + ports_path = "/ports" + port_path = "/ports/%s" + port_bindings_path = "/ports/%s/bindings" + port_binding_path = "/ports/%s/bindings/%s" + port_binding_path_activate = "/ports/%s/bindings/%s/activate" + subnets_path = "/subnets" + subnet_path = "/subnets/%s" + onboard_network_subnets_path = "/subnetpools/%s/onboard_network_subnets" + subnetpools_path = "/subnetpools" + subnetpool_path = "/subnetpools/%s" + address_scopes_path = "/address-scopes" + address_scope_path = "/address-scopes/%s" + quotas_path = "/quotas" + quota_path = "/quotas/%s" + quota_default_path = "/quotas/%s/default" + quota_details_path = "/quotas/%s/details.json" + extensions_path = "/extensions" + extension_path = "/extensions/%s" + routers_path = "/routers" + router_path = "/routers/%s" + floatingips_path = "/floatingips" + floatingip_path = "/floatingips/%s" + port_forwardings_path = "/floatingips/%s/port_forwardings" + port_forwarding_path = "/floatingips/%s/port_forwardings/%s" + security_groups_path = "/security-groups" + security_group_path = "/security-groups/%s" + security_group_rules_path = "/security-group-rules" + security_group_rule_path = "/security-group-rules/%s" + segments_path = "/segments" + segment_path = "/segments/%s" + + sfc_flow_classifiers_path = "/sfc/flow_classifiers" + sfc_flow_classifier_path = "/sfc/flow_classifiers/%s" + sfc_port_pairs_path = "/sfc/port_pairs" + sfc_port_pair_path = "/sfc/port_pairs/%s" + sfc_port_pair_groups_path = "/sfc/port_pair_groups" + sfc_port_pair_group_path = "/sfc/port_pair_groups/%s" + sfc_port_chains_path = "/sfc/port_chains" + sfc_port_chain_path = "/sfc/port_chains/%s" + sfc_service_graphs_path = "/sfc/service_graphs" + sfc_service_graph_path = "/sfc/service_graphs/%s" + + endpoint_groups_path = "/vpn/endpoint-groups" + endpoint_group_path = "/vpn/endpoint-groups/%s" + vpnservices_path = "/vpn/vpnservices" + vpnservice_path = "/vpn/vpnservices/%s" + ipsecpolicies_path = "/vpn/ipsecpolicies" + ipsecpolicy_path = "/vpn/ipsecpolicies/%s" + ikepolicies_path = "/vpn/ikepolicies" + ikepolicy_path = "/vpn/ikepolicies/%s" + ipsec_site_connections_path = "/vpn/ipsec-site-connections" + ipsec_site_connection_path = "/vpn/ipsec-site-connections/%s" + + lbaas_loadbalancers_path = "/lbaas/loadbalancers" + lbaas_loadbalancer_path = "/lbaas/loadbalancers/%s" + lbaas_loadbalancer_path_stats = "/lbaas/loadbalancers/%s/stats" + lbaas_loadbalancer_path_status = "/lbaas/loadbalancers/%s/statuses" + lbaas_listeners_path = "/lbaas/listeners" + lbaas_listener_path = "/lbaas/listeners/%s" + lbaas_l7policies_path = "/lbaas/l7policies" + lbaas_l7policy_path = lbaas_l7policies_path + "/%s" + lbaas_l7rules_path = lbaas_l7policy_path + "/rules" + lbaas_l7rule_path = lbaas_l7rules_path + "/%s" + lbaas_pools_path = "/lbaas/pools" + lbaas_pool_path = "/lbaas/pools/%s" + lbaas_healthmonitors_path = "/lbaas/healthmonitors" + lbaas_healthmonitor_path = "/lbaas/healthmonitors/%s" + lbaas_members_path = lbaas_pool_path + "/members" + lbaas_member_path = lbaas_pool_path + "/members/%s" + + vips_path = "/lb/vips" + vip_path = "/lb/vips/%s" + pools_path = "/lb/pools" + pool_path = "/lb/pools/%s" + pool_path_stats = "/lb/pools/%s/stats" + members_path = "/lb/members" + member_path = "/lb/members/%s" + health_monitors_path = "/lb/health_monitors" + health_monitor_path = "/lb/health_monitors/%s" + associate_pool_health_monitors_path = "/lb/pools/%s/health_monitors" + disassociate_pool_health_monitors_path = ( + "/lb/pools/%(pool)s/health_monitors/%(health_monitor)s") + qos_queues_path = "/qos-queues" + qos_queue_path = "/qos-queues/%s" + agents_path = "/agents" + agent_path = "/agents/%s" + network_gateways_path = "/network-gateways" + network_gateway_path = "/network-gateways/%s" + gateway_devices_path = "/gateway-devices" + gateway_device_path = "/gateway-devices/%s" + service_providers_path = "/service-providers" + metering_labels_path = "/metering/metering-labels" + metering_label_path = "/metering/metering-labels/%s" + metering_label_rules_path = "/metering/metering-label-rules" + metering_label_rule_path = "/metering/metering-label-rules/%s" + + DHCP_NETS = '/dhcp-networks' + DHCP_AGENTS = '/dhcp-agents' + L3_ROUTERS = '/l3-routers' + L3_AGENTS = '/l3-agents' + LOADBALANCER_POOLS = '/loadbalancer-pools' + LOADBALANCER_AGENT = '/loadbalancer-agent' + AGENT_LOADBALANCERS = '/agent-loadbalancers' + LOADBALANCER_HOSTING_AGENT = '/loadbalancer-hosting-agent' + firewall_rules_path = "/fw/firewall_rules" + firewall_rule_path = "/fw/firewall_rules/%s" + firewall_policies_path = "/fw/firewall_policies" + firewall_policy_path = "/fw/firewall_policies/%s" + firewall_policy_insert_path = "/fw/firewall_policies/%s/insert_rule" + firewall_policy_remove_path = "/fw/firewall_policies/%s/remove_rule" + firewalls_path = "/fw/firewalls" + firewall_path = "/fw/firewalls/%s" + fwaas_firewall_groups_path = "/fwaas/firewall_groups" + fwaas_firewall_group_path = "/fwaas/firewall_groups/%s" + fwaas_firewall_rules_path = "/fwaas/firewall_rules" + fwaas_firewall_rule_path = "/fwaas/firewall_rules/%s" + fwaas_firewall_policies_path = "/fwaas/firewall_policies" + fwaas_firewall_policy_path = "/fwaas/firewall_policies/%s" + fwaas_firewall_policy_insert_path = \ + "/fwaas/firewall_policies/%s/insert_rule" + fwaas_firewall_policy_remove_path = \ + "/fwaas/firewall_policies/%s/remove_rule" + rbac_policies_path = "/rbac-policies" + rbac_policy_path = "/rbac-policies/%s" + qos_policies_path = "/qos/policies" + qos_policy_path = "/qos/policies/%s" + qos_bandwidth_limit_rules_path = "/qos/policies/%s/bandwidth_limit_rules" + qos_bandwidth_limit_rule_path = "/qos/policies/%s/bandwidth_limit_rules/%s" + qos_packet_rate_limit_rules_path = \ + "/qos/policies/%s/packet_rate_limit_rules" + qos_packet_rate_limit_rule_path = \ + "/qos/policies/%s/packet_rate_limit_rules/%s" + qos_dscp_marking_rules_path = "/qos/policies/%s/dscp_marking_rules" + qos_dscp_marking_rule_path = "/qos/policies/%s/dscp_marking_rules/%s" + qos_minimum_bandwidth_rules_path = \ + "/qos/policies/%s/minimum_bandwidth_rules" + qos_minimum_bandwidth_rule_path = \ + "/qos/policies/%s/minimum_bandwidth_rules/%s" + qos_minimum_packet_rate_rules_path = \ + "/qos/policies/%s/minimum_packet_rate_rules" + qos_minimum_packet_rate_rule_path = \ + "/qos/policies/%s/minimum_packet_rate_rules/%s" + qos_rule_types_path = "/qos/rule-types" + qos_rule_type_path = "/qos/rule-types/%s" + flavors_path = "/flavors" + flavor_path = "/flavors/%s" + service_profiles_path = "/service_profiles" + service_profile_path = "/service_profiles/%s" + flavor_profile_bindings_path = flavor_path + service_profiles_path + flavor_profile_binding_path = flavor_path + service_profile_path + availability_zones_path = "/availability_zones" + auto_allocated_topology_path = "/auto-allocated-topology/%s" + BGP_DRINSTANCES = "/bgp-drinstances" + BGP_DRINSTANCE = "/bgp-drinstance/%s" + BGP_DRAGENTS = "/bgp-dragents" + BGP_DRAGENT = "/bgp-dragents/%s" + bgp_speakers_path = "/bgp-speakers" + bgp_speaker_path = "/bgp-speakers/%s" + bgp_peers_path = "/bgp-peers" + bgp_peer_path = "/bgp-peers/%s" + network_ip_availabilities_path = '/network-ip-availabilities' + network_ip_availability_path = '/network-ip-availabilities/%s' + tags_path = "/%s/%s/tags" + tag_path = "/%s/%s/tags/%s" + trunks_path = "/trunks" + trunk_path = "/trunks/%s" + subports_path = "/trunks/%s/get_subports" + subports_add_path = "/trunks/%s/add_subports" + subports_remove_path = "/trunks/%s/remove_subports" + bgpvpns_path = "/bgpvpn/bgpvpns" + bgpvpn_path = "/bgpvpn/bgpvpns/%s" + bgpvpn_network_associations_path =\ + "/bgpvpn/bgpvpns/%s/network_associations" + bgpvpn_network_association_path =\ + "/bgpvpn/bgpvpns/%s/network_associations/%s" + bgpvpn_router_associations_path = "/bgpvpn/bgpvpns/%s/router_associations" + bgpvpn_router_association_path =\ + "/bgpvpn/bgpvpns/%s/router_associations/%s" + bgpvpn_port_associations_path = "/bgpvpn/bgpvpns/%s/port_associations" + bgpvpn_port_association_path = "/bgpvpn/bgpvpns/%s/port_associations/%s" + network_logs_path = "/log/logs" + network_log_path = "/log/logs/%s" + network_loggables_path = "/log/loggable-resources" + + # API has no way to report plurals, so we have to hard code them + EXTED_PLURALS = {'routers': 'router', + 'floatingips': 'floatingip', + 'service_types': 'service_type', + 'service_definitions': 'service_definition', + 'security_groups': 'security_group', + 'security_group_rules': 'security_group_rule', + 'segments': 'segment', + 'ipsecpolicies': 'ipsecpolicy', + 'ikepolicies': 'ikepolicy', + 'ipsec_site_connections': 'ipsec_site_connection', + 'vpnservices': 'vpnservice', + 'endpoint_groups': 'endpoint_group', + 'vips': 'vip', + 'pools': 'pool', + 'members': 'member', + 'health_monitors': 'health_monitor', + 'quotas': 'quota', + 'service_providers': 'service_provider', + 'firewall_rules': 'firewall_rule', + 'firewall_policies': 'firewall_policy', + 'firewalls': 'firewall', + 'fwaas_firewall_rules': 'fwaas_firewall_rule', + 'fwaas_firewall_policies': 'fwaas_firewall_policy', + 'fwaas_firewall_groups': 'fwaas_firewall_group', + 'metering_labels': 'metering_label', + 'metering_label_rules': 'metering_label_rule', + 'loadbalancers': 'loadbalancer', + 'listeners': 'listener', + 'l7rules': 'l7rule', + 'l7policies': 'l7policy', + 'lbaas_l7policies': 'lbaas_l7policy', + 'lbaas_pools': 'lbaas_pool', + 'lbaas_healthmonitors': 'lbaas_healthmonitor', + 'lbaas_members': 'lbaas_member', + 'healthmonitors': 'healthmonitor', + 'rbac_policies': 'rbac_policy', + 'address_scopes': 'address_scope', + 'qos_policies': 'qos_policy', + 'policies': 'policy', + 'bandwidth_limit_rules': 'bandwidth_limit_rule', + 'packet_rate_limit_rules': 'packet_rate_limit_rule', + 'minimum_bandwidth_rules': 'minimum_bandwidth_rule', + 'minimum_packet_rate_rules': 'minimum_packet_rate_rule', + 'rules': 'rule', + 'dscp_marking_rules': 'dscp_marking_rule', + 'rule_types': 'rule_type', + 'flavors': 'flavor', + 'bgp_speakers': 'bgp_speaker', + 'bgp_peers': 'bgp_peer', + 'network_ip_availabilities': 'network_ip_availability', + 'trunks': 'trunk', + 'bgpvpns': 'bgpvpn', + 'network_associations': 'network_association', + 'router_associations': 'router_association', + 'port_associations': 'port_association', + 'flow_classifiers': 'flow_classifier', + 'port_pairs': 'port_pair', + 'port_pair_groups': 'port_pair_group', + 'port_chains': 'port_chain', + 'service_graphs': 'service_graph', + 'logs': 'log', + 'loggable_resources': 'loggable_resource', + } + + def list_ext(self, collection, path, retrieve_all, **_params): + """Client extension hook for list.""" + return self.list(collection, path, retrieve_all, **_params) + + def show_ext(self, path, id, **_params): + """Client extension hook for show.""" + return self.get(path % id, params=_params) + + def create_ext(self, path, body=None): + """Client extension hook for create.""" + return self.post(path, body=body) + + def update_ext(self, path, id, body=None): + """Client extension hook for update.""" + return self.put(path % id, body=body) + + def delete_ext(self, path, id): + """Client extension hook for delete.""" + return self.delete(path % id) + + def get_quotas_tenant(self, **_params): + """Fetch project info for following quota operation.""" + return self.get(self.quota_path % 'tenant', params=_params) + + def list_quotas(self, **_params): + """Fetch all projects' quotas.""" + return self.get(self.quotas_path, params=_params) + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + def show_quota(self, project_id, **_params): + """Fetch information of a certain project's quotas.""" + return self.get(self.quota_path % (project_id), params=_params) + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + def show_quota_details(self, project_id, **_params): + """Fetch information of a certain project's quota details.""" + return self.get(self.quota_details_path % (project_id), + params=_params) + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + def show_quota_default(self, project_id, **_params): + """Fetch information of a certain project's default quotas.""" + return self.get(self.quota_default_path % (project_id), params=_params) + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + def update_quota(self, project_id, body=None): + """Update a project's quotas.""" + return self.put(self.quota_path % (project_id), body=body) + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + def delete_quota(self, project_id): + """Delete the specified project's quota values.""" + return self.delete(self.quota_path % (project_id)) + + def list_extensions(self, **_params): + """Fetch a list of all extensions on server side.""" + return self.get(self.extensions_path, params=_params) + + def show_extension(self, ext_alias, **_params): + """Fetches information of a certain extension.""" + return self.get(self.extension_path % ext_alias, params=_params) + + def list_ports(self, retrieve_all=True, **_params): + """Fetches a list of all ports for a project.""" + # Pass filters in "params" argument to do_request + return self.list('ports', self.ports_path, retrieve_all, + **_params) + + def show_port(self, port, **_params): + """Fetches information of a certain port.""" + return self.get(self.port_path % (port), params=_params) + + def create_port(self, body=None): + """Creates a new port.""" + return self.post(self.ports_path, body=body) + + def update_port(self, port, body=None, revision_number=None): + """Updates a port.""" + return self._update_resource(self.port_path % (port), body=body, + revision_number=revision_number) + + def delete_port(self, port): + """Deletes the specified port.""" + return self.delete(self.port_path % (port)) + + def create_port_binding(self, port_id, body=None): + """Creates a new port binding.""" + return self.post(self.port_bindings_path % port_id, body=body) + + def delete_port_binding(self, port_id, host_id): + """Deletes the specified port binding.""" + return self.delete(self.port_binding_path % (port_id, host_id)) + + def show_port_binding(self, port_id, host_id, **_params): + """Fetches information for a certain port binding.""" + return self.get(self.port_binding_path % (port_id, host_id), + params=_params) + + def list_port_bindings(self, port_id, retrieve_all=True, **_params): + """Fetches a list of all bindings for a certain port.""" + return self.list('bindings', self.port_bindings_path % port_id, + retrieve_all, **_params) + + def activate_port_binding(self, port_id, host_id): + """Activates a port binding.""" + return self.put(self.port_binding_path_activate % (port_id, host_id)) + + def list_networks(self, retrieve_all=True, **_params): + """Fetches a list of all networks for a project.""" + # Pass filters in "params" argument to do_request + return self.list('networks', self.networks_path, retrieve_all, + **_params) + + def show_network(self, network, **_params): + """Fetches information of a certain network.""" + return self.get(self.network_path % (network), params=_params) + + def create_network(self, body=None): + """Creates a new network.""" + return self.post(self.networks_path, body=body) + + def update_network(self, network, body=None, revision_number=None): + """Updates a network.""" + return self._update_resource(self.network_path % (network), body=body, + revision_number=revision_number) + + def delete_network(self, network): + """Deletes the specified network.""" + return self.delete(self.network_path % (network)) + + def list_subnets(self, retrieve_all=True, **_params): + """Fetches a list of all subnets for a project.""" + return self.list('subnets', self.subnets_path, retrieve_all, + **_params) + + def show_subnet(self, subnet, **_params): + """Fetches information of a certain subnet.""" + return self.get(self.subnet_path % (subnet), params=_params) + + def create_subnet(self, body=None): + """Creates a new subnet.""" + return self.post(self.subnets_path, body=body) + + def update_subnet(self, subnet, body=None, revision_number=None): + """Updates a subnet.""" + return self._update_resource(self.subnet_path % (subnet), body=body, + revision_number=revision_number) + + def delete_subnet(self, subnet): + """Deletes the specified subnet.""" + return self.delete(self.subnet_path % (subnet)) + + def list_subnetpools(self, retrieve_all=True, **_params): + """Fetches a list of all subnetpools for a project.""" + return self.list('subnetpools', self.subnetpools_path, retrieve_all, + **_params) + + def show_subnetpool(self, subnetpool, **_params): + """Fetches information of a certain subnetpool.""" + return self.get(self.subnetpool_path % (subnetpool), params=_params) + + def create_subnetpool(self, body=None): + """Creates a new subnetpool.""" + return self.post(self.subnetpools_path, body=body) + + def update_subnetpool(self, subnetpool, body=None, revision_number=None): + """Updates a subnetpool.""" + return self._update_resource(self.subnetpool_path % (subnetpool), + body=body, + revision_number=revision_number) + + def delete_subnetpool(self, subnetpool): + """Deletes the specified subnetpool.""" + return self.delete(self.subnetpool_path % (subnetpool)) + + def list_routers(self, retrieve_all=True, **_params): + """Fetches a list of all routers for a project.""" + # Pass filters in "params" argument to do_request + return self.list('routers', self.routers_path, retrieve_all, + **_params) + + def show_router(self, router, **_params): + """Fetches information of a certain router.""" + return self.get(self.router_path % (router), params=_params) + + def create_router(self, body=None): + """Creates a new router.""" + return self.post(self.routers_path, body=body) + + def update_router(self, router, body=None, revision_number=None): + """Updates a router.""" + return self._update_resource(self.router_path % (router), body=body, + revision_number=revision_number) + + def delete_router(self, router): + """Deletes the specified router.""" + return self.delete(self.router_path % (router)) + + def list_address_scopes(self, retrieve_all=True, **_params): + """Fetches a list of all address scopes for a project.""" + return self.list('address_scopes', self.address_scopes_path, + retrieve_all, **_params) + + def show_address_scope(self, address_scope, **_params): + """Fetches information of a certain address scope.""" + return self.get(self.address_scope_path % (address_scope), + params=_params) + + def create_address_scope(self, body=None): + """Creates a new address scope.""" + return self.post(self.address_scopes_path, body=body) + + def update_address_scope(self, address_scope, body=None): + """Updates a address scope.""" + return self.put(self.address_scope_path % (address_scope), body=body) + + def delete_address_scope(self, address_scope): + """Deletes the specified address scope.""" + return self.delete(self.address_scope_path % (address_scope)) + + def add_interface_router(self, router, body=None): + """Adds an internal network interface to the specified router.""" + return self.put((self.router_path % router) + "/add_router_interface", + body=body) + + def remove_interface_router(self, router, body=None): + """Removes an internal network interface from the specified router.""" + return self.put((self.router_path % router) + + "/remove_router_interface", body=body) + + def add_extra_routes_to_router(self, router, body=None): + """Adds extra routes to the specified router.""" + return self.put((self.router_path % router) + "/add_extraroutes", + body=body) + + def remove_extra_routes_from_router(self, router, body=None): + """Removes extra routes from the specified router.""" + return self.put((self.router_path % router) + "/remove_extraroutes", + body=body) + + def add_gateway_router(self, router, body=None): + """Adds an external network gateway to the specified router.""" + return self.put((self.router_path % router), + body={'router': {'external_gateway_info': body}}) + + def remove_gateway_router(self, router): + """Removes an external network gateway from the specified router.""" + return self.put((self.router_path % router), + body={'router': {'external_gateway_info': {}}}) + + def list_floatingips(self, retrieve_all=True, **_params): + """Fetches a list of all floatingips for a project.""" + # Pass filters in "params" argument to do_request + return self.list('floatingips', self.floatingips_path, retrieve_all, + **_params) + + def show_floatingip(self, floatingip, **_params): + """Fetches information of a certain floatingip.""" + return self.get(self.floatingip_path % (floatingip), params=_params) + + def create_floatingip(self, body=None): + """Creates a new floatingip.""" + return self.post(self.floatingips_path, body=body) + + def update_floatingip(self, floatingip, body=None, revision_number=None): + """Updates a floatingip.""" + return self._update_resource(self.floatingip_path % (floatingip), + body=body, + revision_number=revision_number) + + def delete_floatingip(self, floatingip): + """Deletes the specified floatingip.""" + return self.delete(self.floatingip_path % (floatingip)) + + def show_port_forwarding(self, floatingip, portforwarding): + """Fetches information of a certain portforwarding""" + return self.get(self.port_forwarding_path % (floatingip, + portforwarding)) + + def list_port_forwardings(self, floatingip, retrieve_all=True, **_params): + """Fetches a list of all portforwardings for a floatingip.""" + return self.list('port_forwardings', + self.port_forwardings_path % floatingip, retrieve_all, + **_params) + + def create_port_forwarding(self, floatingip, body=None): + """Creates a new portforwarding.""" + return self.post(self.port_forwardings_path % floatingip, body=body) + + def delete_port_forwarding(self, floatingip, portforwarding): + """Deletes the specified portforwarding.""" + return self.delete(self.port_forwarding_path % (floatingip, + portforwarding)) + + def update_port_forwarding(self, floatingip, portforwarding, body=None): + """Updates a portforwarding.""" + return self.put(self.port_forwarding_path % (floatingip, + portforwarding), + body=body) + + def create_security_group(self, body=None): + """Creates a new security group.""" + return self.post(self.security_groups_path, body=body) + + def update_security_group(self, security_group, body=None, + revision_number=None): + """Updates a security group.""" + return self._update_resource(self.security_group_path % + security_group, body=body, + revision_number=revision_number) + + def list_security_groups(self, retrieve_all=True, **_params): + """Fetches a list of all security groups for a project.""" + return self.list('security_groups', self.security_groups_path, + retrieve_all, **_params) + + def show_security_group(self, security_group, **_params): + """Fetches information of a certain security group.""" + return self.get(self.security_group_path % (security_group), + params=_params) + + def delete_security_group(self, security_group): + """Deletes the specified security group.""" + return self.delete(self.security_group_path % (security_group)) + + def create_security_group_rule(self, body=None): + """Creates a new security group rule.""" + return self.post(self.security_group_rules_path, body=body) + + def delete_security_group_rule(self, security_group_rule): + """Deletes the specified security group rule.""" + return self.delete(self.security_group_rule_path % + (security_group_rule)) + + def list_security_group_rules(self, retrieve_all=True, **_params): + """Fetches a list of all security group rules for a project.""" + return self.list('security_group_rules', + self.security_group_rules_path, + retrieve_all, **_params) + + def show_security_group_rule(self, security_group_rule, **_params): + """Fetches information of a certain security group rule.""" + return self.get(self.security_group_rule_path % (security_group_rule), + params=_params) + + def create_segment(self, body=None): + """Creates a new segment.""" + return self.post(self.segments_path, body=body) + + def update_segment(self, segment, body=None, revision_number=None): + """Updates a segment.""" + return self._update_resource(self.segment_path % segment, body=body, + revision_number=revision_number) + + def list_segments(self, retrieve_all=True, **_params): + """Fetches a list of all segments for a project.""" + return self.list('segments', self.segments_path, retrieve_all, + **_params) + + def show_segment(self, segment, **_params): + """Fetches information of a certain segment.""" + return self.get(self.segment_path % segment, params=_params) + + def delete_segment(self, segment): + """Deletes the specified segment.""" + return self.delete(self.segment_path % segment) + + def list_endpoint_groups(self, retrieve_all=True, **_params): + """Fetches a list of all VPN endpoint groups for a project.""" + return self.list('endpoint_groups', self.endpoint_groups_path, + retrieve_all, **_params) + + def show_endpoint_group(self, endpointgroup, **_params): + """Fetches information for a specific VPN endpoint group.""" + return self.get(self.endpoint_group_path % endpointgroup, + params=_params) + + def create_endpoint_group(self, body=None): + """Creates a new VPN endpoint group.""" + return self.post(self.endpoint_groups_path, body=body) + + def update_endpoint_group(self, endpoint_group, body=None): + """Updates a VPN endpoint group.""" + return self.put(self.endpoint_group_path % endpoint_group, body=body) + + def delete_endpoint_group(self, endpoint_group): + """Deletes the specified VPN endpoint group.""" + return self.delete(self.endpoint_group_path % endpoint_group) + + def list_vpnservices(self, retrieve_all=True, **_params): + """Fetches a list of all configured VPN services for a project.""" + return self.list('vpnservices', self.vpnservices_path, retrieve_all, + **_params) + + def show_vpnservice(self, vpnservice, **_params): + """Fetches information of a specific VPN service.""" + return self.get(self.vpnservice_path % (vpnservice), params=_params) + + def create_vpnservice(self, body=None): + """Creates a new VPN service.""" + return self.post(self.vpnservices_path, body=body) + + def update_vpnservice(self, vpnservice, body=None): + """Updates a VPN service.""" + return self.put(self.vpnservice_path % (vpnservice), body=body) + + def delete_vpnservice(self, vpnservice): + """Deletes the specified VPN service.""" + return self.delete(self.vpnservice_path % (vpnservice)) + + def list_ipsec_site_connections(self, retrieve_all=True, **_params): + """Fetches all configured IPsecSiteConnections for a project.""" + return self.list('ipsec_site_connections', + self.ipsec_site_connections_path, + retrieve_all, + **_params) + + def show_ipsec_site_connection(self, ipsecsite_conn, **_params): + """Fetches information of a specific IPsecSiteConnection.""" + return self.get( + self.ipsec_site_connection_path % (ipsecsite_conn), params=_params + ) + + def create_ipsec_site_connection(self, body=None): + """Creates a new IPsecSiteConnection.""" + return self.post(self.ipsec_site_connections_path, body=body) + + def update_ipsec_site_connection(self, ipsecsite_conn, body=None): + """Updates an IPsecSiteConnection.""" + return self.put( + self.ipsec_site_connection_path % (ipsecsite_conn), body=body + ) + + def delete_ipsec_site_connection(self, ipsecsite_conn): + """Deletes the specified IPsecSiteConnection.""" + return self.delete(self.ipsec_site_connection_path % (ipsecsite_conn)) + + def list_ikepolicies(self, retrieve_all=True, **_params): + """Fetches a list of all configured IKEPolicies for a project.""" + return self.list('ikepolicies', self.ikepolicies_path, retrieve_all, + **_params) + + def show_ikepolicy(self, ikepolicy, **_params): + """Fetches information of a specific IKEPolicy.""" + return self.get(self.ikepolicy_path % (ikepolicy), params=_params) + + def create_ikepolicy(self, body=None): + """Creates a new IKEPolicy.""" + return self.post(self.ikepolicies_path, body=body) + + def update_ikepolicy(self, ikepolicy, body=None): + """Updates an IKEPolicy.""" + return self.put(self.ikepolicy_path % (ikepolicy), body=body) + + def delete_ikepolicy(self, ikepolicy): + """Deletes the specified IKEPolicy.""" + return self.delete(self.ikepolicy_path % (ikepolicy)) + + def list_ipsecpolicies(self, retrieve_all=True, **_params): + """Fetches a list of all configured IPsecPolicies for a project.""" + return self.list('ipsecpolicies', + self.ipsecpolicies_path, + retrieve_all, + **_params) + + def show_ipsecpolicy(self, ipsecpolicy, **_params): + """Fetches information of a specific IPsecPolicy.""" + return self.get(self.ipsecpolicy_path % (ipsecpolicy), params=_params) + + def create_ipsecpolicy(self, body=None): + """Creates a new IPsecPolicy.""" + return self.post(self.ipsecpolicies_path, body=body) + + def update_ipsecpolicy(self, ipsecpolicy, body=None): + """Updates an IPsecPolicy.""" + return self.put(self.ipsecpolicy_path % (ipsecpolicy), body=body) + + def delete_ipsecpolicy(self, ipsecpolicy): + """Deletes the specified IPsecPolicy.""" + return self.delete(self.ipsecpolicy_path % (ipsecpolicy)) + + def list_loadbalancers(self, retrieve_all=True, **_params): + """Fetches a list of all loadbalancers for a project.""" + return self.list('loadbalancers', self.lbaas_loadbalancers_path, + retrieve_all, **_params) + + def show_loadbalancer(self, lbaas_loadbalancer, **_params): + """Fetches information for a load balancer.""" + return self.get(self.lbaas_loadbalancer_path % (lbaas_loadbalancer), + params=_params) + + def create_loadbalancer(self, body=None): + """Creates a new load balancer.""" + return self.post(self.lbaas_loadbalancers_path, body=body) + + def update_loadbalancer(self, lbaas_loadbalancer, body=None): + """Updates a load balancer.""" + return self.put(self.lbaas_loadbalancer_path % (lbaas_loadbalancer), + body=body) + + def delete_loadbalancer(self, lbaas_loadbalancer): + """Deletes the specified load balancer.""" + return self.delete(self.lbaas_loadbalancer_path % + (lbaas_loadbalancer)) + + def retrieve_loadbalancer_stats(self, loadbalancer, **_params): + """Retrieves stats for a certain load balancer.""" + return self.get(self.lbaas_loadbalancer_path_stats % (loadbalancer), + params=_params) + + def retrieve_loadbalancer_status(self, loadbalancer, **_params): + """Retrieves status for a certain load balancer.""" + return self.get(self.lbaas_loadbalancer_path_status % (loadbalancer), + params=_params) + + def list_listeners(self, retrieve_all=True, **_params): + """Fetches a list of all lbaas_listeners for a project.""" + return self.list('listeners', self.lbaas_listeners_path, + retrieve_all, **_params) + + def show_listener(self, lbaas_listener, **_params): + """Fetches information for a lbaas_listener.""" + return self.get(self.lbaas_listener_path % (lbaas_listener), + params=_params) + + def create_listener(self, body=None): + """Creates a new lbaas_listener.""" + return self.post(self.lbaas_listeners_path, body=body) + + def update_listener(self, lbaas_listener, body=None): + """Updates a lbaas_listener.""" + return self.put(self.lbaas_listener_path % (lbaas_listener), + body=body) + + def delete_listener(self, lbaas_listener): + """Deletes the specified lbaas_listener.""" + return self.delete(self.lbaas_listener_path % (lbaas_listener)) + + def list_lbaas_l7policies(self, retrieve_all=True, **_params): + """Fetches a list of all L7 policies for a listener.""" + return self.list('l7policies', self.lbaas_l7policies_path, + retrieve_all, **_params) + + def show_lbaas_l7policy(self, l7policy, **_params): + """Fetches information of a certain listener's L7 policy.""" + return self.get(self.lbaas_l7policy_path % l7policy, + params=_params) + + def create_lbaas_l7policy(self, body=None): + """Creates L7 policy for a certain listener.""" + return self.post(self.lbaas_l7policies_path, body=body) + + def update_lbaas_l7policy(self, l7policy, body=None): + """Updates L7 policy.""" + return self.put(self.lbaas_l7policy_path % l7policy, + body=body) + + def delete_lbaas_l7policy(self, l7policy): + """Deletes the specified L7 policy.""" + return self.delete(self.lbaas_l7policy_path % l7policy) + + def list_lbaas_l7rules(self, l7policy, retrieve_all=True, **_params): + """Fetches a list of all rules for L7 policy.""" + return self.list('rules', self.lbaas_l7rules_path % l7policy, + retrieve_all, **_params) + + def show_lbaas_l7rule(self, l7rule, l7policy, **_params): + """Fetches information of a certain L7 policy's rule.""" + return self.get(self.lbaas_l7rule_path % (l7policy, l7rule), + params=_params) + + def create_lbaas_l7rule(self, l7policy, body=None): + """Creates rule for a certain L7 policy.""" + return self.post(self.lbaas_l7rules_path % l7policy, body=body) + + def update_lbaas_l7rule(self, l7rule, l7policy, body=None): + """Updates L7 rule.""" + return self.put(self.lbaas_l7rule_path % (l7policy, l7rule), + body=body) + + def delete_lbaas_l7rule(self, l7rule, l7policy): + """Deletes the specified L7 rule.""" + return self.delete(self.lbaas_l7rule_path % (l7policy, l7rule)) + + def list_lbaas_pools(self, retrieve_all=True, **_params): + """Fetches a list of all lbaas_pools for a project.""" + return self.list('pools', self.lbaas_pools_path, + retrieve_all, **_params) + + def show_lbaas_pool(self, lbaas_pool, **_params): + """Fetches information for a lbaas_pool.""" + return self.get(self.lbaas_pool_path % (lbaas_pool), + params=_params) + + def create_lbaas_pool(self, body=None): + """Creates a new lbaas_pool.""" + return self.post(self.lbaas_pools_path, body=body) + + def update_lbaas_pool(self, lbaas_pool, body=None): + """Updates a lbaas_pool.""" + return self.put(self.lbaas_pool_path % (lbaas_pool), + body=body) + + def delete_lbaas_pool(self, lbaas_pool): + """Deletes the specified lbaas_pool.""" + return self.delete(self.lbaas_pool_path % (lbaas_pool)) + + def list_lbaas_healthmonitors(self, retrieve_all=True, **_params): + """Fetches a list of all lbaas_healthmonitors for a project.""" + return self.list('healthmonitors', self.lbaas_healthmonitors_path, + retrieve_all, **_params) + + def show_lbaas_healthmonitor(self, lbaas_healthmonitor, **_params): + """Fetches information for a lbaas_healthmonitor.""" + return self.get(self.lbaas_healthmonitor_path % (lbaas_healthmonitor), + params=_params) + + def create_lbaas_healthmonitor(self, body=None): + """Creates a new lbaas_healthmonitor.""" + return self.post(self.lbaas_healthmonitors_path, body=body) + + def update_lbaas_healthmonitor(self, lbaas_healthmonitor, body=None): + """Updates a lbaas_healthmonitor.""" + return self.put(self.lbaas_healthmonitor_path % (lbaas_healthmonitor), + body=body) + + def delete_lbaas_healthmonitor(self, lbaas_healthmonitor): + """Deletes the specified lbaas_healthmonitor.""" + return self.delete(self.lbaas_healthmonitor_path % + (lbaas_healthmonitor)) + + def list_lbaas_loadbalancers(self, retrieve_all=True, **_params): + """Fetches a list of all lbaas_loadbalancers for a project.""" + return self.list('loadbalancers', self.lbaas_loadbalancers_path, + retrieve_all, **_params) + + def list_lbaas_members(self, lbaas_pool, retrieve_all=True, **_params): + """Fetches a list of all lbaas_members for a project.""" + return self.list('members', self.lbaas_members_path % lbaas_pool, + retrieve_all, **_params) + + def show_lbaas_member(self, lbaas_member, lbaas_pool, **_params): + """Fetches information of a certain lbaas_member.""" + return self.get(self.lbaas_member_path % (lbaas_pool, lbaas_member), + params=_params) + + def create_lbaas_member(self, lbaas_pool, body=None): + """Creates a lbaas_member.""" + return self.post(self.lbaas_members_path % lbaas_pool, body=body) + + def update_lbaas_member(self, lbaas_member, lbaas_pool, body=None): + """Updates a lbaas_member.""" + return self.put(self.lbaas_member_path % (lbaas_pool, lbaas_member), + body=body) + + def delete_lbaas_member(self, lbaas_member, lbaas_pool): + """Deletes the specified lbaas_member.""" + return self.delete(self.lbaas_member_path % (lbaas_pool, lbaas_member)) + + def list_vips(self, retrieve_all=True, **_params): + """Fetches a list of all load balancer vips for a project.""" + # Pass filters in "params" argument to do_request + return self.list('vips', self.vips_path, retrieve_all, + **_params) + + def show_vip(self, vip, **_params): + """Fetches information of a certain load balancer vip.""" + return self.get(self.vip_path % (vip), params=_params) + + def create_vip(self, body=None): + """Creates a new load balancer vip.""" + return self.post(self.vips_path, body=body) + + def update_vip(self, vip, body=None): + """Updates a load balancer vip.""" + return self.put(self.vip_path % (vip), body=body) + + def delete_vip(self, vip): + """Deletes the specified load balancer vip.""" + return self.delete(self.vip_path % (vip)) + + def list_pools(self, retrieve_all=True, **_params): + """Fetches a list of all load balancer pools for a project.""" + # Pass filters in "params" argument to do_request + return self.list('pools', self.pools_path, retrieve_all, + **_params) + + def show_pool(self, pool, **_params): + """Fetches information of a certain load balancer pool.""" + return self.get(self.pool_path % (pool), params=_params) + + def create_pool(self, body=None): + """Creates a new load balancer pool.""" + return self.post(self.pools_path, body=body) + + def update_pool(self, pool, body=None): + """Updates a load balancer pool.""" + return self.put(self.pool_path % (pool), body=body) + + def delete_pool(self, pool): + """Deletes the specified load balancer pool.""" + return self.delete(self.pool_path % (pool)) + + def retrieve_pool_stats(self, pool, **_params): + """Retrieves stats for a certain load balancer pool.""" + return self.get(self.pool_path_stats % (pool), params=_params) + + def list_members(self, retrieve_all=True, **_params): + """Fetches a list of all load balancer members for a project.""" + # Pass filters in "params" argument to do_request + return self.list('members', self.members_path, retrieve_all, + **_params) + + def show_member(self, member, **_params): + """Fetches information of a certain load balancer member.""" + return self.get(self.member_path % (member), params=_params) + + def create_member(self, body=None): + """Creates a new load balancer member.""" + return self.post(self.members_path, body=body) + + def update_member(self, member, body=None): + """Updates a load balancer member.""" + return self.put(self.member_path % (member), body=body) + + def delete_member(self, member): + """Deletes the specified load balancer member.""" + return self.delete(self.member_path % (member)) + + def list_health_monitors(self, retrieve_all=True, **_params): + """Fetches a list of all load balancer health monitors for a project. + + """ + # Pass filters in "params" argument to do_request + return self.list('health_monitors', self.health_monitors_path, + retrieve_all, **_params) + + def show_health_monitor(self, health_monitor, **_params): + """Fetches information of a certain load balancer health monitor.""" + return self.get(self.health_monitor_path % (health_monitor), + params=_params) + + def create_health_monitor(self, body=None): + """Creates a new load balancer health monitor.""" + return self.post(self.health_monitors_path, body=body) + + def update_health_monitor(self, health_monitor, body=None): + """Updates a load balancer health monitor.""" + return self.put(self.health_monitor_path % (health_monitor), body=body) + + def delete_health_monitor(self, health_monitor): + """Deletes the specified load balancer health monitor.""" + return self.delete(self.health_monitor_path % (health_monitor)) + + def associate_health_monitor(self, pool, body): + """Associate specified load balancer health monitor and pool.""" + return self.post(self.associate_pool_health_monitors_path % (pool), + body=body) + + def disassociate_health_monitor(self, pool, health_monitor): + """Disassociate specified load balancer health monitor and pool.""" + path = (self.disassociate_pool_health_monitors_path % + {'pool': pool, 'health_monitor': health_monitor}) + return self.delete(path) + + def create_qos_queue(self, body=None): + """Creates a new queue.""" + return self.post(self.qos_queues_path, body=body) + + def list_qos_queues(self, **_params): + """Fetches a list of all queues for a project.""" + return self.get(self.qos_queues_path, params=_params) + + def show_qos_queue(self, queue, **_params): + """Fetches information of a certain queue.""" + return self.get(self.qos_queue_path % (queue), + params=_params) + + def delete_qos_queue(self, queue): + """Deletes the specified queue.""" + return self.delete(self.qos_queue_path % (queue)) + + def list_agents(self, **_params): + """Fetches agents.""" + # Pass filters in "params" argument to do_request + return self.get(self.agents_path, params=_params) + + def show_agent(self, agent, **_params): + """Fetches information of a certain agent.""" + return self.get(self.agent_path % (agent), params=_params) + + def update_agent(self, agent, body=None): + """Updates an agent.""" + return self.put(self.agent_path % (agent), body=body) + + def delete_agent(self, agent): + """Deletes the specified agent.""" + return self.delete(self.agent_path % (agent)) + + def list_network_gateways(self, **_params): + """Retrieve network gateways.""" + return self.get(self.network_gateways_path, params=_params) + + def show_network_gateway(self, gateway_id, **_params): + """Fetch a network gateway.""" + return self.get(self.network_gateway_path % gateway_id, params=_params) + + def create_network_gateway(self, body=None): + """Create a new network gateway.""" + return self.post(self.network_gateways_path, body=body) + + def update_network_gateway(self, gateway_id, body=None): + """Update a network gateway.""" + return self.put(self.network_gateway_path % gateway_id, body=body) + + def delete_network_gateway(self, gateway_id): + """Delete the specified network gateway.""" + return self.delete(self.network_gateway_path % gateway_id) + + def connect_network_gateway(self, gateway_id, body=None): + """Connect a network gateway to the specified network.""" + base_uri = self.network_gateway_path % gateway_id + return self.put("%s/connect_network" % base_uri, body=body) + + def disconnect_network_gateway(self, gateway_id, body=None): + """Disconnect a network from the specified gateway.""" + base_uri = self.network_gateway_path % gateway_id + return self.put("%s/disconnect_network" % base_uri, body=body) + + def list_gateway_devices(self, **_params): + """Retrieve gateway devices.""" + return self.get(self.gateway_devices_path, params=_params) + + def show_gateway_device(self, gateway_device_id, **_params): + """Fetch a gateway device.""" + return self.get(self.gateway_device_path % gateway_device_id, + params=_params) + + def create_gateway_device(self, body=None): + """Create a new gateway device.""" + return self.post(self.gateway_devices_path, body=body) + + def update_gateway_device(self, gateway_device_id, body=None): + """Updates a new gateway device.""" + return self.put(self.gateway_device_path % gateway_device_id, + body=body) + + def delete_gateway_device(self, gateway_device_id): + """Delete the specified gateway device.""" + return self.delete(self.gateway_device_path % gateway_device_id) + + def list_dhcp_agent_hosting_networks(self, network, **_params): + """Fetches a list of dhcp agents hosting a network.""" + return self.get((self.network_path + self.DHCP_AGENTS) % network, + params=_params) + + def list_networks_on_dhcp_agent(self, dhcp_agent, **_params): + """Fetches a list of networks hosted on a DHCP agent.""" + return self.get((self.agent_path + self.DHCP_NETS) % dhcp_agent, + params=_params) + + def add_network_to_dhcp_agent(self, dhcp_agent, body=None): + """Adds a network to dhcp agent.""" + return self.post((self.agent_path + self.DHCP_NETS) % dhcp_agent, + body=body) + + def remove_network_from_dhcp_agent(self, dhcp_agent, network_id): + """Remove a network from dhcp agent.""" + return self.delete((self.agent_path + self.DHCP_NETS + "/%s") % ( + dhcp_agent, network_id)) + + def list_l3_agent_hosting_routers(self, router, **_params): + """Fetches a list of L3 agents hosting a router.""" + return self.get((self.router_path + self.L3_AGENTS) % router, + params=_params) + + def list_routers_on_l3_agent(self, l3_agent, **_params): + """Fetches a list of routers hosted on an L3 agent.""" + return self.get((self.agent_path + self.L3_ROUTERS) % l3_agent, + params=_params) + + def add_router_to_l3_agent(self, l3_agent, body): + """Adds a router to L3 agent.""" + return self.post((self.agent_path + self.L3_ROUTERS) % l3_agent, + body=body) + + def list_dragents_hosting_bgp_speaker(self, bgp_speaker, **_params): + """Fetches a list of Dynamic Routing agents hosting a BGP speaker.""" + return self.get((self.bgp_speaker_path + self.BGP_DRAGENTS) + % bgp_speaker, params=_params) + + def add_bgp_speaker_to_dragent(self, bgp_dragent, body): + """Adds a BGP speaker to Dynamic Routing agent.""" + return self.post((self.agent_path + self.BGP_DRINSTANCES) + % bgp_dragent, body=body) + + def remove_bgp_speaker_from_dragent(self, bgp_dragent, bgpspeaker_id): + """Removes a BGP speaker from Dynamic Routing agent.""" + return self.delete((self.agent_path + self.BGP_DRINSTANCES + "/%s") + % (bgp_dragent, bgpspeaker_id)) + + def list_bgp_speaker_on_dragent(self, bgp_dragent, **_params): + """Fetches a list of BGP speakers hosted by Dynamic Routing agent.""" + return self.get((self.agent_path + self.BGP_DRINSTANCES) + % bgp_dragent, params=_params) + + def list_firewall_rules(self, retrieve_all=True, **_params): + """Fetches a list of all firewall rules for a project.""" + # Pass filters in "params" argument to do_request + + return self.list('firewall_rules', self.firewall_rules_path, + retrieve_all, **_params) + + def show_firewall_rule(self, firewall_rule, **_params): + """Fetches information of a certain firewall rule.""" + return self.get(self.firewall_rule_path % (firewall_rule), + params=_params) + + def create_firewall_rule(self, body=None): + """Creates a new firewall rule.""" + return self.post(self.firewall_rules_path, body=body) + + def update_firewall_rule(self, firewall_rule, body=None): + """Updates a firewall rule.""" + return self.put(self.firewall_rule_path % (firewall_rule), body=body) + + def delete_firewall_rule(self, firewall_rule): + """Deletes the specified firewall rule.""" + return self.delete(self.firewall_rule_path % (firewall_rule)) + + def list_firewall_policies(self, retrieve_all=True, **_params): + """Fetches a list of all firewall policies for a project.""" + # Pass filters in "params" argument to do_request + + return self.list('firewall_policies', self.firewall_policies_path, + retrieve_all, **_params) + + def show_firewall_policy(self, firewall_policy, **_params): + """Fetches information of a certain firewall policy.""" + return self.get(self.firewall_policy_path % (firewall_policy), + params=_params) + + def create_firewall_policy(self, body=None): + """Creates a new firewall policy.""" + return self.post(self.firewall_policies_path, body=body) + + def update_firewall_policy(self, firewall_policy, body=None): + """Updates a firewall policy.""" + return self.put(self.firewall_policy_path % (firewall_policy), + body=body) + + def delete_firewall_policy(self, firewall_policy): + """Deletes the specified firewall policy.""" + return self.delete(self.firewall_policy_path % (firewall_policy)) + + def firewall_policy_insert_rule(self, firewall_policy, body=None): + """Inserts specified rule into firewall policy.""" + return self.put(self.firewall_policy_insert_path % (firewall_policy), + body=body) + + def firewall_policy_remove_rule(self, firewall_policy, body=None): + """Removes specified rule from firewall policy.""" + return self.put(self.firewall_policy_remove_path % (firewall_policy), + body=body) + + def list_firewalls(self, retrieve_all=True, **_params): + """Fetches a list of all firewalls for a project.""" + # Pass filters in "params" argument to do_request + + return self.list('firewalls', self.firewalls_path, retrieve_all, + **_params) + + def show_firewall(self, firewall, **_params): + """Fetches information of a certain firewall.""" + return self.get(self.firewall_path % (firewall), params=_params) + + def create_firewall(self, body=None): + """Creates a new firewall.""" + return self.post(self.firewalls_path, body=body) + + def update_firewall(self, firewall, body=None): + """Updates a firewall.""" + return self.put(self.firewall_path % (firewall), body=body) + + def delete_firewall(self, firewall): + """Deletes the specified firewall.""" + return self.delete(self.firewall_path % (firewall)) + + def list_fwaas_firewall_groups(self, retrieve_all=True, **_params): + """Fetches a list of all firewall groups for a project""" + return self.list('firewall_groups', self.fwaas_firewall_groups_path, + retrieve_all, **_params) + + def show_fwaas_firewall_group(self, fwg, **_params): + """Fetches information of a certain firewall group""" + return self.get(self.fwaas_firewall_group_path % (fwg), params=_params) + + def create_fwaas_firewall_group(self, body=None): + """Creates a new firewall group""" + return self.post(self.fwaas_firewall_groups_path, body=body) + + def update_fwaas_firewall_group(self, fwg, body=None): + """Updates a firewall group""" + return self.put(self.fwaas_firewall_group_path % (fwg), body=body) + + def delete_fwaas_firewall_group(self, fwg): + """Deletes the specified firewall group""" + return self.delete(self.fwaas_firewall_group_path % (fwg)) + + def list_fwaas_firewall_rules(self, retrieve_all=True, **_params): + """Fetches a list of all firewall rules for a project""" + # Pass filters in "params" argument to do_request + return self.list('firewall_rules', self.fwaas_firewall_rules_path, + retrieve_all, **_params) + + def show_fwaas_firewall_rule(self, firewall_rule, **_params): + """Fetches information of a certain firewall rule""" + return self.get(self.fwaas_firewall_rule_path % (firewall_rule), + params=_params) + + def create_fwaas_firewall_rule(self, body=None): + """Creates a new firewall rule""" + return self.post(self.fwaas_firewall_rules_path, body=body) + + def update_fwaas_firewall_rule(self, firewall_rule, body=None): + """Updates a firewall rule""" + return self.put(self.fwaas_firewall_rule_path % (firewall_rule), + body=body) + + def delete_fwaas_firewall_rule(self, firewall_rule): + """Deletes the specified firewall rule""" + return self.delete(self.fwaas_firewall_rule_path % (firewall_rule)) + + def list_fwaas_firewall_policies(self, retrieve_all=True, **_params): + """Fetches a list of all firewall policies for a project""" + # Pass filters in "params" argument to do_request + + return self.list('firewall_policies', + self.fwaas_firewall_policies_path, + retrieve_all, **_params) + + def show_fwaas_firewall_policy(self, firewall_policy, **_params): + """Fetches information of a certain firewall policy""" + return self.get(self.fwaas_firewall_policy_path % (firewall_policy), + params=_params) + + def create_fwaas_firewall_policy(self, body=None): + """Creates a new firewall policy""" + return self.post(self.fwaas_firewall_policies_path, body=body) + + def update_fwaas_firewall_policy(self, firewall_policy, body=None): + """Updates a firewall policy""" + return self.put(self.fwaas_firewall_policy_path % (firewall_policy), + body=body) + + def delete_fwaas_firewall_policy(self, firewall_policy): + """Deletes the specified firewall policy""" + return self.delete(self.fwaas_firewall_policy_path % (firewall_policy)) + + def insert_rule_fwaas_firewall_policy(self, firewall_policy, body=None): + """Inserts specified rule into firewall policy""" + return self.put((self.fwaas_firewall_policy_insert_path % + (firewall_policy)), body=body) + + def remove_rule_fwaas_firewall_policy(self, firewall_policy, body=None): + """Removes specified rule from firewall policy""" + return self.put((self.fwaas_firewall_policy_remove_path % + (firewall_policy)), body=body) + + def remove_router_from_l3_agent(self, l3_agent, router_id): + """Remove a router from l3 agent.""" + return self.delete((self.agent_path + self.L3_ROUTERS + "/%s") % ( + l3_agent, router_id)) + + def get_lbaas_agent_hosting_pool(self, pool, **_params): + """Fetches a loadbalancer agent hosting a pool.""" + return self.get((self.pool_path + self.LOADBALANCER_AGENT) % pool, + params=_params) + + def list_pools_on_lbaas_agent(self, lbaas_agent, **_params): + """Fetches a list of pools hosted by the loadbalancer agent.""" + return self.get((self.agent_path + self.LOADBALANCER_POOLS) % + lbaas_agent, params=_params) + + def get_lbaas_agent_hosting_loadbalancer(self, loadbalancer, **_params): + """Fetches a loadbalancer agent hosting a loadbalancer.""" + return self.get((self.lbaas_loadbalancer_path + + self.LOADBALANCER_HOSTING_AGENT) % loadbalancer, + params=_params) + + def list_loadbalancers_on_lbaas_agent(self, lbaas_agent, **_params): + """Fetches a list of loadbalancers hosted by the loadbalancer agent.""" + return self.get((self.agent_path + self.AGENT_LOADBALANCERS) % + lbaas_agent, params=_params) + + def list_service_providers(self, retrieve_all=True, **_params): + """Fetches service providers.""" + # Pass filters in "params" argument to do_request + return self.list('service_providers', self.service_providers_path, + retrieve_all, **_params) + + def create_metering_label(self, body=None): + """Creates a metering label.""" + return self.post(self.metering_labels_path, body=body) + + def delete_metering_label(self, label): + """Deletes the specified metering label.""" + return self.delete(self.metering_label_path % (label)) + + def list_metering_labels(self, retrieve_all=True, **_params): + """Fetches a list of all metering labels for a project.""" + return self.list('metering_labels', self.metering_labels_path, + retrieve_all, **_params) + + def show_metering_label(self, metering_label, **_params): + """Fetches information of a certain metering label.""" + return self.get(self.metering_label_path % + (metering_label), params=_params) + + def create_metering_label_rule(self, body=None): + """Creates a metering label rule.""" + return self.post(self.metering_label_rules_path, body=body) + + def delete_metering_label_rule(self, rule): + """Deletes the specified metering label rule.""" + return self.delete(self.metering_label_rule_path % (rule)) + + def list_metering_label_rules(self, retrieve_all=True, **_params): + """Fetches a list of all metering label rules for a label.""" + return self.list('metering_label_rules', + self.metering_label_rules_path, retrieve_all, + **_params) + + def show_metering_label_rule(self, metering_label_rule, **_params): + """Fetches information of a certain metering label rule.""" + return self.get(self.metering_label_rule_path % + (metering_label_rule), params=_params) + + def create_rbac_policy(self, body=None): + """Create a new RBAC policy.""" + return self.post(self.rbac_policies_path, body=body) + + def update_rbac_policy(self, rbac_policy_id, body=None): + """Update a RBAC policy.""" + return self.put(self.rbac_policy_path % rbac_policy_id, body=body) + + def list_rbac_policies(self, retrieve_all=True, **_params): + """Fetch a list of all RBAC policies for a project.""" + return self.list('rbac_policies', self.rbac_policies_path, + retrieve_all, **_params) + + def show_rbac_policy(self, rbac_policy_id, **_params): + """Fetch information of a certain RBAC policy.""" + return self.get(self.rbac_policy_path % rbac_policy_id, + params=_params) + + def delete_rbac_policy(self, rbac_policy_id): + """Delete the specified RBAC policy.""" + return self.delete(self.rbac_policy_path % rbac_policy_id) + + def list_qos_policies(self, retrieve_all=True, **_params): + """Fetches a list of all qos policies for a project.""" + # Pass filters in "params" argument to do_request + return self.list('policies', self.qos_policies_path, + retrieve_all, **_params) + + def show_qos_policy(self, qos_policy, **_params): + """Fetches information of a certain qos policy.""" + return self.get(self.qos_policy_path % qos_policy, + params=_params) + + def create_qos_policy(self, body=None): + """Creates a new qos policy.""" + return self.post(self.qos_policies_path, body=body) + + def update_qos_policy(self, qos_policy, body=None, revision_number=None): + """Updates a qos policy.""" + return self._update_resource(self.qos_policy_path % qos_policy, + body=body, + revision_number=revision_number) + + def delete_qos_policy(self, qos_policy): + """Deletes the specified qos policy.""" + return self.delete(self.qos_policy_path % qos_policy) + + def list_qos_rule_types(self, retrieve_all=True, **_params): + """List available qos rule types.""" + return self.list('rule_types', self.qos_rule_types_path, + retrieve_all, **_params) + + def list_bandwidth_limit_rules(self, policy_id, + retrieve_all=True, **_params): + """Fetches a list of all bandwidth limit rules for the given policy.""" + return self.list('bandwidth_limit_rules', + self.qos_bandwidth_limit_rules_path % policy_id, + retrieve_all, **_params) + + def show_bandwidth_limit_rule(self, rule, policy, **_params): + """Fetches information of a certain bandwidth limit rule.""" + return self.get(self.qos_bandwidth_limit_rule_path % + (policy, rule), params=_params) + + def create_bandwidth_limit_rule(self, policy, body=None): + """Creates a new bandwidth limit rule.""" + return self.post(self.qos_bandwidth_limit_rules_path % policy, + body=body) + + def update_bandwidth_limit_rule(self, rule, policy, body=None): + """Updates a bandwidth limit rule.""" + return self.put(self.qos_bandwidth_limit_rule_path % + (policy, rule), body=body) + + def delete_bandwidth_limit_rule(self, rule, policy): + """Deletes a bandwidth limit rule.""" + return self.delete(self.qos_bandwidth_limit_rule_path % + (policy, rule)) + + def list_dscp_marking_rules(self, policy_id, + retrieve_all=True, **_params): + """Fetches a list of all DSCP marking rules for the given policy.""" + return self.list('dscp_marking_rules', + self.qos_dscp_marking_rules_path % policy_id, + retrieve_all, **_params) + + def show_dscp_marking_rule(self, rule, policy, **_params): + """Shows information of a certain DSCP marking rule.""" + return self.get(self.qos_dscp_marking_rule_path % + (policy, rule), params=_params) + + def create_dscp_marking_rule(self, policy, body=None): + """Creates a new DSCP marking rule.""" + return self.post(self.qos_dscp_marking_rules_path % policy, + body=body) + + def update_dscp_marking_rule(self, rule, policy, body=None): + """Updates a DSCP marking rule.""" + return self.put(self.qos_dscp_marking_rule_path % + (policy, rule), body=body) + + def delete_dscp_marking_rule(self, rule, policy): + """Deletes a DSCP marking rule.""" + return self.delete(self.qos_dscp_marking_rule_path % + (policy, rule)) + + def list_minimum_bandwidth_rules(self, policy_id, retrieve_all=True, + **_params): + """Fetches a list of all minimum bandwidth rules for the given policy. + + """ + return self.list('minimum_bandwidth_rules', + self.qos_minimum_bandwidth_rules_path % + policy_id, retrieve_all, **_params) + + def show_minimum_bandwidth_rule(self, rule, policy, body=None): + """Fetches information of a certain minimum bandwidth rule.""" + return self.get(self.qos_minimum_bandwidth_rule_path % + (policy, rule), body=body) + + def create_minimum_bandwidth_rule(self, policy, body=None): + """Creates a new minimum bandwidth rule.""" + return self.post(self.qos_minimum_bandwidth_rules_path % policy, + body=body) + + def list_packet_rate_limit_rules(self, policy_id, retrieve_all=True, + **_params): + """Fetches a list of all packet rate limit rules for the given policy + + """ + return self.list('packet_rate_limit_rules', + self.qos_packet_rate_limit_rules_path % + policy_id, retrieve_all, **_params) + + def show_packet_rate_limit_rule(self, rule, policy, body=None): + """Fetches information of a certain packet rate limit rule.""" + return self.get(self.qos_packet_rate_limit_rule_path % + (policy, rule), body=body) + + def create_packet_rate_limit_rule(self, policy, body=None): + """Creates a new packet rate limit rule.""" + return self.post(self.qos_packet_rate_limit_rules_path % policy, + body=body) + + def update_packet_rate_limit_rule(self, rule, policy, body=None): + """Updates a packet rate limit rule.""" + return self.put(self.qos_packet_rate_limit_rule_path % + (policy, rule), body=body) + + def delete_packet_rate_limit_rule(self, rule, policy): + """Deletes a packet rate limit rule.""" + return self.delete(self.qos_packet_rate_limit_rule_path % + (policy, rule)) + + def update_minimum_bandwidth_rule(self, rule, policy, body=None): + """Updates a minimum bandwidth rule.""" + return self.put(self.qos_minimum_bandwidth_rule_path % + (policy, rule), body=body) + + def delete_minimum_bandwidth_rule(self, rule, policy): + """Deletes a minimum bandwidth rule.""" + return self.delete(self.qos_minimum_bandwidth_rule_path % + (policy, rule)) + + def list_minimum_packet_rate_rules(self, policy_id, retrieve_all=True, + **_params): + """Fetches a list of all minimum packet rate rules for the given policy + + """ + return self.list('minimum_packet_rate_rules', + self.qos_minimum_packet_rate_rules_path % + policy_id, retrieve_all, **_params) + + def show_minimum_packet_rate_rule(self, rule, policy, body=None): + """Fetches information of a certain minimum packet rate rule.""" + return self.get(self.qos_minimum_packet_rate_rule_path % + (policy, rule), body=body) + + def create_minimum_packet_rate_rule(self, policy, body=None): + """Creates a new minimum packet rate rule.""" + return self.post(self.qos_minimum_packet_rate_rules_path % policy, + body=body) + + def update_minimum_packet_rate_rule(self, rule, policy, body=None): + """Updates a minimum packet rate rule.""" + return self.put(self.qos_minimum_packet_rate_rule_path % + (policy, rule), body=body) + + def delete_minimum_packet_rate_rule(self, rule, policy): + """Deletes a minimum packet rate rule.""" + return self.delete(self.qos_minimum_packet_rate_rule_path % + (policy, rule)) + + def create_flavor(self, body=None): + """Creates a new Neutron service flavor.""" + return self.post(self.flavors_path, body=body) + + def delete_flavor(self, flavor): + """Deletes the specified Neutron service flavor.""" + return self.delete(self.flavor_path % (flavor)) + + def list_flavors(self, retrieve_all=True, **_params): + """Fetches a list of all Neutron service flavors for a project.""" + return self.list('flavors', self.flavors_path, retrieve_all, + **_params) + + def show_flavor(self, flavor, **_params): + """Fetches information for a certain Neutron service flavor.""" + return self.get(self.flavor_path % (flavor), params=_params) + + def update_flavor(self, flavor, body): + """Update a Neutron service flavor.""" + return self.put(self.flavor_path % (flavor), body=body) + + def associate_flavor(self, flavor, body): + """Associate a Neutron service flavor with a profile.""" + return self.post(self.flavor_profile_bindings_path % + (flavor), body=body) + + def disassociate_flavor(self, flavor, flavor_profile): + """Disassociate a Neutron service flavor with a profile.""" + return self.delete(self.flavor_profile_binding_path % + (flavor, flavor_profile)) + + def create_service_profile(self, body=None): + """Creates a new Neutron service flavor profile.""" + return self.post(self.service_profiles_path, body=body) + + def delete_service_profile(self, flavor_profile): + """Deletes the specified Neutron service flavor profile.""" + return self.delete(self.service_profile_path % (flavor_profile)) + + def list_service_profiles(self, retrieve_all=True, **_params): + """Fetches a list of all Neutron service flavor profiles.""" + return self.list('service_profiles', self.service_profiles_path, + retrieve_all, **_params) + + def show_service_profile(self, flavor_profile, **_params): + """Fetches information for a certain Neutron service flavor profile.""" + return self.get(self.service_profile_path % (flavor_profile), + params=_params) + + def update_service_profile(self, service_profile, body): + """Update a Neutron service profile.""" + return self.put(self.service_profile_path % (service_profile), + body=body) + + def list_availability_zones(self, retrieve_all=True, **_params): + """Fetches a list of all availability zones.""" + return self.list('availability_zones', self.availability_zones_path, + retrieve_all, **_params) + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + def get_auto_allocated_topology(self, project_id, **_params): + """Fetch information about a project's auto-allocated topology.""" + return self.get( + self.auto_allocated_topology_path % project_id, + params=_params) + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + def delete_auto_allocated_topology(self, project_id, **_params): + """Delete a project's auto-allocated topology.""" + return self.delete( + self.auto_allocated_topology_path % project_id, + params=_params) + + @debtcollector.renames.renamed_kwarg( + 'tenant_id', 'project_id', replace=True) + def validate_auto_allocated_topology_requirements(self, project_id): + """Validate requirements for getting an auto-allocated topology.""" + return self.get_auto_allocated_topology(project_id, fields=['dry-run']) + + def list_bgp_speakers(self, retrieve_all=True, **_params): + """Fetches a list of all BGP speakers for a project.""" + return self.list('bgp_speakers', self.bgp_speakers_path, retrieve_all, + **_params) + + def show_bgp_speaker(self, bgp_speaker_id, **_params): + """Fetches information of a certain BGP speaker.""" + return self.get(self.bgp_speaker_path % (bgp_speaker_id), + params=_params) + + def create_bgp_speaker(self, body=None): + """Creates a new BGP speaker.""" + return self.post(self.bgp_speakers_path, body=body) + + def update_bgp_speaker(self, bgp_speaker_id, body=None): + """Update a BGP speaker.""" + return self.put(self.bgp_speaker_path % bgp_speaker_id, body=body) + + def delete_bgp_speaker(self, speaker_id): + """Deletes the specified BGP speaker.""" + return self.delete(self.bgp_speaker_path % (speaker_id)) + + def add_peer_to_bgp_speaker(self, speaker_id, body=None): + """Adds a peer to BGP speaker.""" + return self.put((self.bgp_speaker_path % speaker_id) + + "/add_bgp_peer", body=body) + + def remove_peer_from_bgp_speaker(self, speaker_id, body=None): + """Removes a peer from BGP speaker.""" + return self.put((self.bgp_speaker_path % speaker_id) + + "/remove_bgp_peer", body=body) + + def add_network_to_bgp_speaker(self, speaker_id, body=None): + """Adds a network to BGP speaker.""" + return self.put((self.bgp_speaker_path % speaker_id) + + "/add_gateway_network", body=body) + + def remove_network_from_bgp_speaker(self, speaker_id, body=None): + """Removes a network from BGP speaker.""" + return self.put((self.bgp_speaker_path % speaker_id) + + "/remove_gateway_network", body=body) + + def list_route_advertised_from_bgp_speaker(self, speaker_id, **_params): + """Fetches a list of all routes advertised by BGP speaker.""" + return self.get((self.bgp_speaker_path % speaker_id) + + "/get_advertised_routes", params=_params) + + def list_bgp_peers(self, **_params): + """Fetches a list of all BGP peers.""" + return self.get(self.bgp_peers_path, params=_params) + + def show_bgp_peer(self, peer_id, **_params): + """Fetches information of a certain BGP peer.""" + return self.get(self.bgp_peer_path % peer_id, + params=_params) + + def create_bgp_peer(self, body=None): + """Create a new BGP peer.""" + return self.post(self.bgp_peers_path, body=body) + + def update_bgp_peer(self, bgp_peer_id, body=None): + """Update a BGP peer.""" + return self.put(self.bgp_peer_path % bgp_peer_id, body=body) + + def delete_bgp_peer(self, peer_id): + """Deletes the specified BGP peer.""" + return self.delete(self.bgp_peer_path % peer_id) + + def list_network_ip_availabilities(self, retrieve_all=True, **_params): + """Fetches IP availability information for all networks""" + return self.list('network_ip_availabilities', + self.network_ip_availabilities_path, + retrieve_all, **_params) + + def show_network_ip_availability(self, network, **_params): + """Fetches IP availability information for a specified network""" + return self.get(self.network_ip_availability_path % (network), + params=_params) + + def add_tag(self, resource_type, resource_id, tag, **_params): + """Add a tag on the resource.""" + return self.put(self.tag_path % (resource_type, resource_id, tag)) + + def replace_tag(self, resource_type, resource_id, body, **_params): + """Replace tags on the resource.""" + return self.put(self.tags_path % (resource_type, resource_id), body) + + def remove_tag(self, resource_type, resource_id, tag, **_params): + """Remove a tag on the resource.""" + return self.delete(self.tag_path % (resource_type, resource_id, tag)) + + def remove_tag_all(self, resource_type, resource_id, **_params): + """Remove all tags on the resource.""" + return self.delete(self.tags_path % (resource_type, resource_id)) + + def create_trunk(self, body=None): + """Create a trunk port.""" + return self.post(self.trunks_path, body=body) + + def update_trunk(self, trunk, body=None, revision_number=None): + """Update a trunk port.""" + return self._update_resource(self.trunk_path % trunk, body=body, + revision_number=revision_number) + + def delete_trunk(self, trunk): + """Delete a trunk port.""" + return self.delete(self.trunk_path % (trunk)) + + def list_trunks(self, retrieve_all=True, **_params): + """Fetch a list of all trunk ports.""" + return self.list('trunks', self.trunks_path, retrieve_all, + **_params) + + def show_trunk(self, trunk, **_params): + """Fetch information for a certain trunk port.""" + return self.get(self.trunk_path % (trunk), params=_params) + + def trunk_add_subports(self, trunk, body=None): + """Add specified subports to the trunk.""" + return self.put(self.subports_add_path % (trunk), body=body) + + def trunk_remove_subports(self, trunk, body=None): + """Removes specified subports from the trunk.""" + return self.put(self.subports_remove_path % (trunk), body=body) + + def trunk_get_subports(self, trunk, **_params): + """Fetch a list of all subports attached to given trunk.""" + return self.get(self.subports_path % (trunk), params=_params) + + def list_bgpvpns(self, retrieve_all=True, **_params): + """Fetches a list of all BGP VPNs for a project""" + return self.list('bgpvpns', self.bgpvpns_path, retrieve_all, **_params) + + def show_bgpvpn(self, bgpvpn, **_params): + """Fetches information of a certain BGP VPN""" + return self.get(self.bgpvpn_path % bgpvpn, params=_params) + + def create_bgpvpn(self, body=None): + """Creates a new BGP VPN""" + return self.post(self.bgpvpns_path, body=body) + + def update_bgpvpn(self, bgpvpn, body=None): + """Updates a BGP VPN""" + return self.put(self.bgpvpn_path % bgpvpn, body=body) + + def delete_bgpvpn(self, bgpvpn): + """Deletes the specified BGP VPN""" + return self.delete(self.bgpvpn_path % bgpvpn) + + def list_bgpvpn_network_assocs(self, bgpvpn, retrieve_all=True, **_params): + """Fetches a list of network associations for a given BGP VPN.""" + return self.list('network_associations', + self.bgpvpn_network_associations_path % bgpvpn, + retrieve_all, **_params) + + def show_bgpvpn_network_assoc(self, bgpvpn, net_assoc, **_params): + """Fetches information of a certain BGP VPN's network association""" + return self.get( + self.bgpvpn_network_association_path % (bgpvpn, net_assoc), + params=_params) + + def create_bgpvpn_network_assoc(self, bgpvpn, body=None): + """Creates a new BGP VPN network association""" + return self.post(self.bgpvpn_network_associations_path % bgpvpn, + body=body) + + def update_bgpvpn_network_assoc(self, bgpvpn, net_assoc, body=None): + """Updates a BGP VPN network association""" + return self.put( + self.bgpvpn_network_association_path % (bgpvpn, net_assoc), + body=body) + + def delete_bgpvpn_network_assoc(self, bgpvpn, net_assoc): + """Deletes the specified BGP VPN network association""" + return self.delete( + self.bgpvpn_network_association_path % (bgpvpn, net_assoc)) + + def list_bgpvpn_router_assocs(self, bgpvpn, retrieve_all=True, **_params): + """Fetches a list of router associations for a given BGP VPN.""" + return self.list('router_associations', + self.bgpvpn_router_associations_path % bgpvpn, + retrieve_all, **_params) + + def show_bgpvpn_router_assoc(self, bgpvpn, router_assoc, **_params): + """Fetches information of a certain BGP VPN's router association""" + return self.get( + self.bgpvpn_router_association_path % (bgpvpn, router_assoc), + params=_params) + + def create_bgpvpn_router_assoc(self, bgpvpn, body=None): + """Creates a new BGP VPN router association""" + return self.post(self.bgpvpn_router_associations_path % bgpvpn, + body=body) + + def update_bgpvpn_router_assoc(self, bgpvpn, router_assoc, body=None): + """Updates a BGP VPN router association""" + return self.put( + self.bgpvpn_router_association_path % (bgpvpn, router_assoc), + body=body) + + def delete_bgpvpn_router_assoc(self, bgpvpn, router_assoc): + """Deletes the specified BGP VPN router association""" + return self.delete( + self.bgpvpn_router_association_path % (bgpvpn, router_assoc)) + + def list_bgpvpn_port_assocs(self, bgpvpn, retrieve_all=True, **_params): + """Fetches a list of port associations for a given BGP VPN.""" + return self.list('port_associations', + self.bgpvpn_port_associations_path % bgpvpn, + retrieve_all, **_params) + + def show_bgpvpn_port_assoc(self, bgpvpn, port_assoc, **_params): + """Fetches information of a certain BGP VPN's port association""" + return self.get( + self.bgpvpn_port_association_path % (bgpvpn, port_assoc), + params=_params) + + def create_bgpvpn_port_assoc(self, bgpvpn, body=None): + """Creates a new BGP VPN port association""" + return self.post(self.bgpvpn_port_associations_path % bgpvpn, + body=body) + + def update_bgpvpn_port_assoc(self, bgpvpn, port_assoc, body=None): + """Updates a BGP VPN port association""" + return self.put( + self.bgpvpn_port_association_path % (bgpvpn, port_assoc), + body=body) + + def delete_bgpvpn_port_assoc(self, bgpvpn, port_assoc): + """Deletes the specified BGP VPN port association""" + return self.delete( + self.bgpvpn_port_association_path % (bgpvpn, port_assoc)) + + def create_sfc_port_pair(self, body=None): + """Creates a new Port Pair.""" + return self.post(self.sfc_port_pairs_path, body=body) + + def update_sfc_port_pair(self, port_pair, body=None): + """Update a Port Pair.""" + return self.put(self.sfc_port_pair_path % port_pair, body=body) + + def delete_sfc_port_pair(self, port_pair): + """Deletes the specified Port Pair.""" + return self.delete(self.sfc_port_pair_path % (port_pair)) + + def list_sfc_port_pairs(self, retrieve_all=True, **_params): + """Fetches a list of all Port Pairs.""" + return self.list('port_pairs', self.sfc_port_pairs_path, retrieve_all, + **_params) + + def show_sfc_port_pair(self, port_pair, **_params): + """Fetches information of a certain Port Pair.""" + return self.get(self.sfc_port_pair_path % (port_pair), params=_params) + + def create_sfc_port_pair_group(self, body=None): + """Creates a new Port Pair Group.""" + return self.post(self.sfc_port_pair_groups_path, body=body) + + def update_sfc_port_pair_group(self, port_pair_group, body=None): + """Update a Port Pair Group.""" + return self.put(self.sfc_port_pair_group_path % port_pair_group, + body=body) + + def delete_sfc_port_pair_group(self, port_pair_group): + """Deletes the specified Port Pair Group.""" + return self.delete(self.sfc_port_pair_group_path % (port_pair_group)) + + def list_sfc_port_pair_groups(self, retrieve_all=True, **_params): + """Fetches a list of all Port Pair Groups.""" + return self.list('port_pair_groups', self.sfc_port_pair_groups_path, + retrieve_all, **_params) + + def show_sfc_port_pair_group(self, port_pair_group, **_params): + """Fetches information of a certain Port Pair Group.""" + return self.get(self.sfc_port_pair_group_path % (port_pair_group), + params=_params) + + def create_sfc_port_chain(self, body=None): + """Creates a new Port Chain.""" + return self.post(self.sfc_port_chains_path, body=body) + + def update_sfc_port_chain(self, port_chain, body=None): + """Update a Port Chain.""" + return self.put(self.sfc_port_chain_path % port_chain, body=body) + + def delete_sfc_port_chain(self, port_chain): + """Deletes the specified Port Chain.""" + return self.delete(self.sfc_port_chain_path % (port_chain)) + + def list_sfc_port_chains(self, retrieve_all=True, **_params): + """Fetches a list of all Port Chains.""" + return self.list('port_chains', self.sfc_port_chains_path, + retrieve_all, **_params) + + def show_sfc_port_chain(self, port_chain, **_params): + """Fetches information of a certain Port Chain.""" + return self.get(self.sfc_port_chain_path % (port_chain), + params=_params) + + def create_sfc_flow_classifier(self, body=None): + """Creates a new Flow Classifier.""" + return self.post(self.sfc_flow_classifiers_path, body=body) + + def update_sfc_flow_classifier(self, flow_classifier, body=None): + """Update a Flow Classifier.""" + return self.put(self.sfc_flow_classifier_path % flow_classifier, + body=body) + + def delete_sfc_flow_classifier(self, flow_classifier): + """Deletes the specified Flow Classifier.""" + return self.delete(self.sfc_flow_classifier_path % (flow_classifier)) + + def list_sfc_flow_classifiers(self, retrieve_all=True, **_params): + """Fetches a list of all Flow Classifiers.""" + return self.list('flow_classifiers', self.sfc_flow_classifiers_path, + retrieve_all, **_params) + + def show_sfc_flow_classifier(self, flow_classifier, **_params): + """Fetches information of a certain Flow Classifier.""" + return self.get(self.sfc_flow_classifier_path % (flow_classifier), + params=_params) + + def create_sfc_service_graph(self, body=None): + """Create the specified Service Graph.""" + return self.post(self.sfc_service_graphs_path, body=body) + + def update_sfc_service_graph(self, service_graph, body=None): + """Update a Service Graph.""" + return self.put(self.sfc_service_graph_path % service_graph, + body=body) + + def delete_sfc_service_graph(self, service_graph): + """Deletes the specified Service Graph.""" + return self.delete(self.sfc_service_graph_path % service_graph) + + def list_sfc_service_graphs(self, retrieve_all=True, **_params): + """Fetches a list of all Service Graphs.""" + return self.list('service_graphs', self.sfc_service_graphs_path, + retrieve_all, **_params) + + def show_sfc_service_graph(self, service_graph, **_params): + """Fetches information of a certain Service Graph.""" + return self.get(self.sfc_service_graph_path % service_graph, + params=_params) + + def create_network_log(self, body=None): + """Create a network log.""" + return self.post(self.network_logs_path, body=body) + + def delete_network_log(self, net_log): + """Delete a network log.""" + return self.delete(self.network_log_path % net_log) + + def list_network_logs(self, retrieve_all=True, **_params): + """Fetch a list of all network logs.""" + return self.list( + 'logs', self.network_logs_path, retrieve_all, **_params) + + def show_network_log(self, net_log, **_params): + """Fetch information for a certain network log.""" + return self.get(self.network_log_path % net_log, params=_params) + + def update_network_log(self, net_log, body=None): + """Update a network log.""" + return self.put(self.network_log_path % net_log, body=body) + + def list_network_loggable_resources(self, retrieve_all=True, **_params): + """Fetch a list of supported resource types for network log.""" + return self.list('loggable_resources', self.network_loggables_path, + retrieve_all, **_params) + + def onboard_network_subnets(self, subnetpool, body=None): + """Onboard the specified network's subnets into a subnet pool.""" + return self.put(self.onboard_network_subnets_path % (subnetpool), + body=body) + + def __init__(self, **kwargs): + """Initialize a new client for the Neutron v2.0 API.""" + super(Client, self).__init__(**kwargs) + self._register_extensions(self.version) + + def _update_resource(self, path, **kwargs): + revision_number = kwargs.pop('revision_number', None) + if revision_number: + headers = kwargs.setdefault('headers', {}) + headers['If-Match'] = 'revision_number=%s' % revision_number + return self.put(path, **kwargs) + + def extend_show(self, resource_singular, path, parent_resource): + def _fx(obj, **_params): + return self.show_ext(path, obj, **_params) + + def _parent_fx(obj, parent_id, **_params): + return self.show_ext(path % parent_id, obj, **_params) + fn = _fx if not parent_resource else _parent_fx + setattr(self, "show_%s" % resource_singular, fn) + + def extend_list(self, resource_plural, path, parent_resource): + def _fx(retrieve_all=True, **_params): + return self.list_ext(resource_plural, path, + retrieve_all, **_params) + + def _parent_fx(parent_id, retrieve_all=True, **_params): + return self.list_ext(resource_plural, path % parent_id, + retrieve_all, **_params) + fn = _fx if not parent_resource else _parent_fx + setattr(self, "list_%s" % resource_plural, fn) + + def extend_create(self, resource_singular, path, parent_resource): + def _fx(body=None): + return self.create_ext(path, body) + + def _parent_fx(parent_id, body=None): + return self.create_ext(path % parent_id, body) + fn = _fx if not parent_resource else _parent_fx + setattr(self, "create_%s" % resource_singular, fn) + + def extend_delete(self, resource_singular, path, parent_resource): + def _fx(obj): + return self.delete_ext(path, obj) + + def _parent_fx(obj, parent_id): + return self.delete_ext(path % parent_id, obj) + fn = _fx if not parent_resource else _parent_fx + setattr(self, "delete_%s" % resource_singular, fn) + + def extend_update(self, resource_singular, path, parent_resource): + def _fx(obj, body=None): + return self.update_ext(path, obj, body) + + def _parent_fx(obj, parent_id, body=None): + return self.update_ext(path % parent_id, obj, body) + fn = _fx if not parent_resource else _parent_fx + setattr(self, "update_%s" % resource_singular, fn) + + def _extend_client_with_module(self, module, version): + classes = inspect.getmembers(module, inspect.isclass) + for cls_name, cls in classes: + if hasattr(cls, 'versions'): + if version not in cls.versions: + continue + parent_resource = getattr(cls, 'parent_resource', None) + if issubclass(cls, client_extension.ClientExtensionList): + self.extend_list(cls.resource_plural, cls.object_path, + parent_resource) + elif issubclass(cls, client_extension.ClientExtensionCreate): + self.extend_create(cls.resource, cls.object_path, + parent_resource) + elif issubclass(cls, client_extension.ClientExtensionUpdate): + self.extend_update(cls.resource, cls.resource_path, + parent_resource) + elif issubclass(cls, client_extension.ClientExtensionDelete): + self.extend_delete(cls.resource, cls.resource_path, + parent_resource) + elif issubclass(cls, client_extension.ClientExtensionShow): + self.extend_show(cls.resource, cls.resource_path, + parent_resource) + elif issubclass(cls, client_extension.NeutronClientExtension): + setattr(self, "%s_path" % cls.resource_plural, + cls.object_path) + setattr(self, "%s_path" % cls.resource, cls.resource_path) + self.EXTED_PLURALS.update({cls.resource_plural: cls.resource}) + + def _register_extensions(self, version): + for name, module in itertools.chain( + client_extension._discover_via_entry_points()): + self._extend_client_with_module(module, version) diff --git a/neutronclient/version.py b/neutronclient/version.py new file mode 100644 index 000000000..d597b5c91 --- /dev/null +++ b/neutronclient/version.py @@ -0,0 +1,21 @@ +# 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_info = pbr.version.VersionInfo('python-neutronclient') +__version__ = version_info.version_string() diff --git a/quantumclient/__init__.py b/quantumclient/__init__.py deleted file mode 100644 index 461f9b853..000000000 --- a/quantumclient/__init__.py +++ /dev/null @@ -1,563 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Citrix Systems -# 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. -# @author: Tyler Smith, Cisco Systems - -import gettext -import httplib -import logging -import socket -import time -import urllib - - -# gettext must be initialized before any quantumclient imports -gettext.install('quantumclient', unicode=1) - - -from quantumclient.common import exceptions -from quantumclient.common.serializer import Serializer - - -LOG = logging.getLogger('quantumclient') - - -AUTH_TOKEN_HEADER = "X-Auth-Token" - - -def exception_handler_v10(status_code, error_content): - """ Exception handler for API v1.0 client - - This routine generates the appropriate - Quantum exception according to the contents of the - response body - - :param status_code: HTTP error status code - :param error_content: deserialized body of error response - """ - - quantum_error_types = { - 420: 'networkNotFound', - 421: 'networkInUse', - 430: 'portNotFound', - 431: 'requestedStateInvalid', - 432: 'portInUse', - 440: 'alreadyAttached', - } - - quantum_errors = { - 400: exceptions.BadInputError, - 401: exceptions.NotAuthorized, - 404: exceptions.NotFound, - 420: exceptions.NetworkNotFoundClient, - 421: exceptions.NetworkInUseClient, - 430: exceptions.PortNotFoundClient, - 431: exceptions.StateInvalidClient, - 432: exceptions.PortInUseClient, - 440: exceptions.AlreadyAttachedClient, - 501: NotImplementedError, - } - - # Find real error type - error_type = None - if isinstance(error_content, dict): - error_type = quantum_error_types.get(status_code) - if error_type: - error_dict = error_content[error_type] - error_message = error_dict['message'] + "\n" + error_dict['detail'] - else: - error_message = error_content - # raise the appropriate error! - ex = quantum_errors[status_code](message=error_message) - ex.args = ([dict(status_code=status_code, - message=error_message)],) - raise ex - - -def exception_handler_v11(status_code, error_content): - """ Exception handler for API v1.1 client - - This routine generates the appropriate - Quantum exception according to the contents of the - response body - - :param status_code: HTTP error status code - :param error_content: deserialized body of error response - """ - - quantum_errors = { - 'NetworkNotFound': exceptions.NetworkNotFoundClient, - 'NetworkInUse': exceptions.NetworkInUseClient, - 'PortNotFound': exceptions.PortNotFoundClient, - 'RequestedStateInvalid': exceptions.StateInvalidClient, - 'PortInUse': exceptions.PortInUseClient, - 'AlreadyAttached': exceptions.AlreadyAttachedClient, - } - - error_dict = None - if isinstance(error_content, dict): - error_dict = error_content.get('QuantumError') - # Find real error type - if error_dict: - # If QuantumError key is found, it will definitely contain - # a 'message' and 'type' keys - error_type = error_dict['type'] - error_message = (error_dict['message'] + "\n" + - error_dict['detail']) - # raise the appropriate error! - ex = quantum_errors[error_type](message=error_message) - ex.args = ([dict(status_code=status_code, - message=error_message)],) - raise ex - # If we end up here the exception was not a quantum error - msg = "%s-%s" % (status_code, error_content) - raise exceptions.QuantumClientException(message=msg) - - -EXCEPTION_HANDLERS = { - '1.0': exception_handler_v10, - '1.1': exception_handler_v11, -} - - -class ApiCall(object): - """A Decorator to add support for format and tenant overriding""" - def __init__(self, function): - self.function = function - - def __get__(self, instance, owner): - def with_params(*args, **kwargs): - """ - Temporarily sets the format and tenant for this request - """ - (format, tenant) = (instance.format, instance.tenant) - - if 'format' in kwargs: - instance.format = kwargs['format'] - if 'tenant' in kwargs: - instance.tenant = kwargs['tenant'] - - ret = self.function(instance, *args) - (instance.format, instance.tenant) = (format, tenant) - return ret - return with_params - - -class Client(object): - - """A base client class - derived from Glance.BaseClient""" - - #Metadata for deserializing xml - _serialization_metadata = { - "application/xml": { - "attributes": { - "network": ["id", "name"], - "port": ["id", "state"], - "attachment": ["id"]}, - "plurals": { - "networks": "network", - "ports": "port", - }, - }, - } - - # Action query strings - networks_path = "/networks" - network_path = "/networks/%s" - ports_path = "/networks/%s/ports" - port_path = "/networks/%s/ports/%s" - attachment_path = "/networks/%s/ports/%s/attachment" - detail_path = "/detail" - - def __init__(self, host="127.0.0.1", port=9696, use_ssl=False, tenant=None, - format="xml", testingStub=None, key_file=None, cert_file=None, - auth_token=None, logger=None, - version="1.1", - uri_prefix="/tenants/{tenant_id}", - retries=0, - retry_interval=1): - """ - Creates a new client to some service. - - :param host: The host where service resides - :param port: The port where service resides - :param use_ssl: True to use SSL, False to use HTTP - :param tenant: The tenant ID to make requests with - :param format: The format to query the server with - :param testingStub: A class that stubs basic server methods for tests - :param key_file: The SSL key file to use if use_ssl is true - :param cert_file: The SSL cert file to use if use_ssl is true - :param auth_token: authentication token to be passed to server - :param logger: Logger object for the client library - :param action_prefix: prefix for request URIs - :param retries: How many times to retry a failed connection attempt - :param retry_interval: The # of seconds between connection attempts - """ - self.host = host - self.port = port - self.use_ssl = use_ssl - self.tenant = tenant - self.format = format - self.connection = None - self.testingStub = testingStub - self.key_file = key_file - self.cert_file = cert_file - self.logger = logger - if not self.logger: - self.logger = LOG - self.auth_token = auth_token - self.version = version - self.action_prefix = "/v%s%s" % (version, uri_prefix) - self.retries = retries - self.retry_interval = retry_interval - - def _handle_fault_response(self, status_code, response_body): - # Create exception with HTTP status code and message - error_message = response_body - LOG.debug("Server returned error: %s", status_code) - LOG.debug("Error message: %s", error_message) - # Add deserialized error message to exception arguments - try: - des_error_body = Serializer().deserialize(error_message, - self.content_type()) - except: - # If unable to deserialized body it is probably not a - # Quantum error - des_error_body = {'message': error_message} - # Raise the appropriate exception - EXCEPTION_HANDLERS[self.version](status_code, des_error_body) - - def get_connection_type(self): - """ - Returns the proper connection type - """ - if self.testingStub: - return self.testingStub - if self.use_ssl: - return httplib.HTTPSConnection - else: - return httplib.HTTPConnection - - def _send_request(self, conn, method, action, body, headers): - # Salvatore: Isolating this piece of code in its own method to - # facilitate stubout for testing - if self.logger: - self.logger.debug("Quantum Client Request:\n" - + method + " " + action + "\n") - if body: - self.logger.debug(body) - conn.request(method, action, body, headers) - return conn.getresponse() - - def do_request(self, method, action, body=None, - headers=None, params=None): - """ - Connects to the server and issues a request. - Returns the result data, or raises an appropriate exception if - HTTP status code is not 2xx - - :param method: HTTP method ("GET", "POST", "PUT", etc...) - :param body: string of data to send, or None (default) - :param headers: mapping of key/value pairs to add as headers - :param params: dictionary of key/value pairs to add to append - to action - :raises: ConnectionFailed - """ - LOG.debug("Client issuing request: %s", action) - # Ensure we have a tenant id - if not self.tenant: - raise Exception("Tenant ID not set") - # Add format and tenant_id - action += ".%s" % self.format - action = self.action_prefix + action - action = action.replace('{tenant_id}', self.tenant) - - if type(params) is dict: - action += '?' + urllib.urlencode(params) - if body: - body = self.serialize(body) - try: - connection_type = self.get_connection_type() - headers = headers or {"Content-Type": - "application/%s" % self.format} - # if available, add authentication token - if self.auth_token: - headers[AUTH_TOKEN_HEADER] = self.auth_token - # Open connection and send request, handling SSL certs - certs = {'key_file': self.key_file, 'cert_file': self.cert_file} - certs = dict((x, certs[x]) for x in certs if certs[x] != None) - if self.use_ssl and len(certs): - conn = connection_type(self.host, self.port, **certs) - else: - conn = connection_type(self.host, self.port) - # besides HTTP(s)Connection, we still have testConnection - if (LOG.isEnabledFor(logging.DEBUG) and - isinstance(conn, httplib.HTTPConnection)): - conn.set_debuglevel(1) - - res = self._send_request(conn, method, action, body, headers) - status_code = self.get_status_code(res) - data = res.read() - if self.logger: - self.logger.debug("Quantum Client Reply (code = %s) :\n %s" % - (str(status_code), data)) - if status_code in (httplib.OK, - httplib.CREATED, - httplib.ACCEPTED, - httplib.NO_CONTENT): - return self.deserialize(data, status_code) - else: - self._handle_fault_response(status_code, data) - except (socket.error, IOError), e: - exc = exceptions.ConnectionFailed(reason=str(e)) - LOG.exception(exc.message) - raise exc - - def get_status_code(self, response): - """ - Returns the integer status code from the response, which - can be either a Webob.Response (used in testing) or httplib.Response - """ - if hasattr(response, 'status_int'): - return response.status_int - else: - return response.status - - def serialize(self, data): - """ - Serializes a dictionary with a single key (which can contain any - structure) into either xml or json - """ - if data is None: - return None - elif type(data) is dict: - return Serializer().serialize(data, self.content_type()) - else: - raise Exception("unable to serialize object of type = '%s'" % - type(data)) - - def deserialize(self, data, status_code): - """ - Deserializes a an xml or json string into a dictionary - """ - if status_code == 204: - return data - return Serializer(self._serialization_metadata).deserialize( - data, self.content_type()) - - def content_type(self, format=None): - """ - Returns the mime-type for either 'xml' or 'json'. Defaults to the - currently set format - """ - if not format: - format = self.format - return "application/%s" % (format) - - 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 xrange(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: - LOG.debug(_('Retrying connection to quantum service')) - time.sleep(self.retry_interval) - - raise exceptions.ConnectionFailed(reason=_("Maximum attempts reached")) - - 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) - - @ApiCall - def list_networks(self): - """ - Fetches a list of all networks for a tenant - """ - return self.get(self.networks_path) - - @ApiCall - def list_networks_details(self): - """ - Fetches a detailed list of all networks for a tenant - """ - return self.get(self.networks_path + self.detail_path) - - @ApiCall - def show_network(self, network): - """ - Fetches information of a certain network - """ - return self.get(self.network_path % (network)) - - @ApiCall - def show_network_details(self, network): - """ - Fetches the details of a certain network - """ - return self.get((self.network_path + self.detail_path) % (network)) - - @ApiCall - def create_network(self, body=None): - """ - Creates a new network - """ - return self.post(self.networks_path, body=body) - - @ApiCall - def update_network(self, network, body=None): - """ - Updates a network - """ - return self.put(self.network_path % (network), body=body) - - @ApiCall - def delete_network(self, network): - """ - Deletes the specified network - """ - return self.delete(self.network_path % (network)) - - @ApiCall - def list_ports(self, network): - """ - Fetches a list of ports on a given network - """ - return self.get(self.ports_path % (network)) - - @ApiCall - def list_ports_details(self, network): - """ - Fetches a detailed list of ports on a given network - """ - return self.get((self.ports_path + self.detail_path) % (network)) - - @ApiCall - def show_port(self, network, port): - """ - Fetches the information of a certain port - """ - return self.get(self.port_path % (network, port)) - - @ApiCall - def show_port_details(self, network, port): - """ - Fetches the details of a certain port - """ - return self.get((self.port_path + self.detail_path) % (network, port)) - - @ApiCall - def create_port(self, network, body=None): - """ - Creates a new port on a given network - """ - return self.post(self.ports_path % (network), body=body) - - @ApiCall - def delete_port(self, network, port): - """ - Deletes the specified port from a network - """ - return self.delete(self.port_path % (network, port)) - - @ApiCall - def update_port(self, network, port, body=None): - """ - Sets the attributes of the specified port - """ - return self.put(self.port_path % (network, port), body=body) - - @ApiCall - def show_port_attachment(self, network, port): - """ - Fetches the attachment-id associated with the specified port - """ - return self.get(self.attachment_path % (network, port)) - - @ApiCall - def attach_resource(self, network, port, body=None): - """ - Sets the attachment-id of the specified port - """ - return self.put(self.attachment_path % (network, port), body=body) - - @ApiCall - def detach_resource(self, network, port): - """ - Removes the attachment-id of the specified port - """ - return self.delete(self.attachment_path % (network, port)) - - -class ClientV11(Client): - """ - This class overiddes some methods of the Client class in order to deal with - features specific to API v1.1 such as filters - """ - - @ApiCall - def list_networks(self, **filters): - """ - Fetches a list of all networks for a tenant - """ - # Pass filters in "params" argument to do_request - return self.get(self.networks_path, params=filters) - - @ApiCall - def list_networks_details(self, **filters): - """ - Fetches a detailed list of all networks for a tenant - """ - return self.get(self.networks_path + self.detail_path, params=filters) - - @ApiCall - def list_ports(self, network, **filters): - """ - Fetches a list of ports on a given network - """ - # Pass filters in "params" argument to do_request - return self.get(self.ports_path % (network), params=filters) - - @ApiCall - def list_ports_details(self, network, **filters): - """ - Fetches a detailed list of ports on a given network - """ - return self.get((self.ports_path + self.detail_path) % (network), - params=filters) diff --git a/quantumclient/cli.py b/quantumclient/cli.py deleted file mode 100755 index 6c14f632e..000000000 --- a/quantumclient/cli.py +++ /dev/null @@ -1,325 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Nicira Networks, Inc. -# Copyright 2012 Citrix Systems -# -# 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. -# @author: Somik Behera, Nicira Networks, Inc. -# @author: Brad Hall, Nicira Networks, Inc. -# @author: Salvatore Orlando, Citrix - -import logging -import logging.handlers -from optparse import OptionParser -import os -import sys - -from quantumclient import cli_lib -from quantumclient import Client -from quantumclient import ClientV11 -from quantumclient.common import exceptions -from quantumclient.common import utils - - -# Configure logger for client - cli logger is a child of it -# NOTE(salvatore-orlando): logger name does not map to package -# this is deliberate. Simplifies logger configuration -logging.basicConfig() -LOG = logging.getLogger('quantumclient') - - -DEFAULT_QUANTUM_VERSION = '1.1' -FORMAT = 'json' -commands_v10 = { - "list_nets": { - "func": cli_lib.list_nets, - "args": ["tenant-id"], - }, - "list_nets_detail": { - "func": cli_lib.list_nets_detail, - "args": ["tenant-id"], - }, - "create_net": { - "func": cli_lib.create_net, - "args": ["tenant-id", "net-name"], - }, - "delete_net": { - "func": cli_lib.delete_net, - "args": ["tenant-id", "net-id"], - }, - "show_net": { - "func": cli_lib.show_net, - "args": ["tenant-id", "net-id"], - }, - "show_net_detail": { - "func": cli_lib.show_net_detail, - "args": ["tenant-id", "net-id"], - }, - "update_net": { - "func": cli_lib.update_net, - "args": ["tenant-id", "net-id", "new-name"], - }, - "list_ports": { - "func": cli_lib.list_ports, - "args": ["tenant-id", "net-id"], - }, - "list_ports_detail": { - "func": cli_lib.list_ports_detail, - "args": ["tenant-id", "net-id"], - }, - "create_port": { - "func": cli_lib.create_port, - "args": ["tenant-id", "net-id"], - }, - "delete_port": { - "func": cli_lib.delete_port, - "args": ["tenant-id", "net-id", "port-id"], - }, - "update_port": { - "func": cli_lib.update_port, - "args": ["tenant-id", "net-id", "port-id", "params"], - }, - "show_port": { - "func": cli_lib.show_port, - "args": ["tenant-id", "net-id", "port-id"], - }, - "show_port_detail": { - "func": cli_lib.show_port_detail, - "args": ["tenant-id", "net-id", "port-id"], - }, - "plug_iface": { - "func": cli_lib.plug_iface, - "args": ["tenant-id", "net-id", "port-id", "iface-id"], - }, - "unplug_iface": { - "func": cli_lib.unplug_iface, - "args": ["tenant-id", "net-id", "port-id"], - }, - "show_iface": { - "func": cli_lib.show_iface, - "args": ["tenant-id", "net-id", "port-id"], - }, - } - -commands_v11 = commands_v10.copy() -commands_v11.update({ - "list_nets": { - "func": cli_lib.list_nets_v11, - "args": ["tenant-id"], - "filters": ["name", "op-status", "port-op-status", "port-state", - "has-attachment", "attachment", "port"], - }, - "list_nets_detail": { - "func": cli_lib.list_nets_detail_v11, - "args": ["tenant-id"], - "filters": ["name", "op-status", "port-op-status", "port-state", - "has-attachment", "attachment", "port"], - }, - "list_ports": { - "func": cli_lib.list_ports_v11, - "args": ["tenant-id", "net-id"], - "filters": ["name", "op-status", "has-attachment", "attachment"], - }, - "list_ports_detail": { - "func": cli_lib.list_ports_detail_v11, - "args": ["tenant-id", "net-id"], - "filters": ["name", "op-status", "has-attachment", "attachment"], - }, - }) -commands = { - '1.0': commands_v10, - '1.1': commands_v11, - } -clients = { - '1.0': Client, - '1.1': ClientV11, - } - - -def help(version): - print "\nCommands:" - cmds = commands[version] - for k in cmds.keys(): - print " %s %s %s" % ( - k, - " ".join(["<%s>" % y for y in cmds[k]["args"]]), - 'filters' in cmds[k] and "[filterspec ...]" or "") - - -def print_usage(cmd, version): - cmds = commands[version] - print "Usage:\n %s %s" % ( - cmd, " ".join(["<%s>" % y for y in cmds[cmd]["args"]])) - - -def build_args(cmd, cmdargs, arglist): - arglist_len = len(arglist) - cmdargs_len = len(cmdargs) - if arglist_len < cmdargs_len: - message = ("Not enough arguments for \"%s\" (expected: %d, got: %d)" % - (cmd, len(cmdargs), arglist_len)) - raise exceptions.QuantumCLIError(message=message) - args = arglist[:cmdargs_len] - return args - - -def build_filters(cmd, cmd_filters, filter_list, version): - filters = {} - # Each filter is expected to be in the = format - for flt in filter_list: - split_filter = flt.split("=") - if len(split_filter) != 2: - message = "Invalid filter argument detected (%s)" % flt - raise exceptions.QuantumCLIError(message=message) - filter_key, filter_value = split_filter - # Ensure the filter is allowed - if not filter_key in cmd_filters: - message = "Invalid filter key (%s)" % filter_key - raise exceptions.QuantumCLIError(message=message) - filters[filter_key] = filter_value - return filters - - -def build_cmd(cmd, cmd_args, cmd_filters, arglist, version): - """ - Builds arguments and filters to be passed to the cli library routines - - :param cmd: Command to be executed - :param cmd_args: List of arguments required by the command - :param cmd_filters: List of filters allowed by the command - :param arglist: Command line arguments (includes both arguments and - filter specifications) - :param version: API version - """ - arglist_len = len(arglist) - try: - # Parse arguments - args = build_args(cmd, cmd_args, arglist) - # Parse filters - filters = None - if cmd_filters: - # Pop consumed arguments - arglist = arglist[len(args):] - filters = build_filters(cmd, cmd_filters, arglist, version) - except exceptions.QuantumCLIError as cli_ex: - LOG.error(cli_ex.message) - print " Error in command line:%s" % cli_ex.message - print_usage(cmd, version) - return None, None - filter_len = (filters is not None) and len(filters) or 0 - if len(arglist) - len(args) - filter_len > 0: - message = ("Too many arguments for \"%s\" (expected: %d, got: %d)" % - (cmd, len(cmd_args), arglist_len)) - LOG.error(message) - print "Error in command line: %s " % message - print "Usage:\n %s %s" % ( - cmd, - " ".join(["<%s>" % y for y in commands[version][cmd]["args"]])) - return None, None - # Append version to arguments for cli functions - args.append(version) - return args, filters - - -def instantiate_client(host, port, ssl, tenant, token, version): - client = clients[version](host, - port, - ssl, - tenant, - FORMAT, - auth_token=token, - version=version) - return client - - -def main(): - usagestr = "Usage: %prog [OPTIONS] [args]" - parser = OptionParser(usage=usagestr) - parser.add_option("-H", "--host", dest="host", - type="string", default="127.0.0.1", - help="ip address of api host") - parser.add_option("-p", "--port", dest="port", - type="int", default=9696, help="api poort") - parser.add_option("-s", "--ssl", dest="ssl", - action="store_true", default=False, help="use ssl") - parser.add_option("--debug", dest="debug", - action="store_true", default=False, - help="print debugging output") - parser.add_option("-f", "--logfile", dest="logfile", - type="string", default="syslog", help="log file path") - parser.add_option("-t", "--token", dest="token", - type="string", default=None, help="authentication token") - parser.add_option( - '--version', - default=utils.env('QUANTUM_VERSION', default=DEFAULT_QUANTUM_VERSION), - help='Accepts 1.1 and 1.0, defaults to env[QUANTUM_VERSION].') - options, args = parser.parse_args() - - if options.debug: - LOG.setLevel(logging.DEBUG) - else: - LOG.setLevel(logging.WARN) - - if options.logfile == "syslog": - LOG.addHandler(logging.handlers.SysLogHandler(address='/dev/log')) - else: - LOG.addHandler(logging.handlers.WatchedFileHandler(options.logfile)) - # Set permissions on log file - os.chmod(options.logfile, 0644) - - version = options.version - if not version in commands: - LOG.error("Unknown API version specified:%s", version) - parser.print_help() - sys.exit(1) - - if len(args) < 1: - parser.print_help() - help(version) - sys.exit(1) - - cmd = args[0] - if cmd not in commands[version].keys(): - LOG.error("Unknown command: %s" % cmd) - help(version) - sys.exit(1) - - # Build argument list for CLI command - # The argument list will include the version number as well - args, filters = build_cmd(cmd, - commands[version][cmd]["args"], - commands[version][cmd].get("filters", None), - args[1:], - options.version) - if not args: - sys.exit(1) - LOG.info("Executing command \"%s\" with args: %s" % (cmd, args)) - - client = instantiate_client(options.host, - options.port, - options.ssl, - args[0], - options.token, - options.version) - # append filters to arguments - # this will allow for using the same prototype for v10 and v11 - # TODO: Use **kwargs instead of *args (keyword is better than positional) - if filters: - args.append(filters) - commands[version][cmd]["func"](client, *args) - - LOG.info("Command execution completed") - sys.exit(0) - -if __name__ == '__main__': - main() diff --git a/quantumclient/cli_lib.py b/quantumclient/cli_lib.py deleted file mode 100755 index 6111bcb3d..000000000 --- a/quantumclient/cli_lib.py +++ /dev/null @@ -1,594 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Nicira Networks, Inc. -# Copyright 2011 Citrix Systems -# -# 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. -# @author: Somik Behera, Nicira Networks, Inc. -# @author: Brad Hall, Nicira Networks, Inc. -# @author: Salvatore Orlando, Citrix - -""" Functions providing implementation for CLI commands. """ - -import logging -import traceback - - -LOG = logging.getLogger('quantumclient.cli_lib') - - -FORMAT = "json" - - -class OutputTemplate(object): - """ A class for generating simple templated output. - Based on Python templating mechanism. - Templates can also express attributes on objects, such as network.id; - templates can also be nested, thus allowing for iteration on inner - templates. - - Examples: - 1) template with class attributes - Name: %(person.name)s \n - Surname: %(person.surname)s \n - 2) template with iteration - Telephone numbers: \n - %(phone_numbers|Telephone number:%(number)s) - 3) template with iteration and class attributes - Addresses: \n - %(Addresses|Street:%(address.street)s\nNumber%(address.number)) - - Instances of this class are initialized with a template string and - the dictionary for performing substitution. The class implements the - __str__ method, so it can be directly printed. - """ - - def __init__(self, template, data): - self._template = template - self.data = data - - def __str__(self): - return self._template % self - - def __getitem__(self, key): - items = key.split("|") - # Note(madhav-puri): for now the logic supports only 0 or 1 list - # indicators (|) in a template. - if len(items) > 2: - raise Exception("Found more than one list indicator (|).") - if len(items) == 1: - return self._make_attribute(key) - else: - # Note(salvatore-orlando): items[0] must be subscriptable - return self._make_list(self._make_attribute(items[0]), items[1]) - - def _make_attribute(self, item): - """ Renders an entity attribute key in the template. - e.g.: entity.attribute - """ - items = item.split('.') - attr = self.data[items[0]] - for _item in items[1:]: - attr = attr[_item] - return attr - - def _make_list(self, items, inner_template): - """ Renders a list key in the template. - e.g.: %(list|item data:%(item)) - """ - #make sure list is subscriptable - if not hasattr(items, '__getitem__'): - raise Exception("Element is not iterable") - return "\n".join([str(OutputTemplate(inner_template, item)) - for item in items]) - - -class CmdOutputTemplate(OutputTemplate): - """ This class provides templated output for CLI commands. - Extends OutputTemplate loading a different template for each command. - """ - - _templates_v10 = dict( - list_nets=""" -Virtual Networks for Tenant %(tenant_id)s -%(networks|\tNetwork ID: %(id)s)s -""".strip(), - - list_nets_detail=""" -Virtual Networks for Tenant %(tenant_id)s -%(networks|\tNetwork %(name)s with ID: %(id)s)s -""".strip(), - - show_net=""" -Network ID: %(network.id)s -Network Name: %(network.name)s -""".strip(), - - show_net_detail=""" -Network ID: %(network.id)s -Network Name: %(network.name)s -Ports: %(network.ports|\tID: %(id)s -\t\tadministrative state: %(state)s -\t\tinterface: %(attachment.id)s)s -""".strip(), - - create_net=""" -Created a new Virtual Network with ID: %(network_id)s -for Tenant: %(tenant_id)s -""".strip(), - - update_net=""" -Updated Virtual Network with ID: %(network.id)s -for Tenant: %(tenant_id)s -""".strip(), - - delete_net=""" -Deleted Virtual Network with ID: %(network_id)s -for Tenant %(tenant_id)s -""".strip(), - - list_ports=""" -Ports on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -%(ports|\tLogical Port: %(id)s)s -""".strip(), - - list_ports_detail=""" -Ports on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -%(ports|\tLogical Port: %(id)s -\t\tadministrative State: %(state)s)s -""".strip(), - - create_port=""" -Created new Logical Port with ID: %(port_id)s -on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -""".strip(), - - show_port=""" -Logical Port ID: %(port.id)s -administrative State: %(port.state)s -on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -""".strip(), - - show_port_detail=""" -Logical Port ID: %(port.id)s -administrative State: %(port.state)s -interface: %(port.attachment.id)s -on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -""".strip(), - - update_port=""" -Updated Logical Port with ID: %(port.id)s -on Virtual Network: %(network_id)s -for tenant: %(tenant_id)s -""".strip(), - - delete_port=""" -Deleted Logical Port with ID: %(port_id)s -on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -""".strip(), - - plug_iface=""" -Plugged interface %(attachment)s -into Logical Port: %(port_id)s -on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -""".strip(), - - unplug_iface=""" -Unplugged interface from Logical Port: %(port_id)s -on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -""".strip(), - - show_iface=""" -interface: %(iface.id)s -plugged in Logical Port ID: %(port_id)s -on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -""".strip(), - ) - - _templates_v11 = _templates_v10.copy() - _templates_v11.update(dict( - show_net=""" -Network ID: %(network.id)s -network Name: %(network.name)s -operational Status: %(network.op-status)s -""".strip(), - - show_net_detail=""" -Network ID: %(network.id)s -Network Name: %(network.name)s -operational Status: %(network.op-status)s -Ports: %(network.ports|\tID: %(id)s -\t\tadministrative state: %(state)s -\t\tinterface: %(attachment.id)s)s -""".strip(), - - show_port=""" -Logical Port ID: %(port.id)s -administrative state: %(port.state)s -operational status: %(port.op-status)s -on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -""".strip(), - - show_port_detail=""" -Logical Port ID: %(port.id)s -administrative State: %(port.state)s -operational status: %(port.op-status)s -interface: %(port.attachment.id)s -on Virtual Network: %(network_id)s -for Tenant: %(tenant_id)s -""".strip(), - )) - - _templates = { - '1.0': _templates_v10, - '1.1': _templates_v11 - } - - def __init__(self, cmd, data, version): - super(CmdOutputTemplate, self).__init__( - self._templates[version][cmd], data) - - -def _handle_exception(ex): - status_code = None - message = None - # Retrieve dict at 1st element of tuple at last argument - if ex.args and isinstance(ex.args[-1][0], dict): - status_code = ex.args[-1][0].get('status_code', None) - message = ex.args[-1][0].get('message', None) - msg_1 = ("Command failed with error code: %s" % - (status_code or '')) - msg_2 = "Error message:%s" % (message or '') - print msg_1 - print msg_2 - else: - traceback.print_exc() - - -def prepare_output(cmd, tenant_id, response, version): - LOG.debug("Preparing output for response:%s, version:%s" % - (response, version)) - response['tenant_id'] = tenant_id - output = str(CmdOutputTemplate(cmd, response, version)) - LOG.debug("Finished preparing output for command:%s", cmd) - return output - - -def list_nets(client, *args): - tenant_id, version = args - try: - res = client.list_networks() - LOG.debug("Operation 'list_networks' executed.") - output = prepare_output("list_nets", tenant_id, res, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_nets_v11(client, *args): - filters = {} - tenant_id, version = args[:2] - if len(args) > 2: - filters = args[2] - try: - res = client.list_networks(**filters) - LOG.debug("Operation 'list_networks' executed.") - output = prepare_output("list_nets", tenant_id, res, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_nets_detail(client, *args): - tenant_id, version = args - try: - res = client.list_networks_details() - LOG.debug("Operation 'list_networks_details' executed.") - output = prepare_output("list_nets_detail", tenant_id, res, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_nets_detail_v11(client, *args): - filters = {} - tenant_id, version = args[:2] - if len(args) > 2: - filters = args[2] - try: - res = client.list_networks_details(**filters) - LOG.debug("Operation 'list_networks_details' executed.") - output = prepare_output("list_nets_detail", tenant_id, res, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def create_net(client, *args): - tenant_id, name, version = args - data = {'network': {'name': name}} - new_net_id = None - try: - res = client.create_network(data) - new_net_id = res["network"]["id"] - LOG.debug("Operation 'create_network' executed.") - output = prepare_output("create_net", - tenant_id, - dict(network_id=new_net_id), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def delete_net(client, *args): - tenant_id, network_id, version = args - try: - client.delete_network(network_id) - LOG.debug("Operation 'delete_network' executed.") - output = prepare_output("delete_net", - tenant_id, - dict(network_id=network_id), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def show_net(client, *args): - tenant_id, network_id, version = args - try: - res = client.show_network(network_id)["network"] - LOG.debug("Operation 'show_network' executed.") - output = prepare_output("show_net", tenant_id, - dict(network=res), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def show_net_detail(client, *args): - tenant_id, network_id, version = args - try: - res = client.show_network_details(network_id)["network"] - LOG.debug("Operation 'show_network_details' executed.") - if not len(res["ports"]): - res["ports"] = [{"id": "", - "state": "", - "attachment": {"id": ""}}] - else: - for port in res["ports"]: - if "attachment" not in port: - port["attachment"] = {"id": ""} - - output = prepare_output("show_net_detail", - tenant_id, - dict(network=res), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def update_net(client, *args): - tenant_id, network_id, param_data, version = args - data = {'network': {}} - for kv in param_data.split(","): - k, v = kv.split("=") - data['network'][k] = v - data['network']['id'] = network_id - try: - client.update_network(network_id, data) - LOG.debug("Operation 'update_network' executed.") - # Response has no body. Use data for populating output - output = prepare_output("update_net", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_ports(client, *args): - tenant_id, network_id, version = args - try: - ports = client.list_ports(network_id) - LOG.debug("Operation 'list_ports' executed.") - data = ports - data['network_id'] = network_id - output = prepare_output("list_ports", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_ports_v11(client, *args): - filters = {} - tenant_id, network_id, version = args[:3] - if len(args) > 3: - filters = args[3] - try: - ports = client.list_ports(network_id, **filters) - LOG.debug("Operation 'list_ports' executed.") - data = ports - data['network_id'] = network_id - output = prepare_output("list_ports", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_ports_detail(client, *args): - tenant_id, network_id, version = args - try: - ports = client.list_ports_details(network_id) - LOG.debug("Operation 'list_ports_details' executed.") - data = ports - data['network_id'] = network_id - output = prepare_output("list_ports_detail", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def list_ports_detail_v11(client, *args): - filters = {} - tenant_id, network_id, version = args[:3] - if len(args) > 3: - filters = args[3] - try: - ports = client.list_ports_details(network_id, **filters) - LOG.debug("Operation 'list_ports' executed.") - data = ports - data['network_id'] = network_id - output = prepare_output("list_ports_detail", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def create_port(client, *args): - tenant_id, network_id, version = args - try: - res = client.create_port(network_id) - LOG.debug("Operation 'create_port' executed.") - new_port_id = res["port"]["id"] - output = prepare_output("create_port", - tenant_id, - dict(network_id=network_id, - port_id=new_port_id), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def delete_port(client, *args): - tenant_id, network_id, port_id, version = args - try: - client.delete_port(network_id, port_id) - LOG.debug("Operation 'delete_port' executed.") - output = prepare_output("delete_port", - tenant_id, - dict(network_id=network_id, - port_id=port_id), - version) - print output - except Exception as ex: - _handle_exception(ex) - return - - -def show_port(client, *args): - tenant_id, network_id, port_id, version = args - try: - port = client.show_port(network_id, port_id)["port"] - LOG.debug("Operation 'show_port' executed.") - output = prepare_output("show_port", tenant_id, - dict(network_id=network_id, - port=port), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def show_port_detail(client, *args): - tenant_id, network_id, port_id, version = args - try: - port = client.show_port_details(network_id, port_id)["port"] - LOG.debug("Operation 'show_port_details' executed.") - if 'attachment' not in port: - port['attachment'] = {'id': ''} - output = prepare_output("show_port_detail", tenant_id, - dict(network_id=network_id, - port=port), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def update_port(client, *args): - tenant_id, network_id, port_id, param_data, version = args - data = {'port': {}} - for kv in param_data.split(","): - k, v = kv.split("=") - data['port'][k] = v - data['network_id'] = network_id - data['port']['id'] = port_id - try: - client.update_port(network_id, port_id, data) - LOG.debug("Operation 'udpate_port' executed.") - # Response has no body. Use data for populating output - output = prepare_output("update_port", tenant_id, data, version) - print output - except Exception as ex: - _handle_exception(ex) - - -def plug_iface(client, *args): - tenant_id, network_id, port_id, attachment, version = args - try: - data = {'attachment': {'id': '%s' % attachment}} - client.attach_resource(network_id, port_id, data) - LOG.debug("Operation 'attach_resource' executed.") - output = prepare_output("plug_iface", tenant_id, - dict(network_id=network_id, - port_id=port_id, - attachment=attachment), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def unplug_iface(client, *args): - tenant_id, network_id, port_id, version = args - try: - client.detach_resource(network_id, port_id) - LOG.debug("Operation 'detach_resource' executed.") - output = prepare_output("unplug_iface", - tenant_id, - dict(network_id=network_id, - port_id=port_id), - version) - print output - except Exception as ex: - _handle_exception(ex) - - -def show_iface(client, *args): - tenant_id, network_id, port_id, version = args - try: - iface = client.show_port_attachment(network_id, port_id)['attachment'] - if 'id' not in iface: - iface['id'] = '' - LOG.debug("Operation 'show_port_attachment' executed.") - output = prepare_output("show_iface", tenant_id, - dict(network_id=network_id, - port_id=port_id, - iface=iface), - version) - print output - except Exception as ex: - _handle_exception(ex) diff --git a/quantumclient/common/exceptions.py b/quantumclient/common/exceptions.py deleted file mode 100644 index 33526c2d0..000000000 --- a/quantumclient/common/exceptions.py +++ /dev/null @@ -1,122 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Nicira Networks, 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. - -""" -Quantum base exception handling. -""" - - -class QuantumException(Exception): - """Base Quantum Exception - - Taken from nova.exception.NovaException - 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, **kwargs): - 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 NotFound(QuantumException): - pass - - -class QuantumClientException(QuantumException): - - def __init__(self, **kwargs): - message = kwargs.get('message') - if message: - self.message = message - super(QuantumClientException, self).__init__(**kwargs) - - -# 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 -class NetworkNotFoundClient(QuantumClientException): - pass - - -class PortNotFoundClient(QuantumClientException): - pass - - -class MalformedResponseBody(QuantumException): - message = _("Malformed response body: %(reason)s") - - -class StateInvalidClient(QuantumClientException): - pass - - -class NetworkInUseClient(QuantumClientException): - pass - - -class PortInUseClient(QuantumClientException): - pass - - -class AlreadyAttachedClient(QuantumClientException): - pass - - -class NotAuthorized(QuantumClientException): - pass - - -class QuantumCLIError(QuantumClientException): - """ Exception raised when command line parsing fails """ - pass - - -class ConnectionFailed(QuantumClientException): - message = _("Connection to quantum failed: %(reason)s") - - -class BadInputError(Exception): - """Error resulting from a client sending bad input to a server""" - pass - - -class Error(Exception): - def __init__(self, message=None): - super(Error, self).__init__(message) - - -class MalformedRequestBody(QuantumException): - message = _("Malformed request body: %(reason)s") - - -class Invalid(Error): - pass - - -class InvalidContentType(Invalid): - message = _("Invalid content type %(content_type)s.") diff --git a/quantumclient/common/serializer.py b/quantumclient/common/serializer.py deleted file mode 100644 index 6e914673a..000000000 --- a/quantumclient/common/serializer.py +++ /dev/null @@ -1,155 +0,0 @@ -from xml.dom import minidom - -from quantumclient.common import exceptions as exception -from quantumclient.common import utils - - -# NOTE(maru): this class is duplicated from quantum.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': self._to_json, - 'application/xml': self._to_xml, - } - - 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)(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. - - """ - try: - return self.get_deserialize_handler(content_type)(datastring) - except Exception: - raise exception.MalformedResponseBody( - reason="Unable to deserialize response body") - - def get_deserialize_handler(self, content_type): - handlers = { - 'application/json': self._from_json, - 'application/xml': self._from_xml, - } - - try: - return handlers[content_type] - except Exception: - raise exception.InvalidContentType(content_type=content_type) - - def _from_json(self, datastring): - return utils.loads(datastring) - - def _from_xml(self, datastring): - xmldata = self.metadata.get('application/xml', {}) - plurals = set(xmldata.get('plurals', {})) - node = minidom.parseString(datastring).childNodes[0] - return {node.nodeName: self._from_xml_node(node, plurals)} - - def _from_xml_node(self, node, listnames): - """Convert a minidom node to a simple Python type. - - listnames is a collection of names of XML nodes whose subnodes should - be considered list items. - - """ - if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3: - return node.childNodes[0].nodeValue - elif node.nodeName in listnames: - return [self._from_xml_node(n, listnames) - for n in node.childNodes if n.nodeType != node.TEXT_NODE] - else: - result = dict() - for attr in node.attributes.keys(): - result[attr] = node.attributes[attr].nodeValue - for child in node.childNodes: - if child.nodeType != node.TEXT_NODE: - result[child.nodeName] = self._from_xml_node(child, - listnames) - return result - - def _to_json(self, data): - return utils.dumps(data) - - def _to_xml(self, data): - metadata = self.metadata.get('application/xml', {}) - # We expect data to contain a single key which is the XML root. - root_key = data.keys()[0] - doc = minidom.Document() - node = self._to_xml_node(doc, metadata, root_key, data[root_key]) - - xmlns = node.getAttribute('xmlns') - if not xmlns and self.default_xmlns: - node.setAttribute('xmlns', self.default_xmlns) - - return node.toprettyxml(indent='', newl='') - - def _to_xml_node(self, doc, metadata, nodename, data): - """Recursive method to convert data members to XML nodes.""" - result = doc.createElement(nodename) - - # Set the xml namespace if one is specified - # TODO(justinsb): We could also use prefixes on the keys - xmlns = metadata.get('xmlns', None) - if xmlns: - result.setAttribute('xmlns', xmlns) - if type(data) is list: - collections = metadata.get('list_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for item in data: - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(item)) - result.appendChild(node) - return result - singular = metadata.get('plurals', {}).get(nodename, None) - if singular is None: - if nodename.endswith('s'): - singular = nodename[:-1] - else: - singular = 'item' - for item in data: - node = self._to_xml_node(doc, metadata, singular, item) - result.appendChild(node) - elif type(data) is dict: - collections = metadata.get('dict_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for k, v in data.items(): - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(k)) - text = doc.createTextNode(str(v)) - node.appendChild(text) - result.appendChild(node) - return result - attrs = metadata.get('attributes', {}).get(nodename, {}) - for k, v in data.items(): - if k in attrs: - result.setAttribute(k, str(v)) - else: - node = self._to_xml_node(doc, metadata, k, v) - result.appendChild(node) - else: - # Type is atom. - node = doc.createTextNode(str(data)) - result.appendChild(node) - return result diff --git a/quantumclient/common/utils.py b/quantumclient/common/utils.py deleted file mode 100644 index afb554e26..000000000 --- a/quantumclient/common/utils.py +++ /dev/null @@ -1,70 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011, Nicira Networks, 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. -# @author: Somik Behera, Nicira Networks, Inc. - -"""Utilities and helper functions.""" - -import datetime -import json -import os - - -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 to_primitive(value): - if type(value) is type([]) or type(value) is type((None,)): - o = [] - for v in value: - o.append(to_primitive(v)) - return o - elif type(value) is type({}): - o = {} - for k, v in value.iteritems(): - o[k] = to_primitive(v) - return o - elif isinstance(value, datetime.datetime): - return str(value) - elif hasattr(value, 'iteritems'): - return to_primitive(dict(value.iteritems())) - elif hasattr(value, '__iter__'): - return to_primitive(list(value)) - else: - return value - - -def dumps(value): - try: - return json.dumps(value) - except TypeError: - pass - return json.dumps(to_primitive(value)) - - -def loads(s): - return json.loads(s) diff --git a/quantumclient/tests/unit/stubs.py b/quantumclient/tests/unit/stubs.py deleted file mode 100644 index 3295753fb..000000000 --- a/quantumclient/tests/unit/stubs.py +++ /dev/null @@ -1,65 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" Stubs for client tools unit tests """ - - -from quantum import api as server -from quantum.tests.unit import testlib_api - - -class FakeStdout: - - 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 FakeHTTPConnection: - """ stub HTTP connection class for CLI testing """ - def __init__(self, _1, _2): - # Ignore host and port parameters - self._req = None - plugin = 'quantum.plugins.sample.SamplePlugin.FakePlugin' - options = dict(plugin_provider=plugin) - self._api = server.APIRouterV11(options) - - def request(self, method, action, body, headers): - # TODO: remove version prefix from action! - parts = action.split('/', 2) - path = '/' + parts[2] - self._req = testlib_api.create_request(path, body, "application/json", - method) - - def getresponse(self): - res = self._req.get_response(self._api) - - def _fake_read(): - """ Trick for making a webob.Response look like a - httplib.Response - - """ - return res.body - - setattr(res, 'read', _fake_read) - return res diff --git a/quantumclient/tests/unit/test_cli.py b/quantumclient/tests/unit/test_cli.py deleted file mode 100644 index 6cf618c47..000000000 --- a/quantumclient/tests/unit/test_cli.py +++ /dev/null @@ -1,825 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 ???? -# 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. -# @author: Salvatore Orlando, Citrix Systems - -""" Module containing unit tests for Quantum - command line interface - -""" - -import logging -import sys -import unittest - -from quantum import api as server -from quantum.db import api as db -from quantumclient import cli_lib as cli -from quantumclient import Client -from quantumclient.tests.unit import stubs as client_stubs - - -LOG = logging.getLogger('quantumclient.tests.test_cli') -API_VERSION = "1.1" -FORMAT = 'json' - - -class CLITest(unittest.TestCase): - - def setUp(self): - """Prepare the test environment""" - options = {} - options['plugin_provider'] = ( - 'quantum.plugins.sample.SamplePlugin.FakePlugin') - #TODO: make the version of the API router configurable - self.api = server.APIRouterV11(options) - - self.tenant_id = "test_tenant" - self.network_name_1 = "test_network_1" - self.network_name_2 = "test_network_2" - self.version = API_VERSION - # Prepare client and plugin manager - self.client = Client(tenant=self.tenant_id, - format=FORMAT, - testingStub=client_stubs.FakeHTTPConnection, - version=self.version) - # Redirect stdout - self.fake_stdout = client_stubs.FakeStdout() - sys.stdout = self.fake_stdout - - def tearDown(self): - """Clear the test environment""" - db.clear_db() - sys.stdout = sys.__stdout__ - - def _verify_list_networks(self): - # Verification - get raw result from db - nw_list = db.network_list(self.tenant_id) - networks = [{'id': nw.uuid, 'name': nw.name} - for nw in nw_list] - # Fill CLI template - output = cli.prepare_output('list_nets', - self.tenant_id, - dict(networks=networks), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_list_networks_details(self): - # Verification - get raw result from db - nw_list = db.network_list(self.tenant_id) - networks = [dict(id=nw.uuid, name=nw.name) for nw in nw_list] - # Fill CLI template - output = cli.prepare_output('list_nets_detail', - self.tenant_id, - dict(networks=networks), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_create_network(self): - # Verification - get raw result from db - nw_list = db.network_list(self.tenant_id) - if len(nw_list) != 1: - self.fail("No network created") - network_id = nw_list[0].uuid - # Fill CLI template - output = cli.prepare_output('create_net', - self.tenant_id, - dict(network_id=network_id), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_delete_network(self, network_id): - # Verification - get raw result from db - nw_list = db.network_list(self.tenant_id) - if len(nw_list) != 0: - self.fail("DB should not contain any network") - # Fill CLI template - output = cli.prepare_output('delete_net', - self.tenant_id, - dict(network_id=network_id), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_update_network(self): - # Verification - get raw result from db - nw_list = db.network_list(self.tenant_id) - network_data = {'id': nw_list[0].uuid, - 'name': nw_list[0].name, - 'op-status': nw_list[0].op_status} - # Fill CLI template - output = cli.prepare_output('update_net', - self.tenant_id, - dict(network=network_data), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_show_network(self): - # Verification - get raw result from db - nw = db.network_list(self.tenant_id)[0] - network = {'id': nw.uuid, - 'name': nw.name, - 'op-status': nw.op_status} - # Fill CLI template - output = cli.prepare_output('show_net', - self.tenant_id, - dict(network=network), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_show_network_details(self): - # Verification - get raw result from db - nw = db.network_list(self.tenant_id)[0] - network = {'id': nw.uuid, - 'name': nw.name, - 'op-status': nw.op_status} - port_list = db.port_list(nw.uuid) - if not port_list: - network['ports'] = [ - { - 'id': '', - 'state': '', - 'attachment': { - 'id': '', - }, - }, - ] - else: - network['ports'] = [] - for port in port_list: - network['ports'].append({ - 'id': port.uuid, - 'state': port.state, - 'attachment': { - 'id': port.interface_id or '', - }, - }) - - # Fill CLI template - output = cli.prepare_output('show_net_detail', - self.tenant_id, - dict(network=network), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_list_ports(self, network_id): - # Verification - get raw result from db - port_list = db.port_list(network_id) - ports = [dict(id=port.uuid, state=port.state) - for port in port_list] - # Fill CLI template - output = cli.prepare_output('list_ports', - self.tenant_id, - dict(network_id=network_id, - ports=ports), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_list_ports_details(self, network_id): - # Verification - get raw result from db - port_list = db.port_list(network_id) - ports = [dict(id=port.uuid, state=port.state) - for port in port_list] - # Fill CLI template - output = cli.prepare_output('list_ports_detail', - self.tenant_id, - dict(network_id=network_id, - ports=ports), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_create_port(self, network_id): - # Verification - get raw result from db - port_list = db.port_list(network_id) - if len(port_list) != 1: - self.fail("No port created") - port_id = port_list[0].uuid - # Fill CLI template - output = cli.prepare_output('create_port', - self.tenant_id, - dict(network_id=network_id, - port_id=port_id), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_delete_port(self, network_id, port_id): - # Verification - get raw result from db - port_list = db.port_list(network_id) - if len(port_list) != 0: - self.fail("DB should not contain any port") - # Fill CLI template - output = cli.prepare_output('delete_port', - self.tenant_id, - dict(network_id=network_id, - port_id=port_id), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_update_port(self, network_id, port_id): - # Verification - get raw result from db - port = db.port_get(port_id, network_id) - port_data = {'id': port.uuid, - 'state': port.state, - 'op-status': port.op_status} - # Fill CLI template - output = cli.prepare_output('update_port', - self.tenant_id, - dict(network_id=network_id, - port=port_data), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_show_port(self, network_id, port_id): - # Verification - get raw result from db - port = db.port_get(port_id, network_id) - port_data = {'id': port.uuid, - 'state': port.state, - 'attachment': {'id': port.interface_id or ''}, - 'op-status': port.op_status} - - # Fill CLI template - output = cli.prepare_output('show_port', - self.tenant_id, - dict(network_id=network_id, - port=port_data), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_show_port_details(self, network_id, port_id): - # Verification - get raw result from db - # TODO(salvatore-orlando): Must resolve this issue with - # attachment in separate bug fix. - port = db.port_get(port_id, network_id) - port_data = {'id': port.uuid, - 'state': port.state, - 'attachment': {'id': port.interface_id or ''}, - 'op-status': port.op_status} - - # Fill CLI template - output = cli.prepare_output('show_port_detail', - self.tenant_id, - dict(network_id=network_id, - port=port_data), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_plug_iface(self, network_id, port_id): - # Verification - get raw result from db - port = db.port_get(port_id, network_id) - # Fill CLI template - output = cli.prepare_output("plug_iface", - self.tenant_id, - dict(network_id=network_id, - port_id=port['uuid'], - attachment=port['interface_id']), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_unplug_iface(self, network_id, port_id): - # Verification - get raw result from db - port = db.port_get(port_id, network_id) - # Fill CLI template - output = cli.prepare_output("unplug_iface", - self.tenant_id, - dict(network_id=network_id, - port_id=port['uuid']), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def _verify_show_iface(self, network_id, port_id): - # Verification - get raw result from db - port = db.port_get(port_id, network_id) - iface = {'id': port.interface_id or ''} - - # Fill CLI template - output = cli.prepare_output('show_iface', - self.tenant_id, - dict(network_id=network_id, - port_id=port.uuid, - iface=iface), - self.version) - # Verify! - # Must add newline at the end to match effect of print call - self.assertEquals(self.fake_stdout.make_string(), output + '\n') - - def test_list_networks_v10(self): - try: - # Pre-populate data for testing using db api - db.network_create(self.tenant_id, self.network_name_1) - db.network_create(self.tenant_id, self.network_name_2) - - cli.list_nets(self.client, - self.tenant_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_networks_v10 failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_networks() - - def test_list_networks_details_v10(self): - try: - # Pre-populate data for testing using db api - db.network_create(self.tenant_id, self.network_name_1) - db.network_create(self.tenant_id, self.network_name_2) - - cli.list_nets_detail(self.client, - self.tenant_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_networks_details_v10 failed due to" + - " an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_networks_details() - - def test_list_networks_v11(self): - try: - # Pre-populate data for testing using db api - db.network_create(self.tenant_id, self.network_name_1) - db.network_create(self.tenant_id, self.network_name_2) - #TODO: test filters - cli.list_nets_v11(self.client, - self.tenant_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_networks_v11 failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_networks() - - def test_list_networks_details_v11(self): - try: - # Pre-populate data for testing using db api - db.network_create(self.tenant_id, self.network_name_1) - db.network_create(self.tenant_id, self.network_name_2) - #TODO: test filters - cli.list_nets_detail_v11(self.client, - self.tenant_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_networks_details_v11 failed due to " + - "an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_networks_details() - - def test_create_network(self): - try: - cli.create_net(self.client, - self.tenant_id, - "test", - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_create_network failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_create_network() - - def test_delete_network(self): - try: - db.network_create(self.tenant_id, self.network_name_1) - network_id = db.network_list(self.tenant_id)[0]['uuid'] - cli.delete_net(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_delete_network failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_delete_network(network_id) - - def test_show_network(self): - try: - # Load some data into the datbase - net = db.network_create(self.tenant_id, self.network_name_1) - cli.show_net(self.client, - self.tenant_id, - net['uuid'], - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_detail_network failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_network() - - def test_show_network_details_no_ports(self): - try: - # Load some data into the datbase - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - cli.show_net_detail(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_network_details_no_ports failed due to" + - " an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_network_details() - - def test_show_network_details(self): - iface_id = "flavor crystals" - try: - # Load some data into the datbase - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - db.port_set_attachment(port_id, network_id, iface_id) - port = db.port_create(network_id) - cli.show_net_detail(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_network_details failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_network_details() - - def test_update_network(self): - try: - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - cli.update_net(self.client, - self.tenant_id, - network_id, - 'name=%s' % self.network_name_2, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_update_network failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_update_network() - - def test_list_ports_v10(self): - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - db.port_create(network_id) - db.port_create(network_id) - cli.list_ports(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_ports failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_ports(network_id) - - def test_list_ports_details_v10(self): - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - db.port_create(network_id) - db.port_create(network_id) - cli.list_ports_detail(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_ports_details_v10 failed due to" + - " an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_ports_details(network_id) - - def test_list_ports_v11(self): - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - db.port_create(network_id) - db.port_create(network_id) - #TODO: test filters - cli.list_ports_v11(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_ports_v11 failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_ports(network_id) - - def test_list_ports_details_v11(self): - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - db.port_create(network_id) - db.port_create(network_id) - #TODO: test filters - cli.list_ports_detail_v11(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_list_ports_details_v11 failed due to " + - "an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_list_ports_details(network_id) - - def test_create_port(self): - network_id = None - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - cli.create_port(self.client, - self.tenant_id, - network_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_create_port failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_create_port(network_id) - - def test_delete_port(self): - network_id = None - port_id = None - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - cli.delete_port(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_delete_port failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_delete_port(network_id, port_id) - - def test_update_port(self): - try: - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - # Default state is DOWN - change to ACTIVE. - cli.update_port(self.client, - self.tenant_id, - network_id, - port_id, - 'state=ACTIVE', - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_update_port failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_update_port(network_id, port_id) - - def test_show_port(self): - network_id = None - port_id = None - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - cli.show_port(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_port failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_port(network_id, port_id) - - def test_show_port_details_no_attach(self): - network_id = None - port_id = None - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - cli.show_port_detail(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_port_details_no_attach failed due to" + - " an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_port_details(network_id, port_id) - - def test_show_port_details_with_attach(self): - network_id = None - port_id = None - iface_id = "flavor crystals" - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - db.port_set_attachment(port_id, network_id, iface_id) - cli.show_port_detail(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_port_details_with_attach failed due" + - " to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_port_details(network_id, port_id) - - def test_plug_iface(self): - network_id = None - port_id = None - try: - # Load some data into the datbase - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(net['uuid']) - port_id = port['uuid'] - cli.plug_iface(self.client, - self.tenant_id, - network_id, - port_id, - "test_iface_id", - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_plug_iface failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_plug_iface(network_id, port_id) - - def test_unplug_iface(self): - network_id = None - port_id = None - try: - # Load some data into the datbase - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(net['uuid']) - port_id = port['uuid'] - db.port_set_attachment(port_id, network_id, "test_iface_id") - cli.unplug_iface(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_plug_iface failed due to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_unplug_iface(network_id, port_id) - - def test_show_iface_no_attach(self): - network_id = None - port_id = None - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - cli.show_iface(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_iface_no_attach failed due to" + - " an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_iface(network_id, port_id) - - def test_show_iface_with_attach(self): - network_id = None - port_id = None - iface_id = "flavor crystals" - try: - # Pre-populate data for testing using db api - net = db.network_create(self.tenant_id, self.network_name_1) - network_id = net['uuid'] - port = db.port_create(network_id) - port_id = port['uuid'] - db.port_set_attachment(port_id, network_id, iface_id) - cli.show_iface(self.client, - self.tenant_id, - network_id, - port_id, - self.version) - except: - LOG.exception("Exception caught: %s", sys.exc_info()) - self.fail("test_show_iface_with_attach failed due" + - " to an exception") - - LOG.debug("Operation completed. Verifying result") - LOG.debug(self.fake_stdout.content) - self._verify_show_iface(network_id, port_id) diff --git a/quantumclient/tests/unit/test_clientlib.py b/quantumclient/tests/unit/test_clientlib.py deleted file mode 100644 index 3b88f1521..000000000 --- a/quantumclient/tests/unit/test_clientlib.py +++ /dev/null @@ -1,770 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Cisco Systems -# 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. -# @author: Tyler Smith, Cisco Systems - -import logging -import re -import unittest - -from quantumclient.common import exceptions -from quantumclient.common.serializer import Serializer -from quantumclient import Client - - -LOG = logging.getLogger('quantumclient.tests.test_api') - - -# Set a couple tenants to use for testing -TENANT_1 = 'totore' -TENANT_2 = 'totore2' - - -class ServerStub(): - """This class stubs a basic server for the API client to talk to""" - - class Response(object): - """This class stubs a basic response to send the API client""" - def __init__(self, content=None, status=None): - self.content = content - self.status = status - - def read(self): - return self.content - - def status(self): - return self.status - - # To test error codes, set the host to 10.0.0.1, and the port to the code - def __init__(self, host, port=9696, key_file="", cert_file=""): - self.host = host - self.port = port - self.key_file = key_file - self.cert_file = cert_file - - def request(self, method, action, body, headers): - self.method = method - self.action = action - self.body = body - - def status(self, status=None): - return status or 200 - - def getresponse(self): - res = self.Response(status=self.status()) - - # If the host is 10.0.0.1, return the port as an error code - if self.host == "10.0.0.1": - res.status = self.port - return res - - # Extract important information from the action string to assure sanity - match = re.search('tenants/(.+?)/(.+)\.(json|xml)$', self.action) - - tenant = match.group(1) - path = match.group(2) - format = match.group(3) - - data = {'data': {'method': self.method, 'action': self.action, - 'body': self.body, 'tenant': tenant, 'path': path, - 'format': format, 'key_file': self.key_file, - 'cert_file': self.cert_file}} - - # Serialize it to the proper format so the API client can handle it - if data['data']['format'] == 'json': - res.content = Serializer().serialize(data, "application/json") - else: - res.content = Serializer().serialize(data, "application/xml") - return res - - -class APITest(unittest.TestCase): - - def setUp(self): - """ Setups a test environment for the API client """ - HOST = '127.0.0.1' - PORT = 9696 - USE_SSL = False - - self.client = Client(HOST, PORT, USE_SSL, TENANT_1, 'json', ServerStub) - - def _assert_sanity(self, call, status, method, path, data=[], params={}): - """ Perform common assertions to test the sanity of client requests """ - - # Handle an error case first - if status != 200: - (self.client.host, self.client.port) = ("10.0.0.1", status) - self.assertRaises(Exception, call, *data, **params) - return - - # Make the call, then get the data from the root node and assert it - data = call(*data, **params)['data'] - - self.assertEqual(data['method'], method) - self.assertEqual(data['format'], params['format']) - self.assertEqual(data['tenant'], params['tenant']) - self.assertEqual(data['path'], path) - - return data - - def _test_list_networks(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_list_networks - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.list_networks, - status, - "GET", - "networks", - data=[], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_list_networks - tenant:%s - format:%s - END", - format, tenant) - - def _test_list_networks_details(self, - tenant=TENANT_1, format='json', - status=200): - LOG.debug("_test_list_networks_details - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.list_networks_details, - status, - "GET", - "networks/detail", - data=[], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_list_networks_details - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_show_network(self, - tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_show_network - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.show_network, - status, - "GET", - "networks/001", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_show_network - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_show_network_details(self, - tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_show_network_details - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.show_network_details, - status, - "GET", - "networks/001/detail", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_show_network_details - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_create_network(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_create_network - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.create_network, - status, - "POST", - "networks", - data=[{'network': {'net-name': 'testNetwork'}}], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_create_network - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_update_network(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_update_network - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.update_network, - status, - "PUT", - "networks/001", - data=["001", - {'network': {'net-name': 'newName'}}], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_update_network - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_delete_network(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_delete_network - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.delete_network, - status, - "DELETE", - "networks/001", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_delete_network - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_list_ports(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_list_ports - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.list_ports, - status, - "GET", - "networks/001/ports", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_list_ports - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_list_ports_details(self, - tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_list_ports_details - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.list_ports_details, - status, - "GET", - "networks/001/ports/detail", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_list_ports_details - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_show_port(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_show_port - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.show_port, - status, - "GET", - "networks/001/ports/001", - data=["001", "001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_show_port - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_show_port_details(self, - tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_show_port_details - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.show_port_details, - status, - "GET", - "networks/001/ports/001/detail", - data=["001", "001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_show_port_details - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_create_port(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_create_port - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.create_port, - status, - "POST", - "networks/001/ports", - data=["001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_create_port - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_delete_port(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_delete_port - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.delete_port, - status, - "DELETE", - "networks/001/ports/001", - data=["001", "001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_delete_port - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_update_port(self, tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_update_port - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.update_port, - status, - "PUT", - "networks/001/ports/001", - data=["001", "001", - {'port': {'state': 'ACTIVE'}}], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_update_port - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_show_port_attachment(self, - tenant=TENANT_1, format='json', status=200): - LOG.debug("_test_show_port_attachment - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.show_port_attachment, - status, - "GET", - "networks/001/ports/001/attachment", - data=["001", "001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_show_port_attachment - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_attach_resource(self, tenant=TENANT_1, - format='json', status=200): - LOG.debug("_test_attach_resource - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.attach_resource, - status, - "PUT", - "networks/001/ports/001/attachment", - data=["001", "001", - {'resource': {'id': '1234'}}], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_attach_resource - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_detach_resource(self, tenant=TENANT_1, - format='json', status=200): - LOG.debug("_test_detach_resource - tenant:%s " - "- format:%s - START", format, tenant) - - self._assert_sanity(self.client.detach_resource, - status, - "DELETE", - "networks/001/ports/001/attachment", - data=["001", "001"], - params={'tenant': tenant, 'format': format}) - - LOG.debug("_test_detach_resource - tenant:%s " - "- format:%s - END", format, tenant) - - def _test_ssl_certificates(self, tenant=TENANT_1, - format='json', status=200): - LOG.debug("_test_ssl_certificates - tenant:%s " - "- format:%s - START", format, tenant) - - # Set SSL, and our cert file - self.client.use_ssl = True - cert_file = "/fake.cert" - self.client.key_file = self.client.cert_file = cert_file - - data = self._assert_sanity(self.client.list_networks, - status, - "GET", - "networks", - data=[], - params={'tenant': tenant, 'format': format}) - - self.assertEquals(data["key_file"], cert_file) - self.assertEquals(data["cert_file"], cert_file) - - LOG.debug("_test_ssl_certificates - tenant:%s " - "- format:%s - END", format, tenant) - - def test_list_networks_json(self): - self._test_list_networks(format='json') - - def test_list_networks_xml(self): - self._test_list_networks(format='xml') - - def test_list_networks_alt_tenant(self): - self._test_list_networks(tenant=TENANT_2) - - def test_list_networks_error_470(self): - self._test_list_networks(status=470) - - def test_list_networks_error_401(self): - self._test_list_networks(status=401) - - def test_list_networks_details_json(self): - self._test_list_networks_details(format='json') - - def test_list_networks_details_xml(self): - self._test_list_networks_details(format='xml') - - def test_list_networks_details_alt_tenant(self): - self._test_list_networks_details(tenant=TENANT_2) - - def test_list_networks_details_error_470(self): - self._test_list_networks_details(status=470) - - def test_list_networks_details_error_401(self): - self._test_list_networks_details(status=401) - - def test_show_network_json(self): - self._test_show_network(format='json') - - def test_show_network__xml(self): - self._test_show_network(format='xml') - - def test_show_network_alt_tenant(self): - self._test_show_network(tenant=TENANT_2) - - def test_show_network_error_470(self): - self._test_show_network(status=470) - - def test_show_network_error_401(self): - self._test_show_network(status=401) - - def test_show_network_error_420(self): - self._test_show_network(status=420) - - def test_show_network_details_json(self): - self._test_show_network_details(format='json') - - def test_show_network_details_xml(self): - self._test_show_network_details(format='xml') - - def test_show_network_details_alt_tenant(self): - self._test_show_network_details(tenant=TENANT_2) - - def test_show_network_details_error_470(self): - self._test_show_network_details(status=470) - - def test_show_network_details_error_401(self): - self._test_show_network_details(status=401) - - def test_show_network_details_error_420(self): - self._test_show_network_details(status=420) - - def test_create_network_json(self): - self._test_create_network(format='json') - - def test_create_network_xml(self): - self._test_create_network(format='xml') - - def test_create_network_alt_tenant(self): - self._test_create_network(tenant=TENANT_2) - - def test_create_network_error_470(self): - self._test_create_network(status=470) - - def test_create_network_error_401(self): - self._test_create_network(status=401) - - def test_create_network_error_400(self): - self._test_create_network(status=400) - - def test_create_network_error_422(self): - self._test_create_network(status=422) - - def test_update_network_json(self): - self._test_update_network(format='json') - - def test_update_network_xml(self): - self._test_update_network(format='xml') - - def test_update_network_alt_tenant(self): - self._test_update_network(tenant=TENANT_2) - - def test_update_network_error_470(self): - self._test_update_network(status=470) - - def test_update_network_error_401(self): - self._test_update_network(status=401) - - def test_update_network_error_400(self): - self._test_update_network(status=400) - - def test_update_network_error_420(self): - self._test_update_network(status=420) - - def test_update_network_error_422(self): - self._test_update_network(status=422) - - def test_delete_network_json(self): - self._test_delete_network(format='json') - - def test_delete_network_xml(self): - self._test_delete_network(format='xml') - - def test_delete_network_alt_tenant(self): - self._test_delete_network(tenant=TENANT_2) - - def test_delete_network_error_470(self): - self._test_delete_network(status=470) - - def test_delete_network_error_401(self): - self._test_delete_network(status=401) - - def test_delete_network_error_420(self): - self._test_delete_network(status=420) - - def test_delete_network_error_421(self): - self._test_delete_network(status=421) - - def test_list_ports_json(self): - self._test_list_ports(format='json') - - def test_list_ports_xml(self): - self._test_list_ports(format='xml') - - def test_list_ports_alt_tenant(self): - self._test_list_ports(tenant=TENANT_2) - - def test_list_ports_error_470(self): - self._test_list_ports(status=470) - - def test_list_ports_error_401(self): - self._test_list_ports(status=401) - - def test_list_ports_error_420(self): - self._test_list_ports(status=420) - - def test_list_ports_details_json(self): - self._test_list_ports_details(format='json') - - def test_list_ports_details_xml(self): - self._test_list_ports_details(format='xml') - - def test_list_ports_details_alt_tenant(self): - self._test_list_ports_details(tenant=TENANT_2) - - def test_list_ports_details_error_470(self): - self._test_list_ports_details(status=470) - - def test_list_ports_details_error_401(self): - self._test_list_ports_details(status=401) - - def test_list_ports_details_error_420(self): - self._test_list_ports_details(status=420) - - def test_show_port_json(self): - self._test_show_port(format='json') - - def test_show_port_xml(self): - self._test_show_port(format='xml') - - def test_show_port_alt_tenant(self): - self._test_show_port(tenant=TENANT_2) - - def test_show_port_error_470(self): - self._test_show_port(status=470) - - def test_show_port_error_401(self): - self._test_show_port(status=401) - - def test_show_port_error_420(self): - self._test_show_port(status=420) - - def test_show_port_error_430(self): - self._test_show_port(status=430) - - def test_show_port_details_json(self): - self._test_show_port_details(format='json') - - def test_show_port_details_xml(self): - self._test_show_port_details(format='xml') - - def test_show_port_details_alt_tenant(self): - self._test_show_port_details(tenant=TENANT_2) - - def test_show_port_details_error_470(self): - self._test_show_port_details(status=470) - - def test_show_port_details_error_401(self): - self._test_show_port_details(status=401) - - def test_show_port_details_error_420(self): - self._test_show_port_details(status=420) - - def test_show_port_details_error_430(self): - self._test_show_port_details(status=430) - - def test_create_port_json(self): - self._test_create_port(format='json') - - def test_create_port_xml(self): - self._test_create_port(format='xml') - - def test_create_port_alt_tenant(self): - self._test_create_port(tenant=TENANT_2) - - def test_create_port_error_470(self): - self._test_create_port(status=470) - - def test_create_port_error_401(self): - self._test_create_port(status=401) - - def test_create_port_error_400(self): - self._test_create_port(status=400) - - def test_create_port_error_420(self): - self._test_create_port(status=420) - - def test_create_port_error_430(self): - self._test_create_port(status=430) - - def test_create_port_error_431(self): - self._test_create_port(status=431) - - def test_delete_port_json(self): - self._test_delete_port(format='json') - - def test_delete_port_xml(self): - self._test_delete_port(format='xml') - - def test_delete_port_alt_tenant(self): - self._test_delete_port(tenant=TENANT_2) - - def test_delete_port_error_470(self): - self._test_delete_port(status=470) - - def test_delete_port_error_401(self): - self._test_delete_port(status=401) - - def test_delete_port_error_420(self): - self._test_delete_port(status=420) - - def test_delete_port_error_430(self): - self._test_delete_port(status=430) - - def test_delete_port_error_432(self): - self._test_delete_port(status=432) - - def test_update_port_json(self): - self._test_update_port(format='json') - - def test_update_port_xml(self): - self._test_update_port(format='xml') - - def test_update_port_alt_tenant(self): - self._test_update_port(tenant=TENANT_2) - - def test_update_port_error_470(self): - self._test_update_port(status=470) - - def test_update_port_error_401(self): - self._test_update_port(status=401) - - def test_update_port_error_400(self): - self._test_update_port(status=400) - - def test_update_port_error_420(self): - self._test_update_port(status=420) - - def test_update_port_error_430(self): - self._test_update_port(status=430) - - def test_update_port_error_431(self): - self._test_update_port(status=431) - - def test_show_port_attachment_json(self): - self._test_show_port_attachment(format='json') - - def test_show_port_attachment_xml(self): - self._test_show_port_attachment(format='xml') - - def test_show_port_attachment_alt_tenant(self): - self._test_show_port_attachment(tenant=TENANT_2) - - def test_show_port_attachment_error_470(self): - self._test_show_port_attachment(status=470) - - def test_show_port_attachment_error_401(self): - self._test_show_port_attachment(status=401) - - def test_show_port_attachment_error_400(self): - self._test_show_port_attachment(status=400) - - def test_show_port_attachment_error_420(self): - self._test_show_port_attachment(status=420) - - def test_show_port_attachment_error_430(self): - self._test_show_port_attachment(status=430) - - def test_attach_resource_json(self): - self._test_attach_resource(format='json') - - def test_attach_resource_xml(self): - self._test_attach_resource(format='xml') - - def test_attach_resource_alt_tenant(self): - self._test_attach_resource(tenant=TENANT_2) - - def test_attach_resource_error_470(self): - self._test_attach_resource(status=470) - - def test_attach_resource_error_401(self): - self._test_attach_resource(status=401) - - def test_attach_resource_error_400(self): - self._test_attach_resource(status=400) - - def test_attach_resource_error_420(self): - self._test_attach_resource(status=420) - - def test_attach_resource_error_430(self): - self._test_attach_resource(status=430) - - def test_attach_resource_error_432(self): - self._test_attach_resource(status=432) - - def test_attach_resource_error_440(self): - self._test_attach_resource(status=440) - - def test_detach_resource_json(self): - self._test_detach_resource(format='json') - - def test_detach_resource_xml(self): - self._test_detach_resource(format='xml') - - def test_detach_resource_alt_tenant(self): - self._test_detach_resource(tenant=TENANT_2) - - def test_detach_resource_error_470(self): - self._test_detach_resource(status=470) - - def test_detach_resource_error_401(self): - self._test_detach_resource(status=401) - - def test_detach_resource_error_420(self): - self._test_detach_resource(status=420) - - def test_detach_resource_error_430(self): - self._test_detach_resource(status=430) - - def test_ssl_certificates(self): - self._test_ssl_certificates() - - def test_connection_retry_failure(self): - self.client = Client(port=55555, tenant=TENANT_1, retries=1, - retry_interval=0) - try: - self.client.list_networks() - except exceptions.ConnectionFailed as exc: - self.assertTrue('Maximum attempts reached' in str(exc)) - else: - self.fail('ConnectionFailed not raised') diff --git a/releasenotes/notes/.placeholder b/releasenotes/notes/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/notes/Define-IpAddressAlreadyAllocatedClient-exception-e8600ca5ba1c7f45.yaml b/releasenotes/notes/Define-IpAddressAlreadyAllocatedClient-exception-e8600ca5ba1c7f45.yaml new file mode 100644 index 000000000..4182585e0 --- /dev/null +++ b/releasenotes/notes/Define-IpAddressAlreadyAllocatedClient-exception-e8600ca5ba1c7f45.yaml @@ -0,0 +1,6 @@ +--- +other: + - | + Define a new exception type ``IpAddressAlreadyAllocatedClient``. + Users can catch this specific exception instead of the generic + ``NeutronClientException``. diff --git a/releasenotes/notes/add-aggressive-negotiation-mode-5218b1baff930eb8.yaml b/releasenotes/notes/add-aggressive-negotiation-mode-5218b1baff930eb8.yaml new file mode 100644 index 000000000..ae1b6672f --- /dev/null +++ b/releasenotes/notes/add-aggressive-negotiation-mode-5218b1baff930eb8.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``--phase1-negotiation-mode`` option supports ``aggressive`` mode + in VPNaaS ikepolicy commands. diff --git a/releasenotes/notes/add-auto-allocated-topology-delete-aaccd60bd0f2e7b2.yaml b/releasenotes/notes/add-auto-allocated-topology-delete-aaccd60bd0f2e7b2.yaml new file mode 100644 index 000000000..84c395236 --- /dev/null +++ b/releasenotes/notes/add-auto-allocated-topology-delete-aaccd60bd0f2e7b2.yaml @@ -0,0 +1,4 @@ +--- +features: + - The ``auto-allocated-topology-delete`` command allows users to + delete the auto allocated topology. diff --git a/releasenotes/notes/add-get-me-a-network-5ab2d60bf6f257b1.yaml b/releasenotes/notes/add-get-me-a-network-5ab2d60bf6f257b1.yaml new file mode 100644 index 000000000..51dd37f41 --- /dev/null +++ b/releasenotes/notes/add-get-me-a-network-5ab2d60bf6f257b1.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + CLI support for the "get-me-a-network" feature, which simplifies + the process for launching an instance with basic network + connectivity. + + * The ``auto-allocated-topology-show`` command provides the + network of the automatically allocated topology for a tenant. diff --git a/releasenotes/notes/add-l7-content-policies-capability-0f17cd06f044c83c.yaml b/releasenotes/notes/add-l7-content-policies-capability-0f17cd06f044c83c.yaml new file mode 100644 index 000000000..e413df7ec --- /dev/null +++ b/releasenotes/notes/add-l7-content-policies-capability-0f17cd06f044c83c.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + CLI support for Layer 7 content policies and rules. + + * L7 policies can be defined for listeners along + with the ability to set L7 policy order. + * Multiple rules can be created for an L7 policy. diff --git a/releasenotes/notes/add-lb-status-tree-723f23c09617de3b.yaml b/releasenotes/notes/add-lb-status-tree-723f23c09617de3b.yaml new file mode 100644 index 000000000..8f48d2863 --- /dev/null +++ b/releasenotes/notes/add-lb-status-tree-723f23c09617de3b.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + CLI support for load balancer status tree. + + * The ``lbaas-loadbalancer-status`` command provides the status + tree of a specific load balancer. \ No newline at end of file diff --git a/releasenotes/notes/add-neutron-purge-a89e3d1179dce4b1.yaml b/releasenotes/notes/add-neutron-purge-a89e3d1179dce4b1.yaml new file mode 100644 index 000000000..a689b89ed --- /dev/null +++ b/releasenotes/notes/add-neutron-purge-a89e3d1179dce4b1.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + New command 'neutron purge ' will delete all + supported resources owned by the given tenant, provided + that the user has sufficient authorization and the + resources in question are not shared, in use, or + otherwise undeletable. + + Supported resources are: + * Networks + * Subnets + * Routers + * Ports + * Floating IPs + * Security Groups diff --git a/releasenotes/notes/add-new-session-clear-option-3c0b78ebc133a10c.yaml b/releasenotes/notes/add-new-session-clear-option-3c0b78ebc133a10c.yaml new file mode 100644 index 000000000..2782efe4f --- /dev/null +++ b/releasenotes/notes/add-new-session-clear-option-3c0b78ebc133a10c.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + A new option ``--no-session-persistence`` has been added to + the ``neutron lbaas-pool-update`` CLI to clear the session persistence + with which the current pool is associated. diff --git a/releasenotes/notes/add-no-shared-option-to-qos-policy-update-56ac41fb3af7e309.yaml b/releasenotes/notes/add-no-shared-option-to-qos-policy-update-56ac41fb3af7e309.yaml new file mode 100644 index 000000000..0c34f5eeb --- /dev/null +++ b/releasenotes/notes/add-no-shared-option-to-qos-policy-update-56ac41fb3af7e309.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + CLI support to set QoS policy as not shared if it was shared before. + The ``qos-policy-update`` command include a ``--no-shared`` option. + Closes `bug 1590942 `_. diff --git a/releasenotes/notes/add-osc-dynamic-routing-support-11130b2f440c0ac2.yaml b/releasenotes/notes/add-osc-dynamic-routing-support-11130b2f440c0ac2.yaml new file mode 100644 index 000000000..58cd1ae30 --- /dev/null +++ b/releasenotes/notes/add-osc-dynamic-routing-support-11130b2f440c0ac2.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add OSC plugin to support "Neutron Dynamic Routing" diff --git a/releasenotes/notes/add-osc-trunk-commands-7e77283a369729c5.yaml b/releasenotes/notes/add-osc-trunk-commands-7e77283a369729c5.yaml new file mode 100644 index 000000000..684ae3102 --- /dev/null +++ b/releasenotes/notes/add-osc-trunk-commands-7e77283a369729c5.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add ``network trunk create``, ``network trunk list``, + ``network trunk set``, ``network trunk unset``, ``network trunk delete`` + and ``network subport list`` OSC commands for trunk resource along with + client bindings. + [Blueprint `vlan-aware-vms `_] diff --git a/releasenotes/notes/add-quota-default-show-c2ab35b791dcdcbc.yaml b/releasenotes/notes/add-quota-default-show-c2ab35b791dcdcbc.yaml new file mode 100644 index 000000000..3aececa35 --- /dev/null +++ b/releasenotes/notes/add-quota-default-show-c2ab35b791dcdcbc.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + CLI support to display the default quota reserved for a tenant. + + * The ``quota-default-show`` command outputs the default quota + of resources for a given tenant. diff --git a/releasenotes/notes/add-rbac-qos-type-support-c42e31fadd7b.yaml b/releasenotes/notes/add-rbac-qos-type-support-c42e31fadd7b.yaml new file mode 100644 index 000000000..9dff77c75 --- /dev/null +++ b/releasenotes/notes/add-rbac-qos-type-support-c42e31fadd7b.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + CLI support for QoS policy RBAC. + + * The ``rbac-create`` command include a --type qos-policy + option. + * The ``rbac-list`` command output includes a new 'type' column. diff --git a/releasenotes/notes/add-service-graph-ce4a25b3e32d70a6.yaml b/releasenotes/notes/add-service-graph-ce4a25b3e32d70a6.yaml new file mode 100644 index 000000000..f0b52abe7 --- /dev/null +++ b/releasenotes/notes/add-service-graph-ce4a25b3e32d70a6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for SFC Service Graph resource. + Related RFE: https://bugs.launchpad.net/networking-sfc/+bug/1587486. diff --git a/releasenotes/notes/add-sfc-commands.yaml b/releasenotes/notes/add-sfc-commands.yaml new file mode 100644 index 000000000..c081f50f9 --- /dev/null +++ b/releasenotes/notes/add-sfc-commands.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add OSC plugin support for the “Networking Service Function Chaining” feature commands along with client bindings. + [Blueprint `openstackclient-cli-porting `_] \ No newline at end of file diff --git a/releasenotes/notes/add-shared-pools-support-6f79b565afad3e47.yaml b/releasenotes/notes/add-shared-pools-support-6f79b565afad3e47.yaml new file mode 100644 index 000000000..1be2e0ed1 --- /dev/null +++ b/releasenotes/notes/add-shared-pools-support-6f79b565afad3e47.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + CLI support for Neutron-LBaaS v2 shared pools added. + + * Pools can be created independently from listeners. + * Listeners can share the same default_pool. + * Makes Layer 7 switching support much more useful. diff --git a/releasenotes/notes/add-subnet-onboard-e60772bc4984f698.yaml b/releasenotes/notes/add-subnet-onboard-e60772bc4984f698.yaml new file mode 100644 index 000000000..358fdf006 --- /dev/null +++ b/releasenotes/notes/add-subnet-onboard-e60772bc4984f698.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``network onboard subnets`` OSC command to enable subnet onboard support from the CLI + [Blueprint `subnet-onboard `_] diff --git a/releasenotes/notes/add-support-to-floating-ip-port-forwardings-9dc838a5c5727eb7.yaml b/releasenotes/notes/add-support-to-floating-ip-port-forwardings-9dc838a5c5727eb7.yaml new file mode 100644 index 000000000..3ba168ec9 --- /dev/null +++ b/releasenotes/notes/add-support-to-floating-ip-port-forwardings-9dc838a5c5727eb7.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support to floating ip port forwarding. \ No newline at end of file diff --git a/releasenotes/notes/add-tag-support-bad62d60ecc7075c.yaml b/releasenotes/notes/add-tag-support-bad62d60ecc7075c.yaml new file mode 100644 index 000000000..cc5b6eec2 --- /dev/null +++ b/releasenotes/notes/add-tag-support-bad62d60ecc7075c.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + CLI support for tag. + + * The ``tag-add`` command sets a tag on the network resource. It also + includes ``--resource-type``, ``--resource`` and ``--tag`` options. + * The ``tag-replace`` command replaces tags on the network resource. It + also includes ``--resource-type``, ``--resource`` and ``--tag`` + options. More than one ``--tag`` options can be set. + * The ``tag-remove`` command removes tags on the network resource. It also + includes ``--resource-type``, ``--resource``, ``--tag`` and ``--all`` + options. The ``--all`` option allow to remove all tags on the network + resource. + * The ``net-list`` command includes ``--tags``, ``--tags-any``, + ``--not-tags`` and ``--not-tags-any`` options. \ No newline at end of file diff --git a/releasenotes/notes/availability-zone-support-8e66f55e46b7ef9a.yaml b/releasenotes/notes/availability-zone-support-8e66f55e46b7ef9a.yaml new file mode 100644 index 000000000..5551084f1 --- /dev/null +++ b/releasenotes/notes/availability-zone-support-8e66f55e46b7ef9a.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + CLI support for availability zones. + + * The ``availability-zone-list`` command provides a list of + availability zones. + * The ``net-create`` and ``router-create`` commands include a + ``--availability-zone-hint`` option. + * The ``agent-list`` command output includes availability zones. diff --git a/releasenotes/notes/bgp-dynamic-routing-b97a1c81d3007049.yaml b/releasenotes/notes/bgp-dynamic-routing-b97a1c81d3007049.yaml new file mode 100644 index 000000000..2fa2e8e6e --- /dev/null +++ b/releasenotes/notes/bgp-dynamic-routing-b97a1c81d3007049.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + CLI support for the BGP dynamic routing functionality will help + advertising neutron fixed-ips and dvr host routes via BGP. \ No newline at end of file diff --git a/releasenotes/notes/bug-1676922-81341b70bc6f055a.yaml b/releasenotes/notes/bug-1676922-81341b70bc6f055a.yaml new file mode 100644 index 000000000..9dc6aef41 --- /dev/null +++ b/releasenotes/notes/bug-1676922-81341b70bc6f055a.yaml @@ -0,0 +1,19 @@ +--- +deprecations: + - | + The ``--public`` and ``--private`` attribute of Firewall-as-a-Service v2 + have been deprecated. While the ``--public`` attribute will now be replaced + by ``--share``, the ``--private`` attribute will be replaced by + ``--no-share``. This is because of the similarity between the behavior of + ``--public`` attribute in FireWall-as-a-Service and the ``--share`` + attribute used in OpenStack. This deprecation affects the following CLIs. + + * openstack firewall group create + * openstack firewall group set + * openstack firewall group unset + * openstack firewall policy create + * openstack firewall policy set + * openstack firewall policy unset + * openstack firewall rule create + * openstack firewall rule set + * openstack firewall rule unset diff --git a/releasenotes/notes/bulk-delete-support-94a353db08efec8d.yaml b/releasenotes/notes/bulk-delete-support-94a353db08efec8d.yaml new file mode 100644 index 000000000..4d2575d9d --- /dev/null +++ b/releasenotes/notes/bulk-delete-support-94a353db08efec8d.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + CLI support for bulk delete. + + * By using this feature, multiple resource + can be deleted using a single command. + * Example: ``neutron router-delete router_a router_b`` + deletes both router_a and router_b. diff --git a/releasenotes/notes/default-subnetpool-support-c0d34870e9d3e814.yaml b/releasenotes/notes/default-subnetpool-support-c0d34870e9d3e814.yaml new file mode 100644 index 000000000..2a571d6d8 --- /dev/null +++ b/releasenotes/notes/default-subnetpool-support-c0d34870e9d3e814.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + CLI support for default subnetpools. + + * The ``subnetpool-list`` and ``subnetpool-show`` command output includes + the ``is_default`` field. + * The ``subnetpool-create`` and ``subnetpool-update`` commands include a + ``--is-default`` option. diff --git a/releasenotes/notes/deprecate-bgp-speaker-show-dragents-2fcce99cf6bb5b60.yaml b/releasenotes/notes/deprecate-bgp-speaker-show-dragents-2fcce99cf6bb5b60.yaml new file mode 100644 index 000000000..6233c6f4f --- /dev/null +++ b/releasenotes/notes/deprecate-bgp-speaker-show-dragents-2fcce99cf6bb5b60.yaml @@ -0,0 +1,10 @@ +--- +deprecations: + - | + The ``openstack bgp speaker show dragents`` CLI is deprecated and + will be removed in the future. Use ``openstack bgp dragent list + --bgp-speaker `` CLI instead. +features: + - | + The ``openstack bgp dragent list`` CLI is added to support showing + the list of dynamic routing agents. diff --git a/releasenotes/notes/deprecate-cli-7be1123817969439.yaml b/releasenotes/notes/deprecate-cli-7be1123817969439.yaml new file mode 100644 index 000000000..4833ddda6 --- /dev/null +++ b/releasenotes/notes/deprecate-cli-7be1123817969439.yaml @@ -0,0 +1,8 @@ +--- +deprecations: + - The neutron CLI is now deprecated. This is the signal that it is + time to start using the openstack CLI. No new features will be + added to the neutron CLI, though fixes to the CLI will be assessed + on a case by case basis. Fixes to the API bindings, as well as + development of new API bindings as well as OSC plugins are exempt + from this deprecation. diff --git a/releasenotes/notes/direct-physical-vnic-port-create-736d8b2600faf22b.yaml b/releasenotes/notes/direct-physical-vnic-port-create-736d8b2600faf22b.yaml new file mode 100644 index 000000000..911377496 --- /dev/null +++ b/releasenotes/notes/direct-physical-vnic-port-create-736d8b2600faf22b.yaml @@ -0,0 +1,6 @@ +--- +features: + - Added new 'direct-physical' vnic-type option for port-create CLI command. + Passing this particular value allows for a port to be create with the + vnic-type used for assigning SR-IOV physical functions to + instances. diff --git a/releasenotes/notes/docs-improvements-17e31babe38e2962.yaml b/releasenotes/notes/docs-improvements-17e31babe38e2962.yaml new file mode 100644 index 000000000..f00dc742d --- /dev/null +++ b/releasenotes/notes/docs-improvements-17e31babe38e2962.yaml @@ -0,0 +1,5 @@ +--- +other: + - | + Addition of CLI user documentation including output filters, extra + options, and operation using os-client-config. diff --git a/releasenotes/notes/drop-nuage-commands-df10aab6ccd77ed2.yaml b/releasenotes/notes/drop-nuage-commands-df10aab6ccd77ed2.yaml new file mode 100644 index 000000000..b76f40f05 --- /dev/null +++ b/releasenotes/notes/drop-nuage-commands-df10aab6ccd77ed2.yaml @@ -0,0 +1,3 @@ +--- +upgrade: + - Remove Nuage-specific commands. diff --git a/releasenotes/notes/drop-python-2.7-f615ebae463b2143.yaml b/releasenotes/notes/drop-python-2.7-f615ebae463b2143.yaml new file mode 100644 index 000000000..6fb4d22ab --- /dev/null +++ b/releasenotes/notes/drop-python-2.7-f615ebae463b2143.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Python 2.7 support has been dropped. The minimum version of Python now + supported by python-neutronclient is Python 3.6. diff --git a/releasenotes/notes/drop-python-3-6-and-3-7-73767fa0bbe89a6e.yaml b/releasenotes/notes/drop-python-3-6-and-3-7-73767fa0bbe89a6e.yaml new file mode 100644 index 000000000..db420d739 --- /dev/null +++ b/releasenotes/notes/drop-python-3-6-and-3-7-73767fa0bbe89a6e.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Python 3.6 & 3.7 support has been dropped. The minimum version of Python now + supported is Python 3.8. diff --git a/releasenotes/notes/drop-xml-support-41babecb1784d996.yaml b/releasenotes/notes/drop-xml-support-41babecb1784d996.yaml new file mode 100644 index 000000000..23c95e688 --- /dev/null +++ b/releasenotes/notes/drop-xml-support-41babecb1784d996.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - XML request format support has been removed. +deprecations: + - request-format option is deprecated. diff --git a/releasenotes/notes/dscp_qos-4a26d3c0363624b0.yaml b/releasenotes/notes/dscp_qos-4a26d3c0363624b0.yaml new file mode 100644 index 000000000..37770cd51 --- /dev/null +++ b/releasenotes/notes/dscp_qos-4a26d3c0363624b0.yaml @@ -0,0 +1,6 @@ +--- +prelude: > + Adding new QoS DSCP marking rule commands. +features: + - New create, update, list, show, and delete commands are added for QoS + DSCP marking rule functionality. diff --git a/releasenotes/notes/extraroute-atomic-b11919d8e33b0d92.yaml b/releasenotes/notes/extraroute-atomic-b11919d8e33b0d92.yaml new file mode 100644 index 000000000..fed4a1e10 --- /dev/null +++ b/releasenotes/notes/extraroute-atomic-b11919d8e33b0d92.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + New client methods: ``add_extra_routes_to_router`` and + ``remove_extra_routes_from_router``. diff --git a/releasenotes/notes/fix-exception-typeerror-4.1.0-b37d738146575ed5.yaml b/releasenotes/notes/fix-exception-typeerror-4.1.0-b37d738146575ed5.yaml new file mode 100644 index 000000000..a21eebbeb --- /dev/null +++ b/releasenotes/notes/fix-exception-typeerror-4.1.0-b37d738146575ed5.yaml @@ -0,0 +1,5 @@ +--- +critical: + - Fix a critical bug that when lazy translation is enabled + NeutronClientException raises a TypeError + (`bug 1552760 `_). diff --git a/releasenotes/notes/fix-quota-update-zero-args-d596c4169c2d2e30.yaml b/releasenotes/notes/fix-quota-update-zero-args-d596c4169c2d2e30.yaml new file mode 100644 index 000000000..eb70752a3 --- /dev/null +++ b/releasenotes/notes/fix-quota-update-zero-args-d596c4169c2d2e30.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fix CLI quota-update to return an error message for no args + + * ``quota-update`` CLI will return an error message + ``Must specify a valid resource with new quota value`` if no + argument is provided while executing it. If arguments are + provided with CLI, no existing behavior is changed. diff --git a/releasenotes/notes/fix-rbac-create-command-dd40a474f0f092db.yaml b/releasenotes/notes/fix-rbac-create-command-dd40a474f0f092db.yaml new file mode 100644 index 000000000..7ae438abf --- /dev/null +++ b/releasenotes/notes/fix-rbac-create-command-dd40a474f0f092db.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - Fix 'bug 1596750 ' + that using 'rbac-create' without specifying 'target-tenant' will + return 'Request Failed internal server error while processing your + request'. + Update the default value of the argument '--target-tenant' to '*' diff --git a/releasenotes/notes/fix-token-endpoint-auth-support-26bf7ee12e4ec833.yaml b/releasenotes/notes/fix-token-endpoint-auth-support-26bf7ee12e4ec833.yaml new file mode 100644 index 000000000..9f8b29040 --- /dev/null +++ b/releasenotes/notes/fix-token-endpoint-auth-support-26bf7ee12e4ec833.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - Fix `bug 1450414 `_ + that authentication with via ``--os-token`` and ``--os-url`` options + (or corresponding environment variables) does not work + after keystone v3 API support. diff --git a/releasenotes/notes/global_request_id-56856a93b982a6b3.yaml b/releasenotes/notes/global_request_id-56856a93b982a6b3.yaml new file mode 100644 index 000000000..168987613 --- /dev/null +++ b/releasenotes/notes/global_request_id-56856a93b982a6b3.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds a new ``global_request_id`` parameter to the Client + constructors, which will pass that id on all requests as the + ``X-OpenStack-Request-ID`` header. diff --git a/releasenotes/notes/keystonev3-7f9ede9c21b30841.yaml b/releasenotes/notes/keystonev3-7f9ede9c21b30841.yaml new file mode 100644 index 000000000..507259663 --- /dev/null +++ b/releasenotes/notes/keystonev3-7f9ede9c21b30841.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - | + Keystone v3 support for CLI + + * Using 'tenant_id' and 'tenant_name' arguments in API bindings is + deprecated. Use 'project_id' and 'project_name' arguments instead. diff --git a/releasenotes/notes/log-request-id-64bef955b8292c18.yaml b/releasenotes/notes/log-request-id-64bef955b8292c18.yaml new file mode 100644 index 000000000..62c73fa2e --- /dev/null +++ b/releasenotes/notes/log-request-id-64bef955b8292c18.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added support to log 'x-openstack-request-id' for each api call. \ No newline at end of file diff --git a/releasenotes/notes/minimum-packet-rate-34576b8fd98a3034.yaml b/releasenotes/notes/minimum-packet-rate-34576b8fd98a3034.yaml new file mode 100644 index 000000000..7ce33f65e --- /dev/null +++ b/releasenotes/notes/minimum-packet-rate-34576b8fd98a3034.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added new client methods for QoS minimum packet rate rule: + ``list_minimum_packet_rate_rules``, ``show_minimum_packet_rate_rule``, + ``create_minimum_packet_rate_rule``, ``update_minimum_packet_rate_rule``, + ``delete_minimum_packet_rate_rule``. diff --git a/releasenotes/notes/network-ip-availability-ac9a462f42fe9db4.yaml b/releasenotes/notes/network-ip-availability-ac9a462f42fe9db4.yaml new file mode 100644 index 000000000..7a120fd7e --- /dev/null +++ b/releasenotes/notes/network-ip-availability-ac9a462f42fe9db4.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + CLI support for network IP availability + + * The ``net-ip-availability-list`` command provides a list of IP + usage statistics for all networks. + * The ``net-ip-availability-show`` command provides IP usage stats + for a specific network. diff --git a/releasenotes/notes/neutron-cli-deprecation-398823c87270a296.yaml b/releasenotes/notes/neutron-cli-deprecation-398823c87270a296.yaml new file mode 100644 index 000000000..a8065940e --- /dev/null +++ b/releasenotes/notes/neutron-cli-deprecation-398823c87270a296.yaml @@ -0,0 +1,10 @@ +--- +deprecations: + - | + ``neutron`` CLI will be removed in 'Z' release. + While it has been marked as deprecated for removal for long, + all features in ``neutron`` CLI have been supported in ``openstack`` CLI + (OpenStackClient) as of Xena release and the neutron team plans to + remove it in 'Z' release. Consider using ``openstack`` CLI and + `Mapping Guide `__ + in the OSC documentation would help you. diff --git a/releasenotes/notes/no-new-binding-code-b03c9abbcaf2839e.yaml b/releasenotes/notes/no-new-binding-code-b03c9abbcaf2839e.yaml new file mode 100644 index 000000000..bc41e7438 --- /dev/null +++ b/releasenotes/notes/no-new-binding-code-b03c9abbcaf2839e.yaml @@ -0,0 +1,7 @@ +--- +prelude: > + Openstack community decided to use one SDK for its services, that is + in ``openstacksdk`` repository. To avoid duplication, sooner or later the + python binding code in ``python-neutronclient`` will be deprecated, and + ``Neutron`` team decided on the ``2023.1 (Antelope)`` PTG to not allow + new features\' bindings implemented here. diff --git a/releasenotes/notes/osprofiler-support-9ba539761ae437e9.yaml b/releasenotes/notes/osprofiler-support-9ba539761ae437e9.yaml new file mode 100644 index 000000000..98e2bb168 --- /dev/null +++ b/releasenotes/notes/osprofiler-support-9ba539761ae437e9.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add osprofiler support to the neutronclient python binding. + If osprofiler is initiated, neutronclient sends a special HTTP + header that contains trace info. diff --git a/releasenotes/notes/paket_rate_limit-1266a2a30f18727f.yaml b/releasenotes/notes/paket_rate_limit-1266a2a30f18727f.yaml new file mode 100644 index 000000000..9fdfc1e62 --- /dev/null +++ b/releasenotes/notes/paket_rate_limit-1266a2a30f18727f.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added new client methods for QoS packet rate limit rule: + ``list_packet_rate_limit_rules``, ``show_packet_rate_limit_rule``, + ``create_packet_rate_limit_rule``, ``update_packet_rate_limit_rule``, + ``delete_packet_rate_limit_rule``. diff --git a/releasenotes/notes/port-bindings-c3f36bd76ece0a71.yaml b/releasenotes/notes/port-bindings-c3f36bd76ece0a71.yaml new file mode 100644 index 000000000..7d2ef725e --- /dev/null +++ b/releasenotes/notes/port-bindings-c3f36bd76ece0a71.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + New client methods: ``create_port_binding``, ``delete_port_binding``, + ``show_port_binding``, ``list_port_bindings`` and ``activate_port_binding``. diff --git a/releasenotes/notes/qos_minimum_bandwidth-dc4adb23c51de30b.yaml b/releasenotes/notes/qos_minimum_bandwidth-dc4adb23c51de30b.yaml new file mode 100644 index 000000000..2d1002f07 --- /dev/null +++ b/releasenotes/notes/qos_minimum_bandwidth-dc4adb23c51de30b.yaml @@ -0,0 +1,4 @@ +--- +features: + - New create, update, list, show, and delete commands are added for the QoS + minimum bandwidth rule. diff --git a/releasenotes/notes/quota-update-for-LB-b21e7bc9e4a10f3e.yaml b/releasenotes/notes/quota-update-for-LB-b21e7bc9e4a10f3e.yaml new file mode 100644 index 000000000..9d6321db3 --- /dev/null +++ b/releasenotes/notes/quota-update-for-LB-b21e7bc9e4a10f3e.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Quota of Loadbalancers and listeners can now be updated. diff --git a/releasenotes/notes/quota-update-for-rbac-192a8e65bf481941.yaml b/releasenotes/notes/quota-update-for-rbac-192a8e65bf481941.yaml new file mode 100644 index 000000000..572ce35c2 --- /dev/null +++ b/releasenotes/notes/quota-update-for-rbac-192a8e65bf481941.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Quota for RBAC policies can now be set. diff --git a/releasenotes/notes/relnotes-from-3.0.0-d7306f5af5e3868d.yaml b/releasenotes/notes/relnotes-from-3.0.0-d7306f5af5e3868d.yaml new file mode 100644 index 000000000..2bf4d5ab0 --- /dev/null +++ b/releasenotes/notes/relnotes-from-3.0.0-d7306f5af5e3868d.yaml @@ -0,0 +1,26 @@ +--- +features: + - Support os-client-config. OS_CLOUD environment variable is used for + selecting named cloud configuration. + - Support keystoneauth1 library which brings us better keystone v3 support. + - Client command extension now supports a child resource. + - New CLI for VPNaaS multiple local subnets. + - New CLI for VPNaaS endpoint group API. + - New CLI for flavor argument to loadbalancer v2 create. + - New CLI for Neutron flavor framework. + - Support creating floating IP on a specific subnet ID. + - NSX gateway extension adds new transport type values (ipsec_gre and ipsec_stt). + - New router-update option to update static routes (--route and --no-routes). + - New allowed-address-pairs option to port-update +upgrade: + - Cisco-specific neutron client commands have been removed. + These commands are ported to networking-cisco. + - py26 support has been dropped. + - py33 support has been dropped. +fixes: + - Name is no longer looked up on RBAC policies, + RBAC policies have no name field so the name query to + the server was always returning all entries since the + name filter was ignored. (bug 1517818) +other: + - cliff-tablib has been removed from test dependencies. diff --git a/releasenotes/notes/remote_fwg-0f5362e5be8b2e84.yaml b/releasenotes/notes/remote_fwg-0f5362e5be8b2e84.yaml new file mode 100644 index 000000000..5f5073aee --- /dev/null +++ b/releasenotes/notes/remote_fwg-0f5362e5be8b2e84.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds the remote source firewall group and the remote destination + firewall group field to the firewall rules. diff --git a/releasenotes/notes/remove-bgp-speaker-show-dragents-0a0db4b72b2feffc.yaml b/releasenotes/notes/remove-bgp-speaker-show-dragents-0a0db4b72b2feffc.yaml new file mode 100644 index 000000000..5d4fdda2c --- /dev/null +++ b/releasenotes/notes/remove-bgp-speaker-show-dragents-0a0db4b72b2feffc.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The ``openstack bgp speaker show dragents`` CLI is removed. It was + deprecated in the 7.1.0 release (Ussuri). Use ``openstack bgp dragent list + --bgp-speaker `` CLI instead. diff --git a/releasenotes/notes/remove-case-dependency-773ccb3237c38e81.yaml b/releasenotes/notes/remove-case-dependency-773ccb3237c38e81.yaml new file mode 100644 index 000000000..d32f96b4f --- /dev/null +++ b/releasenotes/notes/remove-case-dependency-773ccb3237c38e81.yaml @@ -0,0 +1,7 @@ +--- +other: + - | + This patch provides user the support to use + any form of casing, thus removing the specific + UPPER/lower case inputs required by different + neutron CLIs. diff --git a/releasenotes/notes/remove-cli-code-53969e9aa927e530.yaml b/releasenotes/notes/remove-cli-code-53969e9aa927e530.yaml new file mode 100644 index 000000000..92ab5423c --- /dev/null +++ b/releasenotes/notes/remove-cli-code-53969e9aa927e530.yaml @@ -0,0 +1,12 @@ +prelude: > + | + This new version of ``python-neutronclient`` does not include the + command line interface code. The "neutron" script is no longer + supported. This project only contains the OpenStack Client bindings + that are currently being moved to OpenStack SDK + _. +deprecations: + - | + This project no longer provides CLI support. All code has been removed. + Please use openstack CLI instead. See `openstack CLI command list + `_. diff --git a/releasenotes/notes/remove-deprecated-option-b53f5d7e6a16ce95.yaml b/releasenotes/notes/remove-deprecated-option-b53f5d7e6a16ce95.yaml new file mode 100644 index 000000000..fb304fbda --- /dev/null +++ b/releasenotes/notes/remove-deprecated-option-b53f5d7e6a16ce95.yaml @@ -0,0 +1,4 @@ +--- +deprecations: + - | + "admin-state-down" option was deprecated in Mitaka and has been removed in Newton. diff --git a/releasenotes/notes/remove-fwaas-osc-plugin-a1b2c3d4e5f6g7h8.yaml b/releasenotes/notes/remove-fwaas-osc-plugin-a1b2c3d4e5f6g7h8.yaml new file mode 100644 index 000000000..171b3eb62 --- /dev/null +++ b/releasenotes/notes/remove-fwaas-osc-plugin-a1b2c3d4e5f6g7h8.yaml @@ -0,0 +1,11 @@ +--- +deprecations: + - | + The Firewall as a Service (FWaaS) v2 OSC plugin commands have been moved + to ``python-openstackclient``. The FWaaS commands are no longer provided + by ``python-neutronclient``. Please use ``python-openstackclient`` for + the following commands: + + * ``firewall group create/delete/list/set/show/unset`` + * ``firewall group policy create/delete/list/set/show/unset/add rule/remove rule`` + * ``firewall group rule create/delete/list/set/show/unset`` diff --git a/releasenotes/notes/remove-public-and-private-parameters-d683e7c30ecedc3b.yaml b/releasenotes/notes/remove-public-and-private-parameters-d683e7c30ecedc3b.yaml new file mode 100644 index 000000000..d340c1763 --- /dev/null +++ b/releasenotes/notes/remove-public-and-private-parameters-d683e7c30ecedc3b.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The deprecated options ``--public`` and ``--private`` were + dropped in FWaaS v2 related commands. Use ``--share`` and + ``--no-share`` instead. diff --git a/releasenotes/notes/remove-py38-26a1befde3f44b82.yaml b/releasenotes/notes/remove-py38-26a1befde3f44b82.yaml new file mode 100644 index 000000000..a0440dc80 --- /dev/null +++ b/releasenotes/notes/remove-py38-26a1befde3f44b82.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Python 3.8 support was dropped. The minimum version of Python now supported + is Python 3.9. diff --git a/releasenotes/notes/return-request-id-to-caller-15b1d23a4ddc27a3.yaml b/releasenotes/notes/return-request-id-to-caller-15b1d23a4ddc27a3.yaml new file mode 100644 index 000000000..da7c5f817 --- /dev/null +++ b/releasenotes/notes/return-request-id-to-caller-15b1d23a4ddc27a3.yaml @@ -0,0 +1,3 @@ +--- +features: + - Neutron client returns 'x-openstack-request-id'. diff --git a/releasenotes/notes/segments-8557f5b0caa5ee26.yaml b/releasenotes/notes/segments-8557f5b0caa5ee26.yaml new file mode 100644 index 000000000..292adf85e --- /dev/null +++ b/releasenotes/notes/segments-8557f5b0caa5ee26.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + New client methods: ``create_segment``, ``update_segment``, + ``list_segments``, ``show_segment`` and ``delete_segment``. diff --git a/releasenotes/notes/sfc-tap-service-function-support-a05242f25f79066b.yaml b/releasenotes/notes/sfc-tap-service-function-support-a05242f25f79066b.yaml new file mode 100644 index 000000000..ccfcc8276 --- /dev/null +++ b/releasenotes/notes/sfc-tap-service-function-support-a05242f25f79066b.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add OSC support to create Port pair group for Tap service functions. diff --git a/releasenotes/notes/show-tenant-id-admin-listing-dc13ee7eb889d418.yaml b/releasenotes/notes/show-tenant-id-admin-listing-dc13ee7eb889d418.yaml new file mode 100644 index 000000000..6b84e1843 --- /dev/null +++ b/releasenotes/notes/show-tenant-id-admin-listing-dc13ee7eb889d418.yaml @@ -0,0 +1,6 @@ +--- +features: + - Show tenant_id when ``*-list`` command is run by admin. In neutron + the list operations by admin retrieve all resources from all tenants. + It is not easy to distinguish resources without tenant_id. + This feature is useful for admin operations. diff --git a/releasenotes/notes/start-using-reno-9081b3e4c1951fdb.yaml b/releasenotes/notes/start-using-reno-9081b3e4c1951fdb.yaml new file mode 100644 index 000000000..873a30fe6 --- /dev/null +++ b/releasenotes/notes/start-using-reno-9081b3e4c1951fdb.yaml @@ -0,0 +1,3 @@ +--- +other: + - Start using reno to manage release notes. diff --git a/releasenotes/notes/support-bgpvpn-route-control-aeda3e698486f73b.yaml b/releasenotes/notes/support-bgpvpn-route-control-aeda3e698486f73b.yaml new file mode 100644 index 000000000..2f7ed1720 --- /dev/null +++ b/releasenotes/notes/support-bgpvpn-route-control-aeda3e698486f73b.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add BGP VPN `port association `_ + support to the CLI, which are introduced for BGP VPN interconnections by the + ``bgpvpn-routes-control`` API extension. diff --git a/releasenotes/notes/support-firewall-group-resource-type-5ad1b69cabcb4aa6.yaml b/releasenotes/notes/support-firewall-group-resource-type-5ad1b69cabcb4aa6.yaml new file mode 100644 index 000000000..78fbff8fd --- /dev/null +++ b/releasenotes/notes/support-firewall-group-resource-type-5ad1b69cabcb4aa6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + CLI support for the "firewal_group" as a loggable resource type for logging + feature, which is enhanced FWaaS functionality, as OSC plugin commands. diff --git a/releasenotes/notes/support-fwaasv2-cli-7f21676c551f8ae0.yaml b/releasenotes/notes/support-fwaasv2-cli-7f21676c551f8ae0.yaml new file mode 100644 index 000000000..2032bfa63 --- /dev/null +++ b/releasenotes/notes/support-fwaasv2-cli-7f21676c551f8ae0.yaml @@ -0,0 +1,4 @@ +--- +features: + - CLI support for the "Firewall as a Service v2" feature, which is enhanced + FWaaS functionality, as OSC plugin commands. diff --git a/releasenotes/notes/support-logging-cli-cd02d3bb03367106.yaml b/releasenotes/notes/support-logging-cli-cd02d3bb03367106.yaml new file mode 100644 index 000000000..415c30cd7 --- /dev/null +++ b/releasenotes/notes/support-logging-cli-cd02d3bb03367106.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + CLI support for 'Logging' feature, which enable to collect packet logs + for specified resource. Currently, only security-group can be logged. diff --git a/releasenotes/notes/support-networking-bgpvpn-cli-fdd0cc3a5b14983d.yaml b/releasenotes/notes/support-networking-bgpvpn-cli-fdd0cc3a5b14983d.yaml new file mode 100644 index 000000000..287e0427c --- /dev/null +++ b/releasenotes/notes/support-networking-bgpvpn-cli-fdd0cc3a5b14983d.yaml @@ -0,0 +1,5 @@ +--- +features: + - CLI support for the "Neutron BGP VPN Interconnection" feature, + which is an API extension to support inter-connection between + L3VPNs/E-VPNs and Neutron resources, as OSC plugin commands. diff --git a/releasenotes/notes/support-routes-advertise-9356a38cf3e2fe5a.yaml b/releasenotes/notes/support-routes-advertise-9356a38cf3e2fe5a.yaml new file mode 100644 index 000000000..f1dbd5ed0 --- /dev/null +++ b/releasenotes/notes/support-routes-advertise-9356a38cf3e2fe5a.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add optional flag to control the advertisement in BGPVPNs + of the routes defined on a Router resource + (``bgpvpn-routes-control`` API extension). diff --git a/releasenotes/notes/support-vni-in-networking-bgpvpn-cli-d284b73b40b79495.yaml b/releasenotes/notes/support-vni-in-networking-bgpvpn-cli-d284b73b40b79495.yaml new file mode 100644 index 000000000..3c3e51c28 --- /dev/null +++ b/releasenotes/notes/support-vni-in-networking-bgpvpn-cli-d284b73b40b79495.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + CLI support for VXLAN VNI ID attribute in bgpvpn. + An optional argument ``--vni`` is added to ``openstack bgpvpn`` + commands to configure VXLAN Network Identifier when VXLAN + encapsulation is used for the bgpvpn. diff --git a/releasenotes/notes/support-vpnaas-cli-9478fb7cfe603e26.yaml b/releasenotes/notes/support-vpnaas-cli-9478fb7cfe603e26.yaml new file mode 100644 index 000000000..3b5e6e491 --- /dev/null +++ b/releasenotes/notes/support-vpnaas-cli-9478fb7cfe603e26.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + CLI support for the "VPN as a Service" feature, which is enhanced + VPNaaS functionality, as OSC plugin commands. diff --git a/releasenotes/notes/tag-support-subnet-port-subnetpool-router-6250ec4714ee8690.yaml b/releasenotes/notes/tag-support-subnet-port-subnetpool-router-6250ec4714ee8690.yaml new file mode 100644 index 000000000..1bc27aeb8 --- /dev/null +++ b/releasenotes/notes/tag-support-subnet-port-subnetpool-router-6250ec4714ee8690.yaml @@ -0,0 +1,4 @@ +--- +features: + - Tag operation for subnet, port, subnetpool and router resources + are now supported. diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst new file mode 100644 index 000000000..2c9a36fae --- /dev/null +++ b/releasenotes/source/2023.1.rst @@ -0,0 +1,6 @@ +=========================== +2023.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: unmaintained/2023.1 diff --git a/releasenotes/source/2023.2.rst b/releasenotes/source/2023.2.rst new file mode 100644 index 000000000..a4838d7d0 --- /dev/null +++ b/releasenotes/source/2023.2.rst @@ -0,0 +1,6 @@ +=========================== +2023.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2023.2 diff --git a/releasenotes/source/2024.1.rst b/releasenotes/source/2024.1.rst new file mode 100644 index 000000000..6896656be --- /dev/null +++ b/releasenotes/source/2024.1.rst @@ -0,0 +1,6 @@ +=========================== +2024.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: unmaintained/2024.1 diff --git a/releasenotes/source/2024.2.rst b/releasenotes/source/2024.2.rst new file mode 100644 index 000000000..aaebcbc8c --- /dev/null +++ b/releasenotes/source/2024.2.rst @@ -0,0 +1,6 @@ +=========================== +2024.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2024.2 diff --git a/releasenotes/source/2025.1.rst b/releasenotes/source/2025.1.rst new file mode 100644 index 000000000..3add0e53a --- /dev/null +++ b/releasenotes/source/2025.1.rst @@ -0,0 +1,6 @@ +=========================== +2025.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.1 diff --git a/releasenotes/source/2025.2.rst b/releasenotes/source/2025.2.rst new file mode 100644 index 000000000..4dae18d86 --- /dev/null +++ b/releasenotes/source/2025.2.rst @@ -0,0 +1,6 @@ +=========================== +2025.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.2 diff --git a/releasenotes/source/2026.1.rst b/releasenotes/source/2026.1.rst new file mode 100644 index 000000000..3d2861580 --- /dev/null +++ b/releasenotes/source/2026.1.rst @@ -0,0 +1,6 @@ +=========================== +2026.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2026.1 diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 000000000..498287605 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# 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. + +# Neutron Client Release Notes documentation build configuration file, +# created by sphinx-quickstart on Tue Nov 3 17:40:50 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'reno.sphinxext', + 'openstackdocstheme', +] + +# openstackdocstheme options +openstackdocs_repo_name = 'openstack/python-neutronclient' +openstackdocs_bug_project = 'python-neutronclient' +openstackdocs_bug_tag = 'doc' +openstackdocs_auto_name = False + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'Neutron Client Release Notes' +copyright = '2015, Neutron Developers' + +# Release notes are version independent. +# The full version, including alpha/beta/rc tags. +release = '' +# The short X.Y version. +version = '' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# 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 + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'openstackdocs' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'NeutronReleaseNotesdoc' + +# -- Options for Internationalization output ------------------------------ +locale_dirs = ['locale/'] diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 000000000..7fe1c4a53 --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,30 @@ +============================== + Neutron Client Release Notes +============================== + +.. toctree:: + :maxdepth: 1 + + unreleased + 2026.1 + 2025.2 + 2025.1 + 2024.2 + 2024.1 + 2023.2 + 2023.1 + zed + yoga + xena + wallaby + victoria + ussuri + train + stein + rocky + queens + pike + ocata + newton + mitaka + old_relnotes diff --git a/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po new file mode 100644 index 000000000..cb1dcb6bd --- /dev/null +++ b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po @@ -0,0 +1,400 @@ +# Andi Chandler , 2022. #zanata +# Andi Chandler , 2023. #zanata +msgid "" +msgstr "" +"Project-Id-Version: Neutron Client Release Notes\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-08 20:18+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2023-07-28 12:44+0000\n" +"Last-Translator: Andi Chandler \n" +"Language-Team: English (United Kingdom)\n" +"Language: en_GB\n" +"X-Generator: Zanata 4.3.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "" +"\"admin-state-down\" option was deprecated in Mitaka and has been removed in " +"Newton." +msgstr "" +"\"admin-state-down\" option was deprecated in Mitaka and has been removed in " +"Newton." + +msgid "10.0.0" +msgstr "10.0.0" + +msgid "2.0" +msgstr "2.0" + +msgid "2.2.0" +msgstr "2.2.0" + +msgid "2.2.2" +msgstr "2.2.2" + +msgid "2023.1 Series Release Notes" +msgstr "2023.1 Series Release Notes" + +msgid "4.0.0" +msgstr "4.0.0" + +msgid "4.1.0" +msgstr "4.1.0" + +msgid "4.1.1" +msgstr "4.1.1" + +msgid "4.1.2-15" +msgstr "4.1.2-15" + +msgid "4.2.0" +msgstr "4.2.0" + +msgid "5.0.0" +msgstr "5.0.0" + +msgid "5.1.0" +msgstr "5.1.0" + +msgid "6.0.0" +msgstr "6.0.0" + +msgid "6.1.0" +msgstr "6.1.0" + +msgid "6.1.1" +msgstr "6.1.1" + +msgid "6.10.0" +msgstr "6.10.0" + +msgid "6.11.0" +msgstr "6.11.0" + +msgid "6.12.0" +msgstr "6.12.0" + +msgid "6.14.0" +msgstr "6.14.0" + +msgid "6.2.0" +msgstr "6.2.0" + +msgid "6.3.0" +msgstr "6.3.0" + +msgid "6.4.0" +msgstr "6.4.0" + +msgid "6.5.0" +msgstr "6.5.0" + +msgid "6.6.0" +msgstr "6.6.0" + +msgid "6.7.0" +msgstr "6.7.0" + +msgid "7.0.0" +msgstr "7.0.0" + +msgid "7.1.0" +msgstr "7.1.0" + +msgid "7.2.0" +msgstr "7.2.0" + +msgid "7.2.1" +msgstr "7.2.1" + +msgid "7.5.0" +msgstr "7.5.0" + +msgid "7.7.0" +msgstr "7.7.0" + +msgid "7.8.0" +msgstr "7.8.0" + +msgid "8.0.0" +msgstr "8.0.0" + +msgid "8.1.0" +msgstr "8.1.0" + +msgid "8.2.0" +msgstr "8.2.0" + +msgid "" +"A new option ``--no-session-persistence`` has been added to the ``neutron " +"lbaas-pool-update`` CLI to clear the session persistence with which the " +"current pool is associated." +msgstr "" +"A new option ``--no-session-persistence`` has been added to the ``neutron " +"lbaas-pool-update`` CLI to clear the session persistence with which the " +"current pool is associated." + +msgid "" +"Add BGP VPN `port association `_ support to the CLI, which are " +"introduced for BGP VPN interconnections by the ``bgpvpn-routes-control`` API " +"extension." +msgstr "" +"Add BGP VPN `port association `_ support to the CLI, which are " +"introduced for BGP VPN interconnections by the ``bgpvpn-routes-control`` API " +"extension." + +msgid "" +"Add OSC plugin support for the “Networking Service Function Chaining” " +"feature commands along with client bindings. [Blueprint `openstackclient-cli-" +"porting `_]" +msgstr "" +"Add OSC plugin support for the “Networking Service Function Chaining” " +"feature commands along with client bindings. [Blueprint `openstackclient-cli-" +"porting `_]" + +msgid "Add OSC plugin to support \"Neutron Dynamic Routing\"" +msgstr "Add OSC plugin to support \"Neutron Dynamic Routing\"" + +msgid "Add OSC support to create Port pair group for Tap service functions." +msgstr "Add OSC support to create Port pair group for Tap service functions." + +msgid "" +"Add ``network onboard subnets`` OSC command to enable subnet onboard support " +"from the CLI [Blueprint `subnet-onboard `_]" +msgstr "" +"Add ``network onboard subnets`` OSC command to enable subnet onboard support " +"from the CLI [Blueprint `subnet-onboard `_]" + +msgid "" +"Add ``network trunk create``, ``network trunk list``, ``network trunk set``, " +"``network trunk unset``, ``network trunk delete`` and ``network subport " +"list`` OSC commands for trunk resource along with client bindings. " +"[Blueprint `vlan-aware-vms `_]" +msgstr "" +"Add ``network trunk create``, ``network trunk list``, ``network trunk set``, " +"``network trunk unset``, ``network trunk delete`` and ``network subport " +"list`` OSC commands for trunk resource along with client bindings. " +"[Blueprint `vlan-aware-vms `_]" + +msgid "" +"Add optional flag to control the advertisement in BGPVPNs of the routes " +"defined on a Router resource (``bgpvpn-routes-control`` API extension)." +msgstr "" +"Add optional flag to control the advertisement in BGPVPNs of the routes " +"defined on a Router resource (``bgpvpn-routes-control`` API extension)." + +msgid "" +"Add osprofiler support to the neutronclient python binding. If osprofiler is " +"initiated, neutronclient sends a special HTTP header that contains trace " +"info." +msgstr "" +"Add osprofiler support to the neutronclient python binding. If osprofiler is " +"initiated, neutronclient sends a special HTTP header that contains trace " +"info." + +msgid "Add support to floating ip port forwarding." +msgstr "Add support to floating ip port forwarding." + +msgid "" +"By using this feature, multiple resource can be deleted using a single " +"command." +msgstr "" +"By using this feature, multiple resource can be deleted using a single " +"command." + +msgid "" +"CLI support for 'Logging' feature, which enable to collect packet logs for " +"specified resource. Currently, only security-group can be logged." +msgstr "" +"CLI support for 'Logging' feature, which enable to collect packet logs for " +"specified resource. Currently, only security-group can be logged." + +msgid "CLI support for Layer 7 content policies and rules." +msgstr "CLI support for Layer 7 content policies and rules." + +msgid "CLI support for Neutron-LBaaS v2 shared pools added." +msgstr "CLI support for Neutron-LBaaS v2 shared pools added." + +msgid "CLI support for QoS policy RBAC." +msgstr "CLI support for QoS policy RBAC." + +msgid "Current Series Release Notes" +msgstr "Current Series Release Notes" + +msgid "Mitaka Series Release Notes" +msgstr "Mitaka Series Release Notes" + +msgid "Neutron Client Release Notes" +msgstr "Neutron Client Release Notes" + +msgid "Newton Series Release Notes" +msgstr "Newton Series Release Notes" + +msgid "Ocata Series Release Notes" +msgstr "Ocata Series Release Notes" + +msgid "Old Release Notes" +msgstr "Old Release Notes" + +msgid "Pike Series Release Notes" +msgstr "Pike Series Release Notes" + +msgid "Queens Series Release Notes" +msgstr "Queens Series Release Notes" + +msgid "Rocky Series Release Notes" +msgstr "Rocky Series Release Notes" + +msgid "Stein Series Release Notes" +msgstr "Stein Series Release Notes" + +msgid "The ``rbac-create`` command include a --type qos-policy option." +msgstr "The ``rbac-create`` command include a --type qos-policy option." + +msgid "The ``rbac-list`` command output includes a new 'type' column." +msgstr "The ``rbac-list`` command output includes a new 'type' column." + +msgid "" +"The ``subnetpool-create`` and ``subnetpool-update`` commands include a ``--" +"is-default`` option." +msgstr "" +"The ``subnetpool-create`` and ``subnetpool-update`` commands include a ``--" +"is-default`` option." + +msgid "" +"The ``subnetpool-list`` and ``subnetpool-show`` command output includes the " +"``is_default`` field." +msgstr "" +"The ``subnetpool-list`` and ``subnetpool-show`` command output includes the " +"``is_default`` field." + +msgid "" +"The ``tag-add`` command sets a tag on the network resource. It also includes " +"``--resource-type``, ``--resource`` and ``--tag`` options." +msgstr "" +"The ``tag-add`` command sets a tag on the network resource. It also includes " +"``--resource-type``, ``--resource`` and ``--tag`` options." + +msgid "" +"This patch provides user the support to use any form of casing, thus " +"removing the specific UPPER/lower case inputs required by different neutron " +"CLIs." +msgstr "" +"This patch provides users the support to use any form of casing, thus " +"removing the specific UPPER/lower case inputs required by different Neutron " +"CLIs." + +msgid "" +"This project no longer provides CLI support. All code has been removed. " +"Please use openstack CLI instead. See `openstack CLI command list `_." +msgstr "" +"This project no longer provides CLI support. All code has been removed. " +"Please use OpenStack CLI instead. See `openstack CLI command list `_." + +msgid "Train Series Release Notes" +msgstr "Train Series Release Notes" + +msgid "Upgrade Notes" +msgstr "Upgrade Notes" + +msgid "" +"Using 'tenant_id' and 'tenant_name' arguments in API bindings is deprecated. " +"Use 'project_id' and 'project_name' arguments instead." +msgstr "" +"Using 'tenant_id' and 'tenant_name' arguments in API bindings is deprecated. " +"Use 'project_id' and 'project_name' arguments instead." + +msgid "Ussuri Series Release Notes" +msgstr "Ussuri Series Release Notes" + +msgid "Victoria Series Release Notes" +msgstr "Victoria Series Release Notes" + +msgid "Wallaby Series Release Notes" +msgstr "Wallaby Series Release Notes" + +msgid "XML request format support has been removed." +msgstr "XML request format support has been removed." + +msgid "Xena Series Release Notes" +msgstr "Xena Series Release Notes" + +msgid "Yoga Series Release Notes" +msgstr "Yoga Series Release Notes" + +msgid "Zed Series Release Notes" +msgstr "Zed Series Release Notes" + +msgid "add --endpoint-type and OS_ENDPOINT_TYPE to shell client" +msgstr "add --endpoint-type and OS_ENDPOINT_TYPE to shell client" + +msgid "add Lbaas commands" +msgstr "add Lbaas commands" + +msgid "add NVP queue and net gateway commands" +msgstr "add NVP queue and net gateway commands" + +msgid "" +"add ability to update security group name (requires 2013.2-Havana or later)" +msgstr "" +"add ability to update security group name (requires 2013.2-Havana or later)" + +msgid "add commands for DHCP and L3 agents scheduling" +msgstr "add commands for DHCP and L3 agents scheduling" + +msgid "add commands for agent management extensions" +msgstr "add commands for agent management extensions" + +msgid "add flake8 and pbr support for testing and building" +msgstr "add flake8 and pbr support for testing and building" + +msgid "add security group commands" +msgstr "add security group commands" + +msgid "allow options put after positional arguments" +msgstr "allow options put after positional arguments" + +msgid "improved support for listing a large number of filtered subnets" +msgstr "improved support for listing a large number of filtered subnets" + +msgid "made the publicURL the default endpoint instead of adminURL" +msgstr "made the publicURL the default endpoint instead of adminURL" + +msgid "openstack firewall rule create" +msgstr "openstack firewall rule create" + +msgid "openstack firewall rule set" +msgstr "openstack firewall rule set" + +msgid "openstack firewall rule unset" +msgstr "openstack firewall rule unset" + +msgid "py26 support has been dropped." +msgstr "py26 support has been dropped." + +msgid "py33 support has been dropped." +msgstr "py33 support has been dropped." + +msgid "request-format option is deprecated." +msgstr "request-format option is deprecated." + +msgid "support Neutron API 2.0" +msgstr "support Neutron API 2.0" + +msgid "support XML request format" +msgstr "support XML request format" + +msgid "support pagination options" +msgstr "support pagination options" diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst new file mode 100644 index 000000000..e54560965 --- /dev/null +++ b/releasenotes/source/mitaka.rst @@ -0,0 +1,6 @@ +=================================== + Mitaka Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/mitaka diff --git a/releasenotes/source/newton.rst b/releasenotes/source/newton.rst new file mode 100644 index 000000000..97036ed25 --- /dev/null +++ b/releasenotes/source/newton.rst @@ -0,0 +1,6 @@ +=================================== + Newton Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/newton diff --git a/releasenotes/source/ocata.rst b/releasenotes/source/ocata.rst new file mode 100644 index 000000000..ebe62f42e --- /dev/null +++ b/releasenotes/source/ocata.rst @@ -0,0 +1,6 @@ +=================================== + Ocata Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/ocata diff --git a/releasenotes/source/old_relnotes.rst b/releasenotes/source/old_relnotes.rst new file mode 100644 index 000000000..6a571a7b4 --- /dev/null +++ b/releasenotes/source/old_relnotes.rst @@ -0,0 +1,29 @@ +================= +Old Release Notes +================= + +2.2.2 +----- + +* improved support for listing a large number of filtered subnets +* add --endpoint-type and OS_ENDPOINT_TYPE to shell client +* made the publicURL the default endpoint instead of adminURL +* add ability to update security group name (requires 2013.2-Havana or later) +* add flake8 and pbr support for testing and building + +2.2.0 +----- + +* add security group commands +* add Lbaas commands +* allow options put after positional arguments +* add NVP queue and net gateway commands +* add commands for agent management extensions +* add commands for DHCP and L3 agents scheduling +* support XML request format +* support pagination options + +2.0 +----- + +* support Neutron API 2.0 diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst new file mode 100644 index 000000000..e43bfc0ce --- /dev/null +++ b/releasenotes/source/pike.rst @@ -0,0 +1,6 @@ +=================================== + Pike Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/pike diff --git a/releasenotes/source/queens.rst b/releasenotes/source/queens.rst new file mode 100644 index 000000000..36ac6160c --- /dev/null +++ b/releasenotes/source/queens.rst @@ -0,0 +1,6 @@ +=================================== + Queens Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/queens diff --git a/releasenotes/source/rocky.rst b/releasenotes/source/rocky.rst new file mode 100644 index 000000000..40dd517b7 --- /dev/null +++ b/releasenotes/source/rocky.rst @@ -0,0 +1,6 @@ +=================================== + Rocky Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/rocky diff --git a/releasenotes/source/stein.rst b/releasenotes/source/stein.rst new file mode 100644 index 000000000..efaceb667 --- /dev/null +++ b/releasenotes/source/stein.rst @@ -0,0 +1,6 @@ +=================================== + Stein Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/stein diff --git a/releasenotes/source/train.rst b/releasenotes/source/train.rst new file mode 100644 index 000000000..583900393 --- /dev/null +++ b/releasenotes/source/train.rst @@ -0,0 +1,6 @@ +========================== +Train Series Release Notes +========================== + +.. release-notes:: + :branch: stable/train diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 000000000..cd22aabcc --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================== + Current Series Release Notes +============================== + +.. release-notes:: diff --git a/releasenotes/source/ussuri.rst b/releasenotes/source/ussuri.rst new file mode 100644 index 000000000..e21e50e0c --- /dev/null +++ b/releasenotes/source/ussuri.rst @@ -0,0 +1,6 @@ +=========================== +Ussuri Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/ussuri diff --git a/releasenotes/source/victoria.rst b/releasenotes/source/victoria.rst new file mode 100644 index 000000000..8ce933419 --- /dev/null +++ b/releasenotes/source/victoria.rst @@ -0,0 +1,6 @@ +============================= +Victoria Series Release Notes +============================= + +.. release-notes:: + :branch: unmaintained/victoria diff --git a/releasenotes/source/wallaby.rst b/releasenotes/source/wallaby.rst new file mode 100644 index 000000000..bcf35c5f8 --- /dev/null +++ b/releasenotes/source/wallaby.rst @@ -0,0 +1,6 @@ +============================ +Wallaby Series Release Notes +============================ + +.. release-notes:: + :branch: unmaintained/wallaby diff --git a/releasenotes/source/xena.rst b/releasenotes/source/xena.rst new file mode 100644 index 000000000..d19eda488 --- /dev/null +++ b/releasenotes/source/xena.rst @@ -0,0 +1,6 @@ +========================= +Xena Series Release Notes +========================= + +.. release-notes:: + :branch: unmaintained/xena diff --git a/releasenotes/source/yoga.rst b/releasenotes/source/yoga.rst new file mode 100644 index 000000000..43cafdea8 --- /dev/null +++ b/releasenotes/source/yoga.rst @@ -0,0 +1,6 @@ +========================= +Yoga Series Release Notes +========================= + +.. release-notes:: + :branch: unmaintained/yoga diff --git a/releasenotes/source/zed.rst b/releasenotes/source/zed.rst new file mode 100644 index 000000000..6cc2b1554 --- /dev/null +++ b/releasenotes/source/zed.rst @@ -0,0 +1,6 @@ +======================== +Zed Series Release Notes +======================== + +.. release-notes:: + :branch: unmaintained/zed diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..41ca9d7ef --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +# Requirements lower bounds listed here are our best effort to keep them up to +# date but we do not test them so no guarantee of having them all correct. If +# you find any incorrect lower bounds, let us know or propose a fix. +pbr!=2.1.0,>=2.0.0 # Apache-2.0 +cliff>=3.4.0 # Apache-2.0 +debtcollector>=1.2.0 # Apache-2.0 +netaddr>=0.7.18 # BSD +openstacksdk>=1.5.0 # Apache-2.0 +osc-lib>=1.12.0 # Apache-2.0 +oslo.i18n>=3.15.3 # Apache-2.0 +oslo.log>=3.36.0 # Apache-2.0 +oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 +oslo.utils>=3.33.0 # Apache-2.0 +keystoneauth1>=3.8.0 # Apache-2.0 +# keystoneclient is used only by neutronclient.osc.utils +# TODO(amotoki): Drop this after osc.utils has no dependency on keystoneclient +python-keystoneclient>=3.8.0 # Apache-2.0 +requests>=2.14.2 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 48000a777..a995bae67 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,120 @@ -[nosetests] -# NOTE(jkoelker) To run the test suite under nose install the following -# coverage http://pypi.python.org/pypi/coverage -# tissue http://pypi.python.org/pypi/tissue (pep8 checker) -# openstack-nose https://github.com/jkoelker/openstack-nose -verbosity=2 -detailed-errors=1 +[metadata] +name = python-neutronclient +summary = CLI and Client Library for OpenStack Networking +description_file = + README.rst +author = OpenStack Networking Project +author_email = openstack-discuss@lists.openstack.org +home_page = https://docs.openstack.org/python-neutronclient/latest/ +python_requires = >=3.9 +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 :: Implementation :: CPython + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + +[files] +packages = + neutronclient + +[entry_points] +openstack.cli.extension = + neutronclient = neutronclient.osc.plugin + +openstack.neutronclient.v2 = + sfc_flow_classifier_create = neutronclient.osc.v2.sfc.sfc_flow_classifier:CreateSfcFlowClassifier + sfc_flow_classifier_delete = neutronclient.osc.v2.sfc.sfc_flow_classifier:DeleteSfcFlowClassifier + sfc_flow_classifier_list = neutronclient.osc.v2.sfc.sfc_flow_classifier:ListSfcFlowClassifier + sfc_flow_classifier_set = neutronclient.osc.v2.sfc.sfc_flow_classifier:SetSfcFlowClassifier + sfc_flow_classifier_show = neutronclient.osc.v2.sfc.sfc_flow_classifier:ShowSfcFlowClassifier + sfc_port_chain_create = neutronclient.osc.v2.sfc.sfc_port_chain:CreateSfcPortChain + sfc_port_chain_delete = neutronclient.osc.v2.sfc.sfc_port_chain:DeleteSfcPortChain + sfc_port_chain_list = neutronclient.osc.v2.sfc.sfc_port_chain:ListSfcPortChain + sfc_port_chain_set = neutronclient.osc.v2.sfc.sfc_port_chain:SetSfcPortChain + sfc_port_chain_show = neutronclient.osc.v2.sfc.sfc_port_chain:ShowSfcPortChain + sfc_port_chain_unset = neutronclient.osc.v2.sfc.sfc_port_chain:UnsetSfcPortChain + sfc_port_pair_create = neutronclient.osc.v2.sfc.sfc_port_pair:CreateSfcPortPair + sfc_port_pair_delete = neutronclient.osc.v2.sfc.sfc_port_pair:DeleteSfcPortPair + sfc_port_pair_list = neutronclient.osc.v2.sfc.sfc_port_pair:ListSfcPortPair + sfc_port_pair_set = neutronclient.osc.v2.sfc.sfc_port_pair:SetSfcPortPair + sfc_port_pair_show = neutronclient.osc.v2.sfc.sfc_port_pair:ShowSfcPortPair + sfc_port_pair_group_create = neutronclient.osc.v2.sfc.sfc_port_pair_group:CreateSfcPortPairGroup + sfc_port_pair_group_delete = neutronclient.osc.v2.sfc.sfc_port_pair_group:DeleteSfcPortPairGroup + sfc_port_pair_group_list = neutronclient.osc.v2.sfc.sfc_port_pair_group:ListSfcPortPairGroup + sfc_port_pair_group_set = neutronclient.osc.v2.sfc.sfc_port_pair_group:SetSfcPortPairGroup + sfc_port_pair_group_show = neutronclient.osc.v2.sfc.sfc_port_pair_group:ShowSfcPortPairGroup + sfc_port_pair_group_unset = neutronclient.osc.v2.sfc.sfc_port_pair_group:UnsetSfcPortPairGroup + sfc_service_graph_create = neutronclient.osc.v2.sfc.sfc_service_graph:CreateSfcServiceGraph + sfc_service_graph_delete = neutronclient.osc.v2.sfc.sfc_service_graph:DeleteSfcServiceGraph + sfc_service_graph_set = neutronclient.osc.v2.sfc.sfc_service_graph:SetSfcServiceGraph + sfc_service_graph_list = neutronclient.osc.v2.sfc.sfc_service_graph:ListSfcServiceGraph + sfc_service_graph_show = neutronclient.osc.v2.sfc.sfc_service_graph:ShowSfcServiceGraph + + bgp_dragent_add_speaker = neutronclient.osc.v2.dynamic_routing.bgp_dragent:AddBgpSpeakerToDRAgent + bgp_dragent_list = neutronclient.osc.v2.dynamic_routing.bgp_dragent:ListDRAgent + bgp_dragent_remove_speaker = neutronclient.osc.v2.dynamic_routing.bgp_dragent:RemoveBgpSpeakerFromDRAgent + bgp_peer_create = neutronclient.osc.v2.dynamic_routing.bgp_peer:CreateBgpPeer + bgp_peer_delete = neutronclient.osc.v2.dynamic_routing.bgp_peer:DeleteBgpPeer + bgp_peer_list = neutronclient.osc.v2.dynamic_routing.bgp_peer:ListBgpPeer + bgp_peer_show = neutronclient.osc.v2.dynamic_routing.bgp_peer:ShowBgpPeer + bgp_peer_set = neutronclient.osc.v2.dynamic_routing.bgp_peer:SetBgpPeer + bgp_speaker_list_advertised_routes = neutronclient.osc.v2.dynamic_routing.bgp_speaker:ListRoutesAdvertisedBySpeaker + bgp_speaker_create = neutronclient.osc.v2.dynamic_routing.bgp_speaker:CreateBgpSpeaker + bgp_speaker_delete = neutronclient.osc.v2.dynamic_routing.bgp_speaker:DeleteBgpSpeaker + bgp_speaker_list = neutronclient.osc.v2.dynamic_routing.bgp_speaker:ListBgpSpeaker + bgp_speaker_add_network = neutronclient.osc.v2.dynamic_routing.bgp_speaker:AddNetworkToSpeaker + bgp_speaker_remove_network = neutronclient.osc.v2.dynamic_routing.bgp_speaker:RemoveNetworkFromSpeaker + bgp_speaker_add_peer = neutronclient.osc.v2.dynamic_routing.bgp_speaker:AddPeerToSpeaker + bgp_speaker_remove_peer = neutronclient.osc.v2.dynamic_routing.bgp_speaker:RemovePeerFromSpeaker + bgp_speaker_set = neutronclient.osc.v2.dynamic_routing.bgp_speaker:SetBgpSpeaker + bgp_speaker_show = neutronclient.osc.v2.dynamic_routing.bgp_speaker:ShowBgpSpeaker + + + network_loggable_resources_list = neutronclient.osc.v2.logging.network_log:ListLoggableResource + network_log_create = neutronclient.osc.v2.logging.network_log:CreateNetworkLog + network_log_delete = neutronclient.osc.v2.logging.network_log:DeleteNetworkLog + network_log_list = neutronclient.osc.v2.logging.network_log:ListNetworkLog + network_log_set = neutronclient.osc.v2.logging.network_log:SetNetworkLog + network_log_show = neutronclient.osc.v2.logging.network_log:ShowNetworkLog + + vpn_endpoint_group_create = neutronclient.osc.v2.vpnaas.endpoint_group:CreateEndpointGroup + vpn_endpoint_group_delete = neutronclient.osc.v2.vpnaas.endpoint_group:DeleteEndpointGroup + vpn_endpoint_group_list = neutronclient.osc.v2.vpnaas.endpoint_group:ListEndpointGroup + vpn_endpoint_group_set = neutronclient.osc.v2.vpnaas.endpoint_group:SetEndpointGroup + vpn_endpoint_group_show = neutronclient.osc.v2.vpnaas.endpoint_group:ShowEndpointGroup + + vpn_ike_policy_create = neutronclient.osc.v2.vpnaas.ikepolicy:CreateIKEPolicy + vpn_ike_policy_delete = neutronclient.osc.v2.vpnaas.ikepolicy:DeleteIKEPolicy + vpn_ike_policy_list = neutronclient.osc.v2.vpnaas.ikepolicy:ListIKEPolicy + vpn_ike_policy_set = neutronclient.osc.v2.vpnaas.ikepolicy:SetIKEPolicy + vpn_ike_policy_show = neutronclient.osc.v2.vpnaas.ikepolicy:ShowIKEPolicy + + vpn_ipsec_policy_create = neutronclient.osc.v2.vpnaas.ipsecpolicy:CreateIPsecPolicy + vpn_ipsec_policy_delete = neutronclient.osc.v2.vpnaas.ipsecpolicy:DeleteIPsecPolicy + vpn_ipsec_policy_list = neutronclient.osc.v2.vpnaas.ipsecpolicy:ListIPsecPolicy + vpn_ipsec_policy_set = neutronclient.osc.v2.vpnaas.ipsecpolicy:SetIPsecPolicy + vpn_ipsec_policy_show = neutronclient.osc.v2.vpnaas.ipsecpolicy:ShowIPsecPolicy + + vpn_service_create = neutronclient.osc.v2.vpnaas.vpnservice:CreateVPNService + vpn_service_delete = neutronclient.osc.v2.vpnaas.vpnservice:DeleteVPNService + vpn_service_list = neutronclient.osc.v2.vpnaas.vpnservice:ListVPNService + vpn_service_set = neutronclient.osc.v2.vpnaas.vpnservice:SetVPNSercice + vpn_service_show = neutronclient.osc.v2.vpnaas.vpnservice:ShowVPNService + + vpn_ipsec_site_connection_create = neutronclient.osc.v2.vpnaas.ipsec_site_connection:CreateIPsecSiteConnection + vpn_ipsec_site_connection_delete = neutronclient.osc.v2.vpnaas.ipsec_site_connection:DeleteIPsecSiteConnection + vpn_ipsec_site_connection_list = neutronclient.osc.v2.vpnaas.ipsec_site_connection:ListIPsecSiteConnection + vpn_ipsec_site_connection_set = neutronclient.osc.v2.vpnaas.ipsec_site_connection:SetIPsecSiteConnection + vpn_ipsec_site_connection_show = neutronclient.osc.v2.vpnaas.ipsec_site_connection:ShowIPsecSiteConnection + + network_onboard_subnets = neutronclient.osc.v2.subnet_onboard.subnet_onboard:NetworkOnboardSubnets diff --git a/setup.py b/setup.py index 087e1546b..cd35c3c35 100644 --- a/setup.py +++ b/setup.py @@ -1,71 +1,20 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Citrix Systems -# All Rights Reserved. +# 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 +# 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 +# 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: - from setuptools import setup, find_packages -except ImportError: - from ez_setup import use_setuptools - use_setuptools() - from setuptools import setup, find_packages - -import version - -Name = 'python-quantumclient' -Url = "https://launchpad.net/quantum" -Version = version.canonical_version_string() -License = 'Apache License 2.0' -Author = 'Netstack' -AuthorEmail = 'netstack@lists.launchpad.net' -Maintainer = '' -Summary = 'Client functionalities for Quantum' -ShortDescription = Summary -Description = Summary - -requires = [ -] - -EagerResources = [ -] - -ProjectScripts = [ -] - -PackageData = { -} - - -setup( - name=Name, - version=Version, - url=Url, - author=Author, - author_email=AuthorEmail, - description=ShortDescription, - long_description=Description, - license=License, - scripts=ProjectScripts, - install_requires=requires, - include_package_data=False, - packages=["quantumclient", "quantumclient.common"], - package_data=PackageData, - eager_resources=EagerResources, - entry_points={ - 'console_scripts': [ - 'quantum = quantumclient.cli:main' - ] - }, -) +# 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 setuptools + +setuptools.setup( + setup_requires=['pbr>=2.0.0'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..d6a160c26 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,12 @@ +hacking>=6.1.0,<6.2.0 # Apache-2.0 + +bandit!=1.6.0,>=1.1.0 # Apache-2.0 +coverage!=4.4,>=4.0 # Apache-2.0 +fixtures>=3.0.0 # Apache-2.0/BSD +flake8-import-order>=0.19.0,<0.20.0 # LGPLv3 +oslotest>=3.2.0 # Apache-2.0 +osprofiler>=2.3.0 # Apache-2.0 +python-openstackclient>=3.12.0 # Apache-2.0 +requests-mock>=1.2.0 # Apache-2.0 +stestr>=2.0.0 # Apache-2.0 +testtools>=2.2.0 # MIT diff --git a/tools/test-requires b/tools/test-requires deleted file mode 100644 index a048ab095..000000000 --- a/tools/test-requires +++ /dev/null @@ -1,11 +0,0 @@ -distribute>=0.6.24 - -mox -nose -nose-exclude -nosexcover -openstack.nose_plugin -pep8==0.6.1 -sphinx>=1.1.2 - --e git+https://review.openstack.org/p/openstack/quantum#egg=quantum-dev diff --git a/tox.ini b/tox.ini index a5feeffaa..a939f8b65 100644 --- a/tox.ini +++ b/tox.ini @@ -1,57 +1,94 @@ [tox] -envlist = py26,py27,pep8 +envlist = py39,pep8 +minversion = 3.18.0 +skipsdist = False +ignore_basepython_conflict = True [testenv] -setenv = - VIRTUAL_ENV={envdir} - NOSE_WITH_OPENSTACK=1 - NOSE_OPENSTACK_COLOR=1 - NOSE_OPENSTACK_RED=0.05 - NOSE_OPENSTACK_YELLOW=0.025 - NOSE_OPENSTACK_SHOW_ELAPSED=1 -deps = - -r{toxinidir}/tools/test-requires -commands = nosetests +basepython = python3 +setenv = VIRTUAL_ENV={envdir} + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C + PYTHONWARNINGS=default::DeprecationWarning +usedevelop = True +deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +# Delete bytecodes from normal directories before running tests. +# Note that bytecodes in dot directories will not be deleted +# to keep bytecodes of python modules installed into virtualenvs. +commands = bash -c "find . -type d -name '.?*' -prune -o \ + \( -type d -name '__pycache__' -o -type f -name '*.py[co]' \) \ + -print0 | xargs -0 rm -rf" + stestr run {posargs} +allowlist_externals = bash [testenv:pep8] commands = - pep8 --exclude=vcsversion.py,*.pyc --repeat --show-source quantumclient \ - setup.py version.py + flake8 + {[testenv:bandit]commands} +distribute = false [testenv:venv] commands = {posargs} [testenv:cover] +setenv = + {[testenv]setenv} + PYTHON=coverage run --source neutronclient --parallel-mode commands = - nosetests --with-coverage --cover-html --cover-erase \ - --cover-package=quantumclient + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report -[tox:jenkins] -downloadcache = ~/cache/pip +[testenv:docs] +deps = + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt +commands = sphinx-build -W -b html doc/source doc/build/html -[testenv:jenkins26] -basepython = python2.6 -setenv = NOSE_WITH_XUNIT=1 -deps = file://{toxinidir}/.cache.bundle +[testenv:pdf-docs] +deps = {[testenv:docs]deps} +allowlist_externals = + make +commands = + sphinx-build -W -b latex doc/source doc/build/pdf + make -C doc/build/pdf -[testenv:jenkins27] -basepython = python2.7 -setenv = NOSE_WITH_XUNIT=1 -deps = file://{toxinidir}/.cache.bundle +[testenv:releasenotes] +deps = + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt +commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html -[testenv:jenkinspep8] -deps = file://{toxinidir}/.cache.bundle -commands = - pep8 --exclude=vcsversion.py,*.pyc --repeat --show-source quantumclient \ - setup.py version.py +[flake8] +# E126 continuation line over-indented for hanging indent +# E128 continuation line under-indented for visual indent +# H405 multi line docstring summary not separated with an empty line +# I202 Additional newline in a group of imports +# N530 direct neutron imports not allowed +# TODO(amotoki) check the following new rules should be fixed or ignored +# E731 do not assign a lambda expression, use a def +# W504 line break after binary operator +ignore = E126,E128,E731,I202,H405,N530,W504 +show-source = true +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools +import-order-style = pep8 -[testenv:jenkinscover] -deps = file://{toxinidir}/.cache.bundle -setenv = NOSE_WITH_XUNIT=1 -commands = - nosetests --cover-erase --cover-package=quantumclient --with-xcoverage +# H106: Don't put vim configuration in source files +# H203: Use assertIs(Not)None to check for None +# H204: Use assert(Not)Equal to check for equality +# H205: Use assert(Greater|Less)(Equal) for comparison +# H904: Delay string interpolations at logging calls +enable-extensions=H106,H203,H204,H205,H904 -[testenv:jenkinsvenv] -deps = file://{toxinidir}/.cache.bundle -setenv = NOSE_WITH_XUNIT=1 -commands = {posargs} +[testenv:bandit] +# B303: blacklist calls: md5, sha1 +# B105: The software contains a hard-coded password, which it uses for its own +# inbound authentication or for outbound communication to external +# components. +deps = -r{toxinidir}/test-requirements.txt +commands = bandit -r neutronclient -x tests -n5 -s B303,B105 diff --git a/version.py b/version.py deleted file mode 100644 index fae32acd3..000000000 --- a/version.py +++ /dev/null @@ -1,46 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -try: - from quantumclient.vcsversion import version_info -except ImportError: - version_info = {'branch_nick': u'LOCALBRANCH', - 'revision_id': 'LOCALREVISION', - 'revno': 0} - -QUANTUM_VERSION = ['2012', '2', None] -YEAR, COUNT, REVSISION = QUANTUM_VERSION - -FINAL = False # This becomes true at Release Candidate time - - -def canonical_version_string(): - return '.'.join(filter(None, QUANTUM_VERSION)) - - -def version_string(): - if FINAL: - return canonical_version_string() - else: - return '%s-dev' % (canonical_version_string(),) - - -def vcs_version_string(): - return "%s:%s" % (version_info['branch_nick'], version_info['revision_id']) - - -def version_string_with_vcs(): - return "%s-%s" % (canonical_version_string(), vcs_version_string())