diff --git a/.coveragerc b/.coveragerc index cd84ef96b..94e40afbb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] branch = True -source = quantumclient -omit = quantumclient/openstack/* +source = neutronclient +omit = neutronclient/tests/* [report] -ignore-errors = True +ignore_errors = True diff --git a/.gitignore b/.gitignore index 1a5761972..abdf7b022 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +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/ -.tox/ .autogenerated -doc/build/ -doc/source/api/ -quantumclient/versioninfo -ChangeLog +.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/.testr.conf b/.testr.conf deleted file mode 100644 index d356fcf1e..000000000 --- a/.testr.conf +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./quantumclient/tests $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list 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 c7643238e..7230971af 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -1,191 +1,11 @@ -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 +- Step 1: Read the OpenStack Style Commandments + https://docs.openstack.org/hacking/latest/ +- Step 2: 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. - Running Tests ------------- The testing system is based on a combination of tox and testr. The canonical diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 41d72419d..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include tox.ini -include LICENSE README HACKING.rst -include ChangeLog -include tools/* -include quantumclient/tests/* -include quantumclient/tests/unit/* -include quantumclient/versioninfo 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/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_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 index fba29db68..70fd0de2f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,16 +1,22 @@ # -*- coding: utf-8 -*- # -import sys -import os - -project = 'python-quantumclient' - # -- General configuration --------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] +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'] @@ -22,7 +28,7 @@ master_doc = 'index' # General information about the project. -copyright = u'OpenStack LLC' +copyright = 'OpenStack Foundation' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True @@ -38,21 +44,30 @@ # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'nature' +html_theme = 'openstackdocs' # Output file base name for HTML help builder. -htmlhelp_basename = '%sdoc' % project +htmlhelp_basename = 'neutronclientdoc' +# -- Options for LaTeX output ------------------------------------------------ -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, -# documentclass [howto/manual]). latex_documents = [ - ('index', - '%s.tex' % project, - u'%s Documentation' % project, - u'OpenStack LLC', 'manual'), + ('index', 'doc-python-neutronclient.tex', + 'python-neutronclient Documentation', + 'Neutron Contributors', 'manual'), ] -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +# 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 index 1dd7caefc..c36d482ed 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,44 +1,65 @@ -Python bindings to the OpenStack Network API -============================================ +.. + 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 -In order to use the python quantum client directly, you must first obtain an auth token and identify which endpoint you wish to speak to. Once you have done so, you can use the API like so:: + http://www.apache.org/licenses/LICENSE-2.0 - >>> import logging - >>> from quantumclient.quantum import client - >>> logging.basicConfig(level=logging.DEBUG) - >>> quantum = client.Client('2.0', endpoint_url=OS_URL, token=OS_TOKEN) - >>> quantum.format = 'json' - >>> network = {'name': 'mynetwork', 'admin_state_up': True} - >>> quantum.create_network({'network':network}) - >>> networks = quantum.list_networks(name='mynetwork') - >>> print networks - >>> network_id = networks['networks'][0]['id'] - >>> quantum.delete_network(network_id) + 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.) -Command-line Tool -================= -In order to use the CLI, you must provide your OpenStack username, password, tenant, and auth endpoint. Use the corresponding configuration options (``--os-username``, ``--os-password``, ``--os-tenant-name``, and ``--os-auth-url``) or set them in environment variables:: +================================== +python-neutronclient documentation +================================== - export OS_USERNAME=user - export OS_PASSWORD=pass - export OS_TENANT_NAME=tenant - export OS_AUTH_URL=http://auth.example.com:5000/v2.0 +This is a client for OpenStack Networking API. It provides +:doc:`Python API bindings ` (the neutronclient module). -The command line tool will attempt to reauthenticate using your provided credentials for every request. You can override this behavior by manually supplying an auth token using ``--os-url`` and ``--os-auth-token``. You can alternatively set these environment variables:: +There is +`OpenStack Client (OSC) `__. +CLI which support the Networking API. - export OS_URL=http://quantum.example.org:9696/ - export OS_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 +User Documentation +------------------ -If quantum server does not require authentication, besides these two arguments or environment variables (We can use any value as token.), we need manually supply ``--os-auth-strategy`` or set the environment variable:: +.. toctree:: + :maxdepth: 2 - export OS_AUTH_STRATEGY=noauth + cli/index + reference/index -Once you've configured your authentication parameters, you can run ``quantum -h`` to see a complete listing of available commands. +Contributor Guide +----------------- -Release Notes -============= +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. -2.0 ------ -* support Quantum API 2.0 +.. 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/openstack/__init__.py b/neutronclient/__init__.py similarity index 100% rename from quantumclient/openstack/__init__.py rename to neutronclient/__init__.py diff --git a/quantumclient/__init__.py b/neutronclient/_i18n.py similarity index 56% rename from quantumclient/__init__.py rename to neutronclient/_i18n.py index 034d66eaf..50ac3c5bf 100644 --- a/quantumclient/__init__.py +++ b/neutronclient/_i18n.py @@ -1,8 +1,3 @@ -# 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 @@ -14,4 +9,23 @@ # 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 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/openstack/common/__init__.py b/neutronclient/common/__init__.py similarity index 100% rename from quantumclient/openstack/common/__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/quantumclient/common/constants.py b/neutronclient/common/constants.py similarity index 75% rename from quantumclient/common/constants.py rename to neutronclient/common/constants.py index 572baa941..ccb22cd80 100644 --- a/quantumclient/common/constants.py +++ b/neutronclient/common/constants.py @@ -1,4 +1,4 @@ -# Copyright (c) 2012 OpenStack, LLC. +# 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. @@ -14,15 +14,6 @@ # limitations under the License. -EXT_NS = '_extension_ns' -XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0' -XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance" -XSI_ATTR = "xsi:nil" -XSI_NIL_ATTR = "xmlns:xsi" -TYPE_XMLNS = "xmlns:quantum" -TYPE_ATTR = "quantum:type" -VIRTUAL_ROOT_KEY = "_v_root" - TYPE_BOOL = "bool" TYPE_INT = "int" TYPE_LONG = "long" @@ -33,6 +24,7 @@ PLURALS = {'networks': 'network', 'ports': 'port', 'subnets': 'subnet', + 'subnetpools': 'subnetpool', 'dns_nameservers': 'dns_nameserver', 'host_routes': 'host_route', 'allocation_pools': 'allocation_pool', 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/quantumclient/tests/__init__.py b/neutronclient/neutron/__init__.py similarity index 100% rename from quantumclient/tests/__init__.py rename to neutronclient/neutron/__init__.py 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/quantumclient/tests/unit/__init__.py b/neutronclient/neutron/v2_0/servicetype.py similarity index 63% rename from quantumclient/tests/unit/__init__.py rename to neutronclient/neutron/v2_0/servicetype.py index 353303fb4..ec8454f94 100644 --- a/quantumclient/tests/unit/__init__.py +++ b/neutronclient/neutron/v2_0/servicetype.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright 2013 OpenStack Foundation. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,10 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. # -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -import gettext +from neutronclient.neutron import v2_0 as neutronV20 -# Because we installed '_' for quantum cli in shell.py, this help unittest -# have definition of '_' -gettext.install('quantumclient', unicode=1) + +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/fwaas/__init__.py b/neutronclient/osc/v2/fwaas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/quantumclient/quantum/v2_0/lb/__init__.py b/neutronclient/osc/v2/fwaas/constants.py similarity index 62% rename from quantumclient/quantum/v2_0/lb/__init__.py rename to neutronclient/osc/v2/fwaas/constants.py index 1668497e7..a5a74af73 100644 --- a/quantumclient/quantum/v2_0/lb/__init__.py +++ b/neutronclient/osc/v2/fwaas/constants.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright 2016-2017 FUJITSU LIMITED # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,4 +13,16 @@ # License for the specific language governing permissions and limitations # under the License. # -# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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/quantumclient/common/__init__.py b/neutronclient/osc/v2/utils.py similarity index 65% rename from quantumclient/common/__init__.py rename to neutronclient/osc/v2/utils.py index 1415c50a3..b03e29c3d 100644 --- a/quantumclient/common/__init__.py +++ b/neutronclient/osc/v2/utils.py @@ -1,6 +1,4 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 Nicira Networks, Inc. -# All Rights Reserved. +# 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 @@ -13,12 +11,14 @@ # 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. -import gettext +"""This module is intended to contain methods specific +to Networking v2 API and its extensions. +""" -t = gettext.translation('quantumclient', fallback=True) +from cliff import columns as cliff_columns -def _(msg): - return t.ugettext(msg) +class AdminStateColumn(cliff_columns.FormattableColumn): + def human_readable(self): + return 'UP' if self._value else 'DOWN' 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/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/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/quantumclient/tests/unit/lb/__init__.py b/neutronclient/version.py similarity index 75% rename from quantumclient/tests/unit/lb/__init__.py rename to neutronclient/version.py index 1668497e7..d597b5c91 100644 --- a/quantumclient/tests/unit/lb/__init__.py +++ b/neutronclient/version.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack LLC. +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,4 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. # -# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import pbr.version + + +version_info = pbr.version.VersionInfo('python-neutronclient') +__version__ = version_info.version_string() diff --git a/openstack-common.conf b/openstack-common.conf deleted file mode 100644 index d52611484..000000000 --- a/openstack-common.conf +++ /dev/null @@ -1,7 +0,0 @@ -[DEFAULT] - -# The list of modules to copy from openstack-common -modules=jsonutils,setup,timeutils - -# The base module to hold the copy of openstack.common -base=quantumclient diff --git a/quantum_test.sh b/quantum_test.sh deleted file mode 100755 index b42299beb..000000000 --- a/quantum_test.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/bin/bash -set -x -function die() { - local exitcode=$? - set +o xtrace - echo $@ - exit $exitcode -} - -noauth_tenant_id=me -if [ $1 == 'noauth' ]; then - NOAUTH="--tenant_id $noauth_tenant_id" -else - NOAUTH= -fi - -FORMAT=" --request-format xml" - -# test the CRUD of network -network=mynet1 -quantum net-create $FORMAT $NOAUTH $network || die "fail to create network $network" -temp=`quantum net-list $FORMAT -- --name $network --fields id | wc -l` -echo $temp -if [ $temp -ne 5 ]; then - die "networks with name $network is not unique or found" -fi -network_id=`quantum net-list -- --name $network --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` -echo "ID of network with name $network is $network_id" - -quantum net-show $FORMAT $network || die "fail to show network $network" -quantum net-show $FORMAT $network_id || die "fail to show network $network_id" - -quantum net-update $FORMAT $network --admin_state_up False || die "fail to update network $network" -quantum net-update $FORMAT $network_id --admin_state_up True || die "fail to update network $network_id" - -quantum net-list $FORMAT -c id -- --id fakeid || die "fail to list networks with column selection on empty list" - -# test the CRUD of subnet -subnet=mysubnet1 -cidr=10.0.1.3/24 -quantum subnet-create $FORMAT $NOAUTH $network $cidr --name $subnet || die "fail to create subnet $subnet" -tempsubnet=`quantum subnet-list $FORMAT -- --name $subnet --fields id | wc -l` -echo $tempsubnet -if [ $tempsubnet -ne 5 ]; then - die "subnets with name $subnet is not unique or found" -fi -subnet_id=`quantum subnet-list $FORMAT -- --name $subnet --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` -echo "ID of subnet with name $subnet is $subnet_id" -quantum subnet-show $FORMAT $subnet || die "fail to show subnet $subnet" -quantum subnet-show $FORMAT $subnet_id || die "fail to show subnet $subnet_id" - -quantum subnet-update $FORMAT $subnet --dns_namesevers host1 || die "fail to update subnet $subnet" -quantum subnet-update $FORMAT $subnet_id --dns_namesevers host2 || die "fail to update subnet $subnet_id" - -# test the crud of ports -port=myport1 -quantum port-create $FORMAT $NOAUTH $network --name $port || die "fail to create port $port" -tempport=`quantum port-list $FORMAT -- --name $port --fields id | wc -l` -echo $tempport -if [ $tempport -ne 5 ]; then - die "ports with name $port is not unique or found" -fi -port_id=`quantum port-list $FORMAT -- --name $port --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` -echo "ID of port with name $port is $port_id" -quantum port-show $FORMAT $port || die "fail to show port $port" -quantum port-show $FORMAT $port_id || die "fail to show port $port_id" - -quantum port-update $FORMAT $port --device_id deviceid1 || die "fail to update port $port" -quantum port-update $FORMAT $port_id --device_id deviceid2 || die "fail to update port $port_id" - -# test quota commands RUD -DEFAULT_NETWORKS=10 -DEFAULT_PORTS=50 -tenant_id=tenant_a -tenant_id_b=tenant_b -quantum quota-update $FORMAT --tenant_id $tenant_id --network 30 || die "fail to update quota for tenant $tenant_id" -quantum quota-update $FORMAT --tenant_id $tenant_id_b --network 20 || die "fail to update quota for tenant $tenant_id" -networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id | awk '{print $2}'` -if [ $networks -ne 30 ]; then - die "networks quota should be 30" -fi -networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id_b | awk '{print $2}'` -if [ $networks -ne 20 ]; then - die "networks quota should be 20" -fi -networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` -if [ $networks -ne 30 ]; then - die "networks quota should be 30" -fi -quantum quota-delete $FORMAT --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id" -networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'` -if [ $networks -ne $DEFAULT_NETWORKS ]; then - die "networks quota should be $DEFAULT_NETWORKS" -fi -# update self -if [ "t$NOAUTH" = "t" ]; then - # with auth - quantum quota-update $FORMAT --port 99 || die "fail to update quota for self" - ports=`quantum quota-show $FORMAT | grep port | awk -F'|' '{print $3}'` - if [ $ports -ne 99 ]; then - die "ports quota should be 99" - fi - - ports=`quantum quota-list $FORMAT -c port | grep 99 | awk '{print $2}'` - if [ $ports -ne 99 ]; then - die "ports quota should be 99" - fi - quantum quota-delete $FORMAT || die "fail to delete quota for tenant self" - ports=`quantum quota-show $FORMAT | grep port | awk -F'|' '{print $3}'` - if [ $ports -ne $DEFAULT_PORTS ]; then - die "ports quota should be $DEFAULT_PORTS" - fi -else - # without auth - quantum quota-update $FORMAT --port 100 - if [ $? -eq 0 ]; then - die "without valid context on server, quota update command should fail." - fi - quantum quota-show $FORMAT - if [ $? -eq 0 ]; then - die "without valid context on server, quota show command should fail." - fi - quantum quota-delete $FORMAT - if [ $? -eq 0 ]; then - die "without valid context on server, quota delete command should fail." - fi - quantum quota-list $FORMAT || die "fail to update quota for self" -fi diff --git a/quantumclient/client.py b/quantumclient/client.py deleted file mode 100644 index 39e770e79..000000000 --- a/quantumclient/client.py +++ /dev/null @@ -1,216 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -try: - import json -except ImportError: - import simplejson as json -import logging -import os -import urlparse -# Python 2.5 compat fix -if not hasattr(urlparse, 'parse_qsl'): - import cgi - urlparse.parse_qsl = cgi.parse_qsl - -import httplib2 - -from quantumclient.common import exceptions -from quantumclient.common import utils - -_logger = logging.getLogger(__name__) - -if 'QUANTUMCLIENT_DEBUG' in os.environ and os.environ['QUANTUMCLIENT_DEBUG']: - ch = logging.StreamHandler() - _logger.setLevel(logging.DEBUG) - _logger.addHandler(ch) - - -class ServiceCatalog(object): - """Helper methods for dealing with a Keystone Service Catalog.""" - - def __init__(self, resource_dict): - self.catalog = resource_dict - - def get_token(self): - """Fetch token details fron service catalog""" - token = {'id': self.catalog['access']['token']['id'], - 'expires': self.catalog['access']['token']['expires'], } - try: - token['user_id'] = self.catalog['access']['user']['id'] - token['tenant_id'] = ( - self.catalog['access']['token']['tenant']['id']) - except: - # just leave the tenant and user out if it doesn't exist - pass - return token - - def url_for(self, attr=None, filter_value=None, - service_type='network', endpoint_type='adminURL'): - """Fetch the admin URL from the Quantum service for - a particular endpoint attribute. If none given, return - the first. See tests for sample service catalog.""" - - catalog = self.catalog['access'].get('serviceCatalog', []) - matching_endpoints = [] - for service in catalog: - if service['type'] != service_type: - continue - - endpoints = service['endpoints'] - for endpoint in endpoints: - if not filter_value or endpoint.get(attr) == filter_value: - matching_endpoints.append(endpoint) - - if not matching_endpoints: - raise exceptions.EndpointNotFound() - elif len(matching_endpoints) > 1: - raise exceptions.AmbiguousEndpoints(message=matching_endpoints) - else: - return matching_endpoints[0][endpoint_type] - - -class HTTPClient(httplib2.Http): - """Handles the REST calls and responses, include authn""" - - USER_AGENT = 'python-quantumclient' - - def __init__(self, username=None, tenant_name=None, - password=None, auth_url=None, - token=None, region_name=None, timeout=None, - endpoint_url=None, insecure=False, - auth_strategy='keystone', **kwargs): - super(HTTPClient, self).__init__(timeout=timeout) - self.username = username - self.tenant_name = tenant_name - self.password = password - self.auth_url = auth_url.rstrip('/') if auth_url else None - self.region_name = region_name - self.auth_token = token - self.token_retrieved = False - self.content_type = 'application/json' - self.endpoint_url = endpoint_url - self.auth_strategy = auth_strategy - # httplib2 overrides - self.force_exception_to_status_code = True - self.disable_ssl_certificate_validation = insecure - - def _cs_request(self, *args, **kwargs): - kargs = {} - kargs.setdefault('headers', kwargs.get('headers', {})) - kargs['headers']['User-Agent'] = self.USER_AGENT - - if 'content_type' in kwargs: - kargs['headers']['Content-Type'] = kwargs['content_type'] - kargs['headers']['Accept'] = kwargs['content_type'] - else: - kargs['headers']['Content-Type'] = self.content_type - kargs['headers']['Accept'] = self.content_type - - if 'body' in kwargs: - kargs['body'] = kwargs['body'] - utils.http_log_req(_logger, args, kargs) - resp, body = self.request(*args, **kargs) - utils.http_log_resp(_logger, resp, body) - status_code = self.get_status_code(resp) - if status_code == 401: - raise exceptions.Unauthorized(message=body) - elif status_code == 403: - raise exceptions.Forbidden(message=body) - return resp, body - - def do_request(self, url, method, **kwargs): - if not self.endpoint_url: - self.authenticate() - - # Perform the request once. If we get a 401 back then it - # might be because the auth token expired, so try to - # re-authenticate and try again. If it still fails, bail. - try: - if self.auth_token: - kwargs.setdefault('headers', {}) - kwargs['headers']['X-Auth-Token'] = self.auth_token - resp, body = self._cs_request(self.endpoint_url + url, method, - **kwargs) - return resp, body - except exceptions.Unauthorized as ex: - if not self.endpoint_url or self.token_retrieved: - self.authenticate() - if self.auth_token: - kwargs.setdefault('headers', {}) - kwargs['headers']['X-Auth-Token'] = self.auth_token - resp, body = self._cs_request( - self.endpoint_url + url, method, **kwargs) - return resp, body - else: - raise ex - - def _extract_service_catalog(self, body): - """ Set the client's service catalog from the response data. """ - self.service_catalog = ServiceCatalog(body) - try: - sc = self.service_catalog.get_token() - self.auth_token = sc['id'] - self.auth_tenant_id = sc.get('tenant_id') - self.auth_user_id = sc.get('user_id') - self.token_retrieved = True - except KeyError: - raise exceptions.Unauthorized() - self.endpoint_url = self.service_catalog.url_for( - attr='region', filter_value=self.region_name, - endpoint_type='adminURL') - - def authenticate(self): - if self.auth_strategy != 'keystone': - raise exceptions.Unauthorized(message='unknown auth strategy') - body = {'auth': {'passwordCredentials': - {'username': self.username, - 'password': self.password, }, - 'tenantName': self.tenant_name, }, } - - token_url = self.auth_url + "/tokens" - - # Make sure we follow redirects when trying to reach Keystone - tmp_follow_all_redirects = self.follow_all_redirects - self.follow_all_redirects = True - try: - resp, body = self._cs_request(token_url, "POST", - body=json.dumps(body), - content_type="application/json") - finally: - self.follow_all_redirects = tmp_follow_all_redirects - status_code = self.get_status_code(resp) - if status_code != 200: - raise exceptions.Unauthorized(message=body) - if body: - try: - body = json.loads(body) - except ValueError: - pass - else: - body = None - self._extract_service_catalog(body) - - 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 diff --git a/quantumclient/common/clientmanager.py b/quantumclient/common/clientmanager.py deleted file mode 100644 index 7346aa541..000000000 --- a/quantumclient/common/clientmanager.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -"""Manage access to the clients, including authenticating when needed. -""" - -import logging - -from quantumclient.client import HTTPClient -from quantumclient.common import exceptions as exc -from quantumclient.quantum import client as quantum_client - - -LOG = logging.getLogger(__name__) - - -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. - """ - quantum = ClientCache(quantum_client.make_client) - - def __init__(self, token=None, url=None, - auth_url=None, - tenant_name=None, tenant_id=None, - username=None, password=None, - region_name=None, - api_version=None, - auth_strategy=None - ): - self._token = token - self._url = url - self._auth_url = auth_url - self._tenant_name = tenant_name - self._tenant_id = tenant_id - self._username = username - self._password = password - self._region_name = region_name - self._api_version = api_version - self._service_catalog = None - self._auth_strategy = auth_strategy - return - - def initialize(self): - if not self._url: - httpclient = HTTPClient(username=self._username, - tenant_name=self._tenant_name, - password=self._password, - region_name=self._region_name, - auth_url=self._auth_url) - httpclient.authenticate() - # Populate other password flow attributes - self._token = httpclient.auth_token - self._url = httpclient.endpoint_url diff --git a/quantumclient/common/command.py b/quantumclient/common/command.py deleted file mode 100644 index f2706d019..000000000 --- a/quantumclient/common/command.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -""" -OpenStack base command -""" - -from cliff.command import Command - - -class OpenStackCommand(Command): - """Base class for OpenStack commands - """ - - api = None - - def run(self, parsed_args): - if not self.api: - return - else: - return super(OpenStackCommand, self).run(parsed_args) - - def get_data(self, parsed_args): - pass - - def take_action(self, parsed_args): - return self.get_data(parsed_args) diff --git a/quantumclient/common/exceptions.py b/quantumclient/common/exceptions.py deleted file mode 100644 index 1df133d7b..000000000 --- a/quantumclient/common/exceptions.py +++ /dev/null @@ -1,159 +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. - -from quantumclient.common import _ - -""" -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') - self.status_code = kwargs.get('status_code', 0) - 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 Unauthorized(QuantumClientException): - """ - HTTP 401 - Unauthorized: bad credentials. - """ - message = _("Unauthorized: bad credentials.") - - -class Forbidden(QuantumClientException): - """ - HTTP 403 - Forbidden: your credentials don't give you access to this - resource. - """ - message = _("Forbidden: your credentials don't give you access to this " - "resource.") - - -class EndpointNotFound(QuantumClientException): - """Could not find Service or Region in Service Catalog.""" - message = _("Could not find Service or Region in Service Catalog.") - - -class AmbiguousEndpoints(QuantumClientException): - """Found more than one matching endpoint in Service Catalog.""" - - def __str__(self): - return "AmbiguousEndpoints: %s" % repr(self.message) - - -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.") - - -class UnsupportedVersion(Exception): - """Indicates that the user is trying to use an unsupported - version of the API""" - pass - - -class CommandError(Exception): - pass diff --git a/quantumclient/common/serializer.py b/quantumclient/common/serializer.py deleted file mode 100644 index 7eba93069..000000000 --- a/quantumclient/common/serializer.py +++ /dev/null @@ -1,400 +0,0 @@ -# Copyright 2013 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -### -### Codes from quantum wsgi -### - -import logging - -from xml.etree import ElementTree as etree -from xml.parsers import expat - -from quantumclient.common import constants -from quantumclient.common import exceptions as exception -from quantumclient.openstack.common import jsonutils - -LOG = logging.getLogger(__name__) - - -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): - return jsonutils.dumps(data) - - -class XMLDictSerializer(DictSerializer): - - def __init__(self, metadata=None, xmlns=None): - """ - :param metadata: information needed to deserialize xml into - a dictionary. - :param xmlns: XML namespace to include with serialized xml - """ - super(XMLDictSerializer, self).__init__() - self.metadata = metadata or {} - if not xmlns: - xmlns = self.metadata.get('xmlns') - if not xmlns: - xmlns = constants.XML_NS_V20 - self.xmlns = xmlns - - def default(self, data): - # We expect data to contain a single key which is the XML root or - # non root - try: - key_len = data and len(data.keys()) or 0 - if (key_len == 1): - root_key = data.keys()[0] - root_value = data[root_key] - else: - root_key = constants.VIRTUAL_ROOT_KEY - root_value = data - doc = etree.Element("_temp_root") - used_prefixes = [] - self._to_xml_node(doc, self.metadata, root_key, - root_value, used_prefixes) - return self.to_xml_string(list(doc)[0], used_prefixes) - except AttributeError as e: - LOG.exception(str(e)) - return '' - - def __call__(self, data): - # Provides a migration path to a cleaner WSGI layer, this - # "default" stuff and extreme extensibility isn't being used - # like originally intended - return self.default(data) - - def to_xml_string(self, node, used_prefixes, has_atom=False): - self._add_xmlns(node, used_prefixes, has_atom) - return etree.tostring(node, encoding='UTF-8') - - #NOTE (ameade): the has_atom should be removed after all of the - # xml serializers and view builders have been updated to the current - # spec that required all responses include the xmlns:atom, the has_atom - # flag is to prevent current tests from breaking - def _add_xmlns(self, node, used_prefixes, has_atom=False): - node.set('xmlns', self.xmlns) - node.set(constants.TYPE_XMLNS, self.xmlns) - if has_atom: - node.set('xmlns:atom', "http://www.w3.org/2005/Atom") - node.set(constants.XSI_NIL_ATTR, constants.XSI_NAMESPACE) - ext_ns = self.metadata.get(constants.EXT_NS, {}) - for prefix in used_prefixes: - if prefix in ext_ns: - node.set('xmlns:' + prefix, ext_ns[prefix]) - - def _to_xml_node(self, parent, metadata, nodename, data, used_prefixes): - """Recursive method to convert data members to XML nodes.""" - result = etree.SubElement(parent, nodename) - if ":" in nodename: - used_prefixes.append(nodename.split(":", 1)[0]) - #TODO(bcwaldon): accomplish this without a type-check - if isinstance(data, list): - if not data: - result.set( - constants.TYPE_ATTR, - constants.TYPE_LIST) - return result - singular = metadata.get('plurals', {}).get(nodename, None) - if singular is None: - if nodename.endswith('s'): - singular = nodename[:-1] - else: - singular = 'item' - for item in data: - self._to_xml_node(result, metadata, singular, item, - used_prefixes) - #TODO(bcwaldon): accomplish this without a type-check - elif isinstance(data, dict): - if not data: - result.set( - constants.TYPE_ATTR, - constants.TYPE_DICT) - return result - attrs = metadata.get('attributes', {}).get(nodename, {}) - for k, v in data.items(): - if k in attrs: - result.set(k, str(v)) - else: - self._to_xml_node(result, metadata, k, v, - used_prefixes) - elif data is None: - result.set(constants.XSI_ATTR, 'true') - else: - if isinstance(data, bool): - result.set( - constants.TYPE_ATTR, - constants.TYPE_BOOL) - elif isinstance(data, int): - result.set( - constants.TYPE_ATTR, - constants.TYPE_INT) - elif isinstance(data, long): - result.set( - constants.TYPE_ATTR, - constants.TYPE_LONG) - elif isinstance(data, float): - result.set( - constants.TYPE_ATTR, - constants.TYPE_FLOAT) - LOG.debug(_("Data %(data)s type is %(type)s"), - {'data': data, - 'type': type(data)}) - result.text = str(data) - return result - - def _create_link_nodes(self, xml_doc, links): - link_nodes = [] - for link in links: - link_node = xml_doc.createElement('atom:link') - link_node.set('rel', link['rel']) - link_node.set('href', link['href']) - if 'type' in link: - link_node.set('type', link['type']) - link_nodes.append(link_node) - return link_nodes - - -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.MalformedRequestBody(reason=msg) - - def default(self, datastring): - return {'body': self._from_json(datastring)} - - -class XMLDeserializer(TextDeserializer): - - def __init__(self, metadata=None): - """ - :param metadata: information needed to deserialize xml into - a dictionary. - """ - super(XMLDeserializer, self).__init__() - self.metadata = metadata or {} - xmlns = self.metadata.get('xmlns') - if not xmlns: - xmlns = constants.XML_NS_V20 - self.xmlns = xmlns - - def _get_key(self, tag): - tags = tag.split("}", 1) - if len(tags) == 2: - ns = tags[0][1:] - bare_tag = tags[1] - ext_ns = self.metadata.get(constants.EXT_NS, {}) - if ns == self.xmlns: - return bare_tag - for prefix, _ns in ext_ns.items(): - if ns == _ns: - return prefix + ":" + bare_tag - else: - return tag - - def _from_xml(self, datastring): - if datastring is None: - return None - plurals = set(self.metadata.get('plurals', {})) - try: - node = etree.fromstring(datastring) - result = self._from_xml_node(node, plurals) - root_tag = self._get_key(node.tag) - if root_tag == constants.VIRTUAL_ROOT_KEY: - return result - else: - return {root_tag: result} - except Exception as e: - parseError = False - # Python2.7 - if (hasattr(etree, 'ParseError') and - isinstance(e, getattr(etree, 'ParseError'))): - parseError = True - # Python2.6 - elif isinstance(e, expat.ExpatError): - parseError = True - if parseError: - msg = _("Cannot understand XML") - raise exception.MalformedRequestBody(reason=msg) - else: - raise - - def _from_xml_node(self, node, listnames): - """Convert a minidom node to a simple Python type. - - :param listnames: list of XML node names whose subnodes should - be considered list items. - - """ - attrNil = node.get(str(etree.QName(constants.XSI_NAMESPACE, "nil"))) - attrType = node.get(str(etree.QName( - self.metadata.get('xmlns'), "type"))) - if (attrNil and attrNil.lower() == 'true'): - return None - elif not len(node) and not node.text: - if (attrType and attrType == constants.TYPE_DICT): - return {} - elif (attrType and attrType == constants.TYPE_LIST): - return [] - else: - return '' - elif (len(node) == 0 and node.text): - converters = {constants.TYPE_BOOL: - lambda x: x.lower() == 'true', - constants.TYPE_INT: - lambda x: int(x), - constants.TYPE_LONG: - lambda x: long(x), - constants.TYPE_FLOAT: - lambda x: float(x)} - if attrType and attrType in converters: - return converters[attrType](node.text) - else: - return node.text - elif self._get_key(node.tag) in listnames: - return [self._from_xml_node(n, listnames) for n in node] - else: - result = dict() - for attr in node.keys(): - if (attr == 'xmlns' or - attr.startswith('xmlns:') or - attr == constants.XSI_ATTR or - attr == constants.TYPE_ATTR): - continue - result[self._get_key(attr)] = node.get[attr] - children = list(node) - for child in children: - result[self._get_key(child.tag)] = self._from_xml_node( - child, listnames) - return result - - def find_first_child_named(self, parent, name): - """Search a nodes children for the first child with a given name""" - for node in parent.childNodes: - if node.nodeName == name: - return node - return None - - def find_children_named(self, parent, name): - """Return all of a nodes children who have the given name""" - for node in parent.childNodes: - if node.nodeName == name: - yield node - - def extract_text(self, node): - """Get the text field contained by the given node""" - if len(node.childNodes) == 1: - child = node.childNodes[0] - if child.nodeType == child.TEXT_NODE: - return child.nodeValue - return "" - - def default(self, datastring): - return {'body': self._from_xml(datastring)} - - def __call__(self, datastring): - # Adding a migration path to allow us to remove unncessary classes - return self.default(datastring) - - -# NOTE(maru): this class is duplicated from 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': JSONDictSerializer(), - 'application/xml': XMLDictSerializer(self.metadata), - } - - try: - return handlers[content_type] - except Exception: - raise exception.InvalidContentType(content_type=content_type) - - def serialize(self, data, content_type): - """Serialize a dictionary into the specified content type.""" - return self._get_serialize_handler(content_type).serialize(data) - - def deserialize(self, datastring, content_type): - """Deserialize a string to a dictionary. - - The string must be in the format of a supported MIME type. - - """ - return self.get_deserialize_handler(content_type).deserialize( - datastring) - - def get_deserialize_handler(self, content_type): - handlers = { - 'application/json': JSONDeserializer(), - 'application/xml': XMLDeserializer(self.metadata), - } - - try: - return handlers[content_type] - except Exception: - raise exception.InvalidContentType(content_type=content_type) diff --git a/quantumclient/common/utils.py b/quantumclient/common/utils.py deleted file mode 100644 index 45c64a998..000000000 --- a/quantumclient/common/utils.py +++ /dev/null @@ -1,174 +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 logging -import os -import sys - -from quantumclient.common import exceptions - - -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 isinstance(value, list) or isinstance(value, tuple): - o = [] - for v in value: - o.append(to_primitive(v)) - return o - elif isinstance(value, dict): - 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, indent=None): - try: - return json.dumps(value, indent=indent) - except TypeError: - pass - return json.dumps(to_primitive(value)) - - -def loads(s): - return json.loads(s) - - -def import_class(import_str): - """Returns a class from a string including module and class - - :param import_str: a string representation of the class name - :rtype: the requested class - """ - mod_str, _sep, class_str = import_str.rpartition('.') - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - - -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 %s client version '%s'. must be one of: %s" % ( - (api_name, version, ', '.join(version_map.keys()))) - raise exceptions.UnsupportedVersion(msg) - - return import_class(client_path) - - -def get_item_properties(item, fields, mixed_case_fields=[], formatters={}): - """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 - """ - 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 - else: - return strbool.lower() == 'true' - - -def str2dict(strdict): - '''@param strdict: key1=value1,key2=value2''' - _info = {} - for kv_str in strdict.split(","): - k, v = kv_str.split("=", 1) - _info.update({k: v}) - return _info - - -def http_log_req(_logger, args, kwargs): - if not _logger.isEnabledFor(logging.DEBUG): - return - - string_parts = ['curl -i'] - for element in args: - if element in ('GET', 'POST', 'DELETE', 'PUT'): - string_parts.append(' -X %s' % element) - else: - string_parts.append(' %s' % element) - - for element in kwargs['headers']: - header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) - string_parts.append(header) - - if 'body' in kwargs and kwargs['body']: - string_parts.append(" -d '%s'" % (kwargs['body'])) - _logger.debug("\nREQ: %s\n" % "".join(string_parts)) - - -def http_log_resp(_logger, resp, body): - if not _logger.isEnabledFor(logging.DEBUG): - return - _logger.debug("RESP:%s %s\n", resp, body) diff --git a/quantumclient/openstack/common/jsonutils.py b/quantumclient/openstack/common/jsonutils.py deleted file mode 100644 index ad76e0655..000000000 --- a/quantumclient/openstack/common/jsonutils.py +++ /dev/null @@ -1,148 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Justin Santa Barbara -# 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. - -''' -JSON related utilities. - -This module provides a few things: - - 1) A handy function for getting an object down to something that can be - JSON serialized. See to_primitive(). - - 2) Wrappers around loads() and dumps(). The dumps() wrapper will - automatically use to_primitive() for you if needed. - - 3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson - is available. -''' - - -import datetime -import inspect -import itertools -import json -import xmlrpclib - -from quantumclient.openstack.common import timeutils - - -def to_primitive(value, convert_instances=False, level=0): - """Convert a complex object into primitives. - - Handy for JSON serialization. We can optionally handle instances, - but since this is a recursive function, we could have cyclical - data structures. - - To handle cyclical data structures we could track the actual objects - visited in a set, but not all objects are hashable. Instead we just - track the depth of the object inspections and don't go too deep. - - Therefore, convert_instances=True is lossy ... be aware. - - """ - nasty = [inspect.ismodule, inspect.isclass, inspect.ismethod, - inspect.isfunction, inspect.isgeneratorfunction, - inspect.isgenerator, inspect.istraceback, inspect.isframe, - inspect.iscode, inspect.isbuiltin, inspect.isroutine, - inspect.isabstract] - for test in nasty: - if test(value): - return unicode(value) - - # value of itertools.count doesn't get caught by inspects - # above and results in infinite loop when list(value) is called. - if type(value) == itertools.count: - return unicode(value) - - # FIXME(vish): Workaround for LP bug 852095. Without this workaround, - # tests that raise an exception in a mocked method that - # has a @wrap_exception with a notifier will fail. If - # we up the dependency to 0.5.4 (when it is released) we - # can remove this workaround. - if getattr(value, '__module__', None) == 'mox': - return 'mock' - - if level > 3: - return '?' - - # The try block may not be necessary after the class check above, - # but just in case ... - try: - # It's not clear why xmlrpclib created their own DateTime type, but - # for our purposes, make it a datetime type which is explicitly - # handled - if isinstance(value, xmlrpclib.DateTime): - value = datetime.datetime(*tuple(value.timetuple())[:6]) - - if isinstance(value, (list, tuple)): - o = [] - for v in value: - o.append(to_primitive(v, convert_instances=convert_instances, - level=level)) - return o - elif isinstance(value, dict): - o = {} - for k, v in value.iteritems(): - o[k] = to_primitive(v, convert_instances=convert_instances, - level=level) - return o - elif isinstance(value, datetime.datetime): - return timeutils.strtime(value) - elif hasattr(value, 'iteritems'): - return to_primitive(dict(value.iteritems()), - convert_instances=convert_instances, - level=level + 1) - elif hasattr(value, '__iter__'): - return to_primitive(list(value), - convert_instances=convert_instances, - level=level) - elif convert_instances and hasattr(value, '__dict__'): - # Likely an instance of something. Watch for cycles. - # Ignore class member vars. - return to_primitive(value.__dict__, - convert_instances=convert_instances, - level=level + 1) - else: - return value - except TypeError: - # Class objects are tricky since they may define something like - # __iter__ defined but it isn't callable as list(). - return unicode(value) - - -def dumps(value, default=to_primitive, **kwargs): - return json.dumps(value, default=default, **kwargs) - - -def loads(s): - return json.loads(s) - - -def load(s): - return json.load(s) - - -try: - import anyjson -except ImportError: - pass -else: - anyjson._modules.append((__name__, 'dumps', TypeError, - 'loads', ValueError, 'load')) - anyjson.force_implementation(__name__) diff --git a/quantumclient/openstack/common/setup.py b/quantumclient/openstack/common/setup.py deleted file mode 100644 index e6f72f034..000000000 --- a/quantumclient/openstack/common/setup.py +++ /dev/null @@ -1,366 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# 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. - -""" -Utilities with minimum-depends for use in setup.py -""" - -import datetime -import os -import re -import subprocess -import sys - -from setuptools.command import sdist - - -def parse_mailmap(mailmap='.mailmap'): - mapping = {} - if os.path.exists(mailmap): - with open(mailmap, 'r') as fp: - for l in fp: - l = l.strip() - if not l.startswith('#') and ' ' in l: - canonical_email, alias = [x for x in l.split(' ') - if x.startswith('<')] - mapping[alias] = canonical_email - return mapping - - -def canonicalize_emails(changelog, mapping): - """Takes in a string and an email alias mapping and replaces all - instances of the aliases in the string with their real email. - """ - for alias, email in mapping.iteritems(): - changelog = changelog.replace(alias, email) - return changelog - - -# Get requirements from the first file that exists -def get_reqs_from_files(requirements_files): - for requirements_file in requirements_files: - if os.path.exists(requirements_file): - with open(requirements_file, 'r') as fil: - return fil.read().split('\n') - return [] - - -def parse_requirements(requirements_files=['requirements.txt', - 'tools/pip-requires']): - requirements = [] - for line in get_reqs_from_files(requirements_files): - # For the requirements list, we need to inject only the portion - # after egg= so that distutils knows the package it's looking for - # such as: - # -e git://github.com/openstack/nova/master#egg=nova - if re.match(r'\s*-e\s+', line): - requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', - line)) - # such as: - # http://github.com/openstack/nova/zipball/master#egg=nova - elif re.match(r'\s*https?:', line): - requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', - line)) - # -f lines are for index locations, and don't get used here - elif re.match(r'\s*-f\s+', line): - pass - # argparse is part of the standard library starting with 2.7 - # adding it to the requirements list screws distro installs - elif line == 'argparse' and sys.version_info >= (2, 7): - pass - else: - requirements.append(line) - - return requirements - - -def parse_dependency_links(requirements_files=['requirements.txt', - 'tools/pip-requires']): - dependency_links = [] - # dependency_links inject alternate locations to find packages listed - # in requirements - for line in get_reqs_from_files(requirements_files): - # skip comments and blank lines - if re.match(r'(\s*#)|(\s*$)', line): - continue - # lines with -e or -f need the whole line, minus the flag - if re.match(r'\s*-[ef]\s+', line): - dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line)) - # lines that are only urls can go in unmolested - elif re.match(r'\s*https?:', line): - dependency_links.append(line) - return dependency_links - - -def write_requirements(): - venv = os.environ.get('VIRTUAL_ENV', None) - if venv is not None: - with open("requirements.txt", "w") as req_file: - output = subprocess.Popen(["pip", "-E", venv, "freeze", "-l"], - stdout=subprocess.PIPE) - requirements = output.communicate()[0].strip() - req_file.write(requirements) - - -def _run_shell_command(cmd): - if os.name == 'nt': - output = subprocess.Popen(["cmd.exe", "/C", cmd], - stdout=subprocess.PIPE) - else: - output = subprocess.Popen(["/bin/sh", "-c", cmd], - stdout=subprocess.PIPE) - out = output.communicate() - if len(out) == 0: - return None - if len(out[0].strip()) == 0: - return None - return out[0].strip() - - -def _get_git_next_version_suffix(branch_name): - datestamp = datetime.datetime.now().strftime('%Y%m%d') - if branch_name == 'milestone-proposed': - revno_prefix = "r" - else: - revno_prefix = "" - _run_shell_command("git fetch origin +refs/meta/*:refs/remotes/meta/*") - milestone_cmd = "git show meta/openstack/release:%s" % branch_name - milestonever = _run_shell_command(milestone_cmd) - if milestonever: - first_half = "%s~%s" % (milestonever, datestamp) - else: - first_half = datestamp - - post_version = _get_git_post_version() - # post version should look like: - # 0.1.1.4.gcc9e28a - # where the bit after the last . is the short sha, and the bit between - # the last and second to last is the revno count - (revno, sha) = post_version.split(".")[-2:] - second_half = "%s%s.%s" % (revno_prefix, revno, sha) - return ".".join((first_half, second_half)) - - -def _get_git_current_tag(): - return _run_shell_command("git tag --contains HEAD") - - -def _get_git_tag_info(): - return _run_shell_command("git describe --tags") - - -def _get_git_post_version(): - current_tag = _get_git_current_tag() - if current_tag is not None: - return current_tag - else: - tag_info = _get_git_tag_info() - if tag_info is None: - base_version = "0.0" - cmd = "git --no-pager log --oneline" - out = _run_shell_command(cmd) - revno = len(out.split("\n")) - sha = _run_shell_command("git describe --always") - else: - tag_infos = tag_info.split("-") - base_version = "-".join(tag_infos[:-2]) - (revno, sha) = tag_infos[-2:] - return "%s.%s.%s" % (base_version, revno, sha) - - -def write_git_changelog(): - """Write a changelog based on the git changelog.""" - new_changelog = 'ChangeLog' - if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'): - if os.path.isdir('.git'): - git_log_cmd = 'git log --stat' - changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() - with open(new_changelog, "w") as changelog_file: - changelog_file.write(canonicalize_emails(changelog, mailmap)) - else: - open(new_changelog, 'w').close() - - -def generate_authors(): - """Create AUTHORS file using git commits.""" - jenkins_email = 'jenkins@review.(openstack|stackforge).org' - old_authors = 'AUTHORS.in' - new_authors = 'AUTHORS' - if not os.getenv('SKIP_GENERATE_AUTHORS'): - if os.path.isdir('.git'): - # don't include jenkins email address in AUTHORS file - git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | " - "egrep -v '" + jenkins_email + "'") - changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() - with open(new_authors, 'w') as new_authors_fh: - new_authors_fh.write(canonicalize_emails(changelog, mailmap)) - if os.path.exists(old_authors): - with open(old_authors, "r") as old_authors_fh: - new_authors_fh.write('\n' + old_authors_fh.read()) - else: - open(new_authors, 'w').close() - - -_rst_template = """%(heading)s -%(underline)s - -.. automodule:: %(module)s - :members: - :undoc-members: - :show-inheritance: -""" - - -def read_versioninfo(project): - """Read the versioninfo file. If it doesn't exist, we're in a github - zipball, and there's really no way to know what version we really - are, but that should be ok, because the utility of that should be - just about nil if this code path is in use in the first place.""" - versioninfo_path = os.path.join(project, 'versioninfo') - if os.path.exists(versioninfo_path): - with open(versioninfo_path, 'r') as vinfo: - version = vinfo.read().strip() - else: - version = "0.0.0" - return version - - -def write_versioninfo(project, version): - """Write a simple file containing the version of the package.""" - with open(os.path.join(project, 'versioninfo'), 'w') as fil: - fil.write("%s\n" % version) - - -def get_cmdclass(): - """Return dict of commands to run from setup.py.""" - - cmdclass = dict() - - def _find_modules(arg, dirname, files): - for filename in files: - if filename.endswith('.py') and filename != '__init__.py': - arg["%s.%s" % (dirname.replace('/', '.'), - filename[:-3])] = True - - class LocalSDist(sdist.sdist): - """Builds the ChangeLog and Authors files from VC first.""" - - def run(self): - write_git_changelog() - generate_authors() - # sdist.sdist is an old style class, can't use super() - sdist.sdist.run(self) - - cmdclass['sdist'] = LocalSDist - - # If Sphinx is installed on the box running setup.py, - # enable setup.py to build the documentation, otherwise, - # just ignore it - try: - from sphinx.setup_command import BuildDoc - - class LocalBuildDoc(BuildDoc): - def generate_autoindex(self): - print "**Autodocumenting from %s" % os.path.abspath(os.curdir) - modules = {} - option_dict = self.distribution.get_option_dict('build_sphinx') - source_dir = os.path.join(option_dict['source_dir'][1], 'api') - if not os.path.exists(source_dir): - os.makedirs(source_dir) - for pkg in self.distribution.packages: - if '.' not in pkg: - os.path.walk(pkg, _find_modules, modules) - module_list = modules.keys() - module_list.sort() - autoindex_filename = os.path.join(source_dir, 'autoindex.rst') - with open(autoindex_filename, 'w') as autoindex: - autoindex.write(""".. toctree:: - :maxdepth: 1 - -""") - for module in module_list: - output_filename = os.path.join(source_dir, - "%s.rst" % module) - heading = "The :mod:`%s` Module" % module - underline = "=" * len(heading) - values = dict(module=module, heading=heading, - underline=underline) - - print "Generating %s" % output_filename - with open(output_filename, 'w') as output_file: - output_file.write(_rst_template % values) - autoindex.write(" %s.rst\n" % module) - - def run(self): - if not os.getenv('SPHINX_DEBUG'): - self.generate_autoindex() - - for builder in ['html', 'man']: - self.builder = builder - self.finalize_options() - self.project = self.distribution.get_name() - self.version = self.distribution.get_version() - self.release = self.distribution.get_version() - BuildDoc.run(self) - cmdclass['build_sphinx'] = LocalBuildDoc - except ImportError: - pass - - return cmdclass - - -def get_git_branchname(): - for branch in _run_shell_command("git branch --color=never").split("\n"): - if branch.startswith('*'): - _branch_name = branch.split()[1].strip() - if _branch_name == "(no": - _branch_name = "no-branch" - return _branch_name - - -def get_pre_version(projectname, base_version): - """Return a version which is leading up to a version that will - be released in the future.""" - if os.path.isdir('.git'): - current_tag = _get_git_current_tag() - if current_tag is not None: - version = current_tag - else: - branch_name = os.getenv('BRANCHNAME', - os.getenv('GERRIT_REFNAME', - get_git_branchname())) - version_suffix = _get_git_next_version_suffix(branch_name) - version = "%s~%s" % (base_version, version_suffix) - write_versioninfo(projectname, version) - return version - else: - version = read_versioninfo(projectname) - return version - - -def get_post_version(projectname): - """Return a version which is equal to the tag that's on the current - revision if there is one, or tag plus number of additional revisions - if the current revision has no tag.""" - - if os.path.isdir('.git'): - version = _get_git_post_version() - write_versioninfo(projectname, version) - return version - return read_versioninfo(projectname) diff --git a/quantumclient/openstack/common/timeutils.py b/quantumclient/openstack/common/timeutils.py deleted file mode 100644 index 0f346087f..000000000 --- a/quantumclient/openstack/common/timeutils.py +++ /dev/null @@ -1,164 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# 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. - -""" -Time related utilities and helper functions. -""" - -import calendar -import datetime - -import iso8601 - - -TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" -PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" - - -def isotime(at=None): - """Stringify time in ISO 8601 format""" - if not at: - at = utcnow() - str = at.strftime(TIME_FORMAT) - tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' - str += ('Z' if tz == 'UTC' else tz) - return str - - -def parse_isotime(timestr): - """Parse time from ISO 8601 format""" - try: - return iso8601.parse_date(timestr) - except iso8601.ParseError as e: - raise ValueError(e.message) - except TypeError as e: - raise ValueError(e.message) - - -def strtime(at=None, fmt=PERFECT_TIME_FORMAT): - """Returns formatted utcnow.""" - if not at: - at = utcnow() - return at.strftime(fmt) - - -def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): - """Turn a formatted time back into a datetime.""" - return datetime.datetime.strptime(timestr, fmt) - - -def normalize_time(timestamp): - """Normalize time in arbitrary timezone to UTC naive object""" - offset = timestamp.utcoffset() - if offset is None: - return timestamp - return timestamp.replace(tzinfo=None) - offset - - -def is_older_than(before, seconds): - """Return True if before is older than seconds.""" - if isinstance(before, basestring): - before = parse_strtime(before).replace(tzinfo=None) - return utcnow() - before > datetime.timedelta(seconds=seconds) - - -def is_newer_than(after, seconds): - """Return True if after is newer than seconds.""" - if isinstance(after, basestring): - after = parse_strtime(after).replace(tzinfo=None) - return after - utcnow() > datetime.timedelta(seconds=seconds) - - -def utcnow_ts(): - """Timestamp version of our utcnow function.""" - return calendar.timegm(utcnow().timetuple()) - - -def utcnow(): - """Overridable version of utils.utcnow.""" - if utcnow.override_time: - try: - return utcnow.override_time.pop(0) - except AttributeError: - return utcnow.override_time - return datetime.datetime.utcnow() - - -utcnow.override_time = None - - -def set_time_override(override_time=datetime.datetime.utcnow()): - """ - Override utils.utcnow to return a constant time or a list thereof, - one at a time. - """ - utcnow.override_time = override_time - - -def advance_time_delta(timedelta): - """Advance overridden time using a datetime.timedelta.""" - assert(not utcnow.override_time is None) - try: - for dt in utcnow.override_time: - dt += timedelta - except TypeError: - utcnow.override_time += timedelta - - -def advance_time_seconds(seconds): - """Advance overridden time by seconds.""" - advance_time_delta(datetime.timedelta(0, seconds)) - - -def clear_time_override(): - """Remove the overridden time.""" - utcnow.override_time = None - - -def marshall_now(now=None): - """Make an rpc-safe datetime with microseconds. - - Note: tzinfo is stripped, but not required for relative times.""" - if not now: - now = utcnow() - return dict(day=now.day, month=now.month, year=now.year, hour=now.hour, - minute=now.minute, second=now.second, - microsecond=now.microsecond) - - -def unmarshall_time(tyme): - """Unmarshall a datetime dict.""" - return datetime.datetime(day=tyme['day'], - month=tyme['month'], - year=tyme['year'], - hour=tyme['hour'], - minute=tyme['minute'], - second=tyme['second'], - microsecond=tyme['microsecond']) - - -def delta_seconds(before, after): - """ - Compute the difference in seconds between two date, time, or - datetime objects (as a float, to microsecond resolution). - """ - delta = after - before - try: - return delta.total_seconds() - except AttributeError: - return ((delta.days * 24 * 3600) + delta.seconds + - float(delta.microseconds) / (10 ** 6)) diff --git a/quantumclient/quantum/__init__.py b/quantumclient/quantum/__init__.py deleted file mode 100644 index 63c3905e2..000000000 --- a/quantumclient/quantum/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. diff --git a/quantumclient/quantum/client.py b/quantumclient/quantum/client.py deleted file mode 100644 index c89408b9f..000000000 --- a/quantumclient/quantum/client.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -from quantumclient.common import exceptions -from quantumclient.common import utils - - -API_NAME = 'network' -API_VERSIONS = { - '2.0': 'quantumclient.v2_0.client.Client', -} - - -def make_client(instance): - """Returns an quantum client. - """ - quantum_client = utils.get_client_class( - API_NAME, - instance._api_version[API_NAME], - API_VERSIONS, - ) - instance.initialize() - url = instance._url - url = url.rstrip("/") - if '2.0' == instance._api_version[API_NAME]: - client = quantum_client(username=instance._username, - tenant_name=instance._tenant_name, - password=instance._password, - region_name=instance._region_name, - auth_url=instance._auth_url, - endpoint_url=url, - token=instance._token, - auth_strategy=instance._auth_strategy) - return client - else: - raise exceptions.UnsupportedVersion("API version %s is not supported" % - instance._api_version[API_NAME]) - - -def Client(api_version, *args, **kwargs): - """Return an quantum client. - @param api_version: only 2.0 is supported now - """ - quantum_client = utils.get_client_class( - API_NAME, - api_version, - API_VERSIONS, - ) - return quantum_client(*args, **kwargs) diff --git a/quantumclient/quantum/v2_0/__init__.py b/quantumclient/quantum/v2_0/__init__.py deleted file mode 100644 index cba5ca1ad..000000000 --- a/quantumclient/quantum/v2_0/__init__.py +++ /dev/null @@ -1,520 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import argparse -import logging -import re - -from cliff import lister -from cliff import show - -from quantumclient.common import command -from quantumclient.common import exceptions -from quantumclient.common import utils - -HEX_ELEM = '[0-9A-Fa-f]' -UUID_PATTERN = '-'.join([HEX_ELEM + '{8}', HEX_ELEM + '{4}', - HEX_ELEM + '{4}', HEX_ELEM + '{4}', - HEX_ELEM + '{12}']) - - -def find_resourceid_by_name_or_id(client, resource, name_or_id): - obj_lister = getattr(client, "list_%ss" % resource) - # perform search by id only if we are passing a valid UUID - match = re.match(UUID_PATTERN, name_or_id) - collection = resource + "s" - if match: - data = obj_lister(id=name_or_id, fields='id') - if data and data[collection]: - return data[collection][0]['id'] - return _find_resourceid_by_name(client, resource, name_or_id) - - -def _find_resourceid_by_name(client, resource, name): - obj_lister = getattr(client, "list_%ss" % resource) - data = obj_lister(name=name, fields='id') - collection = resource + "s" - info = data[collection] - if len(info) > 1: - msg = (_("Multiple %(resource)s matches found for name '%(name)s'," - " use an ID to be more specific.") % - {'resource': resource, 'name': name}) - raise exceptions.QuantumClientException( - message=msg) - elif len(info) == 0: - not_found_message = (_("Unable to find %(resource)s with name " - "'%(name)s'") % - {'resource': resource, 'name': name}) - # 404 is used to simulate server side behavior - raise exceptions.QuantumClientException( - message=not_found_message, status_code=404) - else: - return info[0]['id'] - - -def add_show_list_common_argument(parser): - parser.add_argument( - '-D', '--show-details', - help='show detailed info', - action='store_true', - default=False, ) - parser.add_argument( - '--show_details', - action='store_true', - help=argparse.SUPPRESS) - parser.add_argument( - '--fields', - help=argparse.SUPPRESS, - action='append', - default=[]) - parser.add_argument( - '-F', '--field', - dest='fields', metavar='FIELD', - help='specify the field(s) to be returned by server,' - ' can be repeated', - action='append', - default=[]) - - -def add_extra_argument(parser, name, _help): - parser.add_argument( - name, - nargs=argparse.REMAINDER, - help=_help + ': --key1 [type=int|bool|...] value ' - '[--key2 [type=int|bool|...] value ...]') - - -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 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. - - ''' - # -- is a pseudo argument - values_specs_copy = values_specs[:] - if values_specs_copy and values_specs_copy[0] == '--': - del values_specs_copy[0] - _options = {} - current_arg = None - _values_specs = [] - _value_number = 0 - _list_flag = False - current_item = None - for _item in values_specs_copy: - if _item.startswith('--'): - if current_arg is not None: - if _value_number > 1 or _list_flag: - current_arg.update({'nargs': '+'}) - elif _value_number == 0: - current_arg.update({'action': 'store_true'}) - _temp = _item - if "=" in _item: - _item = _item.split('=')[0] - if _item in _options: - raise exceptions.CommandError( - "duplicated options %s" % ' '.join(values_specs)) - else: - _options.update({_item: {}}) - current_arg = _options[_item] - _item = _temp - 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: - _type_str = _item.split('=', 2)[1] - current_arg.update({'type': eval(_type_str)}) - if _type_str == 'bool': - current_arg.update({'type': utils.str2bool}) - elif _type_str == 'dict': - current_arg.update({'type': utils.str2dict}) - continue - elif _item == 'list=true': - _list_flag = True - continue - if not _item.startswith('--'): - 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 - elif _item.startswith('--'): - current_item = _item - if '=' in current_item: - _value_number = 1 - else: - _value_number = 0 - _list_flag = False - _values_specs.append(_item) - if current_arg is not None: - if _value_number > 1 or _list_flag: - current_arg.update({'nargs': '+'}) - elif _value_number == 0: - current_arg.update({'action': 'store_true'}) - _args = None - if _values_specs: - _parser = argparse.ArgumentParser(add_help=False) - for opt, optspec in _options.iteritems(): - _parser.add_argument(opt, **optspec) - _args = _parser.parse_args(_values_specs) - result_dict = {} - if _args: - for opt in _options.iterkeys(): - _opt = opt.split('--', 2)[1] - _opt = _opt.replace('-', '_') - _value = getattr(_args, _opt) - if _value is not None: - 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.iteritems(): - 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 type(arg_value[0]) == type(value[0]): - arg_value.extend(value) - _extra_values.pop(key) - - -def update_dict(obj, dict, attributes): - for attribute in attributes: - if hasattr(obj, attribute) and getattr(obj, attribute): - dict[attribute] = getattr(obj, attribute) - - -class QuantumCommand(command.OpenStackCommand): - api = 'network' - log = logging.getLogger(__name__ + '.QuantumCommand') - values_specs = [] - json_indent = None - - def get_client(self): - return self.app.client_manager.quantum - - def get_parser(self, prog_name): - parser = super(QuantumCommand, self).get_parser(prog_name) - parser.add_argument( - '--request-format', - help=_('the xml or json request format'), - default='json', - choices=['json', 'xml', ], ) - parser.add_argument( - '--request_format', - choices=['json', 'xml', ], - help=argparse.SUPPRESS) - - return parser - - def format_output_data(self, data): - # Modify data to make it more readable - if self.resource in data: - for k, v in data[self.resource].iteritems(): - if isinstance(v, list): - value = '\n'.join(utils.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 = utils.dumps(v, indent=self.json_indent) - data[self.resource][k] = value - elif v is None: - data[self.resource][k] = '' - - def add_known_arguments(self, parser): - pass - - def args2body(self, parsed_args): - return {} - - -class CreateCommand(QuantumCommand, show.ShowOne): - """Create a resource for a given tenant - - """ - - api = 'network' - resource = None - 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 get_data(self, parsed_args): - self.log.debug('get_data(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - _extra_values = parse_args_to_dict(self.values_specs) - _merge_args(self, parsed_args, _extra_values, - self.values_specs) - body = self.args2body(parsed_args) - body[self.resource].update(_extra_values) - obj_creator = getattr(quantum_client, - "create_%s" % self.resource) - data = obj_creator(body) - self.format_output_data(data) - # {u'network': {u'id': u'e9424a76-6db4-4c93-97b6-ec311cd51f19'}} - info = self.resource in data and data[self.resource] or None - if info: - print >>self.app.stdout, _('Created a new %s:' % self.resource) - else: - info = {'': ''} - return zip(*sorted(info.iteritems())) - - -class UpdateCommand(QuantumCommand): - """Update resource's information - """ - - api = 'network' - resource = None - log = None - - def get_parser(self, prog_name): - parser = super(UpdateCommand, self).get_parser(prog_name) - parser.add_argument( - 'id', metavar=self.resource, - help='ID or name of %s to update' % self.resource) - self.add_known_arguments(parser) - return parser - - def run(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - _extra_values = parse_args_to_dict(self.values_specs) - _merge_args(self, parsed_args, _extra_values, - self.values_specs) - body = self.args2body(parsed_args) - if self.resource in body: - body[self.resource].update(_extra_values) - else: - body[self.resource] = _extra_values - if not body[self.resource]: - raise exceptions.CommandError( - "Must specify new values to update %s" % self.resource) - _id = find_resourceid_by_name_or_id(quantum_client, - self.resource, - parsed_args.id) - obj_updator = getattr(quantum_client, - "update_%s" % self.resource) - obj_updator(_id, body) - print >>self.app.stdout, ( - _('Updated %(resource)s: %(id)s') % - {'id': parsed_args.id, 'resource': self.resource}) - return - - -class DeleteCommand(QuantumCommand): - """Delete a given resource - - """ - - api = 'network' - resource = None - log = None - allow_names = True - - def get_parser(self, prog_name): - parser = super(DeleteCommand, self).get_parser(prog_name) - if self.allow_names: - help_str = 'ID or name of %s to delete' - else: - help_str = 'ID of %s to delete' - parser.add_argument( - 'id', metavar=self.resource, - help=help_str % self.resource) - return parser - - def run(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - obj_deleter = getattr(quantum_client, - "delete_%s" % self.resource) - if self.allow_names: - _id = find_resourceid_by_name_or_id(quantum_client, self.resource, - parsed_args.id) - else: - _id = parsed_args.id - obj_deleter(_id) - print >>self.app.stdout, (_('Deleted %(resource)s: %(id)s') - % {'id': parsed_args.id, - 'resource': self.resource}) - return - - -class ListCommand(QuantumCommand, lister.Lister): - """List resources that belong to a given tenant - - """ - - api = 'network' - resource = None - log = None - _formatters = None - list_columns = [] - - def get_parser(self, prog_name): - parser = super(ListCommand, self).get_parser(prog_name) - add_show_list_common_argument(parser) - return parser - - def args2search_opts(self, parsed_args): - search_opts = {} - fields = parsed_args.fields - if parsed_args.fields: - search_opts.update({'fields': fields}) - if parsed_args.show_details: - search_opts.update({'verbose': 'True'}) - return search_opts - - def retrieve_list(self, parsed_args): - """Retrieve a list of resources from Quantum server""" - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - _extra_values = parse_args_to_dict(self.values_specs) - _merge_args(self, parsed_args, _extra_values, - self.values_specs) - search_opts = self.args2search_opts(parsed_args) - search_opts.update(_extra_values) - obj_lister = getattr(quantum_client, - "list_%ss" % self.resource) - data = obj_lister(**search_opts) - collection = self.resource + "s" - return data.get(collection, []) - - def extend_list(self, data, parsed_args): - """Update a retrieved list. - - This method provides a way to modify a original list returned from - the quantum server. For example, you can add subnet cidr information - to a list network. - """ - pass - - def setup_columns(self, info, parsed_args): - _columns = len(info) > 0 and sorted(info[0].keys()) or [] - if not _columns: - # clean the parsed_args.columns so that cliff will not break - parsed_args.columns = [] - elif parsed_args.columns: - _columns = [x for x in parsed_args.columns if x in _columns] - elif self.list_columns: - # if no -c(s) by user and list_columns, we use columns in - # both list_columns and returned resource. - # Also Keep their order the same as in list_columns - _columns = [x for x in self.list_columns if x in _columns] - return (_columns, (utils.get_item_properties( - s, _columns, formatters=self._formatters, ) - for s in info), ) - - def get_data(self, parsed_args): - self.log.debug('get_data(%s)' % parsed_args) - data = self.retrieve_list(parsed_args) - self.extend_list(data, parsed_args) - return self.setup_columns(data, parsed_args) - - -class ShowCommand(QuantumCommand, show.ShowOne): - """Show information of a given resource - - """ - - api = 'network' - resource = None - log = None - allow_names = True - - def get_parser(self, prog_name): - parser = super(ShowCommand, self).get_parser(prog_name) - add_show_list_common_argument(parser) - if self.allow_names: - help_str = 'ID or name of %s to look up' - else: - help_str = 'ID of %s to look up' - parser.add_argument( - 'id', metavar=self.resource, - help=help_str % self.resource) - return parser - - def get_data(self, parsed_args): - self.log.debug('get_data(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - - params = {} - if parsed_args.show_details: - params = {'verbose': 'True'} - if parsed_args.fields: - params = {'fields': parsed_args.fields} - if self.allow_names: - _id = find_resourceid_by_name_or_id(quantum_client, self.resource, - parsed_args.id) - else: - _id = parsed_args.id - - obj_shower = getattr(quantum_client, "show_%s" % self.resource) - data = obj_shower(_id, **params) - self.format_output_data(data) - resource = data[self.resource] - if self.resource in data: - return zip(*sorted(resource.iteritems())) - else: - return None diff --git a/quantumclient/quantum/v2_0/agent.py b/quantumclient/quantum/v2_0/agent.py deleted file mode 100644 index 1597465be..000000000 --- a/quantumclient/quantum/v2_0/agent.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2013 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import logging - -from quantumclient.quantum import v2_0 as quantumV20 - - -def _format_timestamp(component): - try: - return component['heartbeat_timestamp'].split(".", 2)[0] - except Exception: - return '' - - -class ListAgent(quantumV20.ListCommand): - """List agents.""" - - resource = 'agent' - log = logging.getLogger(__name__ + '.ListAgent') - list_columns = ['id', 'agent_type', 'host', 'alive', 'admin_state_up'] - _formatters = {'heartbeat_timestamp': _format_timestamp} - - def extend_list(self, data, parsed_args): - for agent in data: - agent['alive'] = ":-)" if agent['alive'] else 'xxx' - - -class ShowAgent(quantumV20.ShowCommand): - """Show information of a given agent.""" - - resource = 'agent' - log = logging.getLogger(__name__ + '.ShowAgent') - allow_names = False - json_indent = 5 - - -class DeleteAgent(quantumV20.DeleteCommand): - """Delete a given agent.""" - - log = logging.getLogger(__name__ + '.DeleteAgent') - resource = 'agent' - - -class UpdateAgent(quantumV20.UpdateCommand): - """Update a given agent.""" - - log = logging.getLogger(__name__ + '.UpdateAgent') - resource = 'agent' - allow_names = False diff --git a/quantumclient/quantum/v2_0/extension.py b/quantumclient/quantum/v2_0/extension.py deleted file mode 100644 index df4a88bbf..000000000 --- a/quantumclient/quantum/v2_0/extension.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import logging - -from cliff import lister -from cliff import show - -from quantumclient.common import utils -from quantumclient.quantum.v2_0 import QuantumCommand - - -class ListExt(QuantumCommand, lister.Lister): - """List all exts.""" - - api = 'network' - resource = 'extension' - log = logging.getLogger(__name__ + '.ListExt') - _formatters = None - - def get_parser(self, prog_name): - parser = super(ListExt, self).get_parser(prog_name) - return parser - - def get_data(self, parsed_args): - self.log.debug('get_data(%s)' % parsed_args) - quantum_client = self.get_client() - search_opts = {} - quantum_client.format = parsed_args.request_format - obj_lister = getattr(quantum_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 ShowExt(QuantumCommand, show.ShowOne): - """Show information of a given resource - - """ - api = 'network' - resource = "extension" - log = logging.getLogger(__name__ + '.ShowExt') - - def get_parser(self, prog_name): - parser = super(ShowExt, self).get_parser(prog_name) - parser.add_argument( - 'ext_alias', metavar='ext-alias', - help='the extension alias') - return parser - - def get_data(self, parsed_args): - self.log.debug('get_data(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - params = {} - obj_shower = getattr(quantum_client, - "show_%s" % self.resource) - data = obj_shower(parsed_args.ext_alias, **params) - if self.resource in data: - for k, v in data[self.resource].iteritems(): - if isinstance(v, list): - value = "" - for _item in v: - if value: - value += "\n" - if isinstance(_item, dict): - value += utils.dumps(_item) - else: - value += str(_item) - data[self.resource][k] = value - elif v is None: - data[self.resource][k] = '' - return zip(*sorted(data[self.resource].iteritems())) - else: - return None diff --git a/quantumclient/quantum/v2_0/floatingip.py b/quantumclient/quantum/v2_0/floatingip.py deleted file mode 100644 index 3198dfeb7..000000000 --- a/quantumclient/quantum/v2_0/floatingip.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import argparse -import logging - -from quantumclient.quantum import v2_0 as quantumv20 -from quantumclient.quantum.v2_0 import CreateCommand -from quantumclient.quantum.v2_0 import DeleteCommand -from quantumclient.quantum.v2_0 import ListCommand -from quantumclient.quantum.v2_0 import QuantumCommand -from quantumclient.quantum.v2_0 import ShowCommand - - -class ListFloatingIP(ListCommand): - """List floating ips that belong to a given tenant.""" - - resource = 'floatingip' - log = logging.getLogger(__name__ + '.ListFloatingIP') - _formatters = {} - list_columns = ['id', 'fixed_ip_address', 'floating_ip_address', - 'port_id'] - - -class ShowFloatingIP(ShowCommand): - """Show information of a given floating ip.""" - - resource = 'floatingip' - log = logging.getLogger(__name__ + '.ShowFloatingIP') - allow_names = False - - -class CreateFloatingIP(CreateCommand): - """Create a floating ip for a given tenant.""" - - resource = 'floatingip' - log = logging.getLogger(__name__ + '.CreateFloatingIP') - - def add_known_arguments(self, parser): - parser.add_argument( - 'floating_network_id', metavar='FLOATING_NETWORK', - help='Network name or id to allocate floating IP from') - parser.add_argument( - '--port-id', - help='ID of the port to be associated with the floatingip') - 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) - - def args2body(self, parsed_args): - _network_id = quantumv20.find_resourceid_by_name_or_id( - self.get_client(), 'network', parsed_args.floating_network_id) - body = {self.resource: {'floating_network_id': _network_id}} - if parsed_args.port_id: - body[self.resource].update({'port_id': parsed_args.port_id}) - if parsed_args.tenant_id: - body[self.resource].update({'tenant_id': parsed_args.tenant_id}) - if parsed_args.fixed_ip_address: - body[self.resource].update({'fixed_ip_address': - parsed_args.fixed_ip_address}) - return body - - -class DeleteFloatingIP(DeleteCommand): - """Delete a given floating ip.""" - - log = logging.getLogger(__name__ + '.DeleteFloatingIP') - resource = 'floatingip' - allow_names = False - - -class AssociateFloatingIP(QuantumCommand): - """Create a mapping between a floating ip and a fixed ip.""" - - api = 'network' - log = logging.getLogger(__name__ + '.AssociateFloatingIP') - 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='IP address of the floating IP to associate') - parser.add_argument( - 'port_id', - help='ID of the port to be associated with the floatingip') - 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 run(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - update_dict = {} - if parsed_args.port_id: - update_dict['port_id'] = parsed_args.port_id - if parsed_args.fixed_ip_address: - update_dict['fixed_ip_address'] = parsed_args.fixed_ip_address - quantum_client.update_floatingip(parsed_args.floatingip_id, - {'floatingip': update_dict}) - print >>self.app.stdout, ( - _('Associated floatingip %s') % parsed_args.floatingip_id) - - -class DisassociateFloatingIP(QuantumCommand): - """Remove a mapping from a floating ip to a fixed ip. - """ - - api = 'network' - log = logging.getLogger(__name__ + '.DisassociateFloatingIP') - 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='IP address of the floating IP to associate') - return parser - - def run(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - quantum_client.update_floatingip(parsed_args.floatingip_id, - {'floatingip': {'port_id': None}}) - print >>self.app.stdout, ( - _('Disassociated floatingip %s') % parsed_args.floatingip_id) diff --git a/quantumclient/quantum/v2_0/lb/healthmonitor.py b/quantumclient/quantum/v2_0/lb/healthmonitor.py deleted file mode 100644 index 9aa287401..000000000 --- a/quantumclient/quantum/v2_0/lb/healthmonitor.py +++ /dev/null @@ -1,172 +0,0 @@ -# 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. -# -# @author: Ilya Shakhat, Mirantis Inc. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import logging - -from quantumclient.quantum import v2_0 as quantumv20 - - -class ListHealthMonitor(quantumv20.ListCommand): - """List healthmonitors that belong to a given tenant.""" - - resource = 'health_monitor' - log = logging.getLogger(__name__ + '.ListHealthMonitor') - list_columns = ['id', 'type', 'admin_state_up', 'status'] - _formatters = {} - - -class ShowHealthMonitor(quantumv20.ShowCommand): - """Show information of a given healthmonitor.""" - - resource = 'health_monitor' - log = logging.getLogger(__name__ + '.ShowHealthMonitor') - - -class CreateHealthMonitor(quantumv20.CreateCommand): - """Create a healthmonitor""" - - resource = 'health_monitor' - log = logging.getLogger(__name__ + '.CreateHealthMonitor') - - 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( - '--delay', - required=True, - help='the minimum time in seconds between regular connections ' - 'of the member.') - 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( - '--max-retries', - required=True, - help='number of permissible connection failures before changing ' - 'the member status to INACTIVE.') - 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, - help='one of predefined health monitor types, e.g. RoundRobin') - 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)') - - def args2body(self, parsed_args): - body = { - self.resource: { - 'admin_state_up': parsed_args.admin_state_down, - 'delay': parsed_args.delay, - 'max_retries': parsed_args.max_retries, - 'timeout': parsed_args.timeout, - 'type': parsed_args.type, - }, - } - quantumv20.update_dict(parsed_args, body[self.resource], - ['expected_codes', 'http_method', 'url_path', - 'tenant_id']) - return body - - -class UpdateHealthMonitor(quantumv20.UpdateCommand): - """Update a given healthmonitor.""" - - resource = 'health_monitor' - log = logging.getLogger(__name__ + '.UpdateHealthMonitor') - - -class DeleteHealthMonitor(quantumv20.DeleteCommand): - """Delete a given healthmonitor.""" - - resource = 'health_monitor' - log = logging.getLogger(__name__ + '.DeleteHealthMonitor') - - -class AssociateHealthMonitor(quantumv20.QuantumCommand): - """Create a mapping between a health monitor and a pool.""" - - log = logging.getLogger(__name__ + '.AssociateHealthMonitor') - resource = 'health_monitor' - - def get_parser(self, prog_name): - parser = super(AssociateHealthMonitor, self).get_parser(prog_name) - parser.add_argument( - 'health_monitor_id', - help='Health monitor to associate') - parser.add_argument( - 'pool_id', - help='ID of the pool to be associated with the health monitor') - return parser - - def run(self, parsed_args): - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - body = {'health_monitor': {'id': parsed_args.health_monitor_id}} - pool_id = quantumv20.find_resourceid_by_name_or_id( - quantum_client, 'pool', parsed_args.pool_id) - quantum_client.associate_health_monitor(pool_id, body) - print >>self.app.stdout, (_('Associated health monitor ' - '%s') % parsed_args.health_monitor_id) - - -class DisassociateHealthMonitor(quantumv20.QuantumCommand): - """Remove a mapping from a health monitor to a pool.""" - - log = logging.getLogger(__name__ + '.DisassociateHealthMonitor') - resource = 'health_monitor' - - def get_parser(self, prog_name): - parser = super(DisassociateHealthMonitor, self).get_parser(prog_name) - parser.add_argument( - 'health_monitor_id', - help='Health monitor to associate') - parser.add_argument( - 'pool_id', - help='ID of the pool to be associated with the health monitor') - return parser - - def run(self, parsed_args): - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - pool_id = quantumv20.find_resourceid_by_name_or_id( - quantum_client, 'pool', parsed_args.pool_id) - quantum_client.disassociate_health_monitor(pool_id, - parsed_args - .health_monitor_id) - print >>self.app.stdout, (_('Disassociated health monitor ' - '%s') % parsed_args.health_monitor_id) diff --git a/quantumclient/quantum/v2_0/lb/member.py b/quantumclient/quantum/v2_0/lb/member.py deleted file mode 100644 index 25707175a..000000000 --- a/quantumclient/quantum/v2_0/lb/member.py +++ /dev/null @@ -1,93 +0,0 @@ -# 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. -# -# @author: Ilya Shakhat, Mirantis Inc. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import logging - -from quantumclient.quantum import v2_0 as quantumv20 - - -class ListMember(quantumv20.ListCommand): - """List members that belong to a given tenant.""" - - resource = 'member' - log = logging.getLogger(__name__ + '.ListMember') - list_columns = ['id', 'address', 'port', 'admin_state_up', 'status'] - _formatters = {} - - -class ShowMember(quantumv20.ShowCommand): - """Show information of a given member.""" - - resource = 'member' - log = logging.getLogger(__name__ + '.ShowMember') - - -class CreateMember(quantumv20.CreateCommand): - """Create a member""" - - resource = 'member' - log = logging.getLogger(__name__ + '.CreateMember') - - def add_known_arguments(self, parser): - parser.add_argument( - 'pool_id', metavar='pool', - help='Pool id or name this vip belongs to') - parser.add_argument( - '--address', - required=True, - help='IP address of the pool member on the pool network. ') - parser.add_argument( - '--admin-state-down', - default=True, action='store_false', - help='set admin state up to false') - parser.add_argument( - '--port', - required=True, - help='port on which the pool member listens for requests or ' - 'connections. ') - parser.add_argument( - '--weight', - help='weight of pool member in the pool') - - def args2body(self, parsed_args): - _pool_id = quantumv20.find_resourceid_by_name_or_id( - self.get_client(), 'pool', parsed_args.pool_id) - body = { - self.resource: { - 'pool_id': _pool_id, - 'admin_state_up': parsed_args.admin_state_down, - }, - } - quantumv20.update_dict(parsed_args, body[self.resource], - ['address', 'port', 'weight', 'tenant_id']) - return body - - -class UpdateMember(quantumv20.UpdateCommand): - """Update a given member.""" - - resource = 'member' - log = logging.getLogger(__name__ + '.UpdateMember') - - -class DeleteMember(quantumv20.DeleteCommand): - """Delete a given member.""" - - resource = 'member' - log = logging.getLogger(__name__ + '.DeleteMember') diff --git a/quantumclient/quantum/v2_0/lb/pool.py b/quantumclient/quantum/v2_0/lb/pool.py deleted file mode 100644 index b39c68459..000000000 --- a/quantumclient/quantum/v2_0/lb/pool.py +++ /dev/null @@ -1,120 +0,0 @@ -# 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. -# -# @author: Ilya Shakhat, Mirantis Inc. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import logging - -from quantumclient.quantum import v2_0 as quantumv20 - - -class ListPool(quantumv20.ListCommand): - """List pools that belong to a given tenant.""" - - resource = 'pool' - log = logging.getLogger(__name__ + '.ListPool') - list_columns = ['id', 'name', 'lb_method', 'protocol', - 'admin_state_up', 'status'] - _formatters = {} - - -class ShowPool(quantumv20.ShowCommand): - """Show information of a given pool.""" - - resource = 'pool' - log = logging.getLogger(__name__ + '.ShowPool') - - -class CreatePool(quantumv20.CreateCommand): - """Create a pool""" - - resource = 'pool' - log = logging.getLogger(__name__ + '.CreatePool') - - 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( - '--description', - help='description of the pool') - parser.add_argument( - '--lb-method', - required=True, - 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, - help='protocol for balancing') - parser.add_argument( - '--subnet-id', - required=True, - help='the subnet on which the members of the pool will be located') - - def args2body(self, parsed_args): - body = { - self.resource: { - 'admin_state_up': parsed_args.admin_state_down, - }, - } - quantumv20.update_dict(parsed_args, body[self.resource], - ['description', 'lb_method', 'name', - 'subnet_id', 'protocol', 'tenant_id']) - return body - - -class UpdatePool(quantumv20.UpdateCommand): - """Update a given pool.""" - - resource = 'pool' - log = logging.getLogger(__name__ + '.UpdatePool') - - -class DeletePool(quantumv20.DeleteCommand): - """Delete a given pool.""" - - resource = 'pool' - log = logging.getLogger(__name__ + '.DeletePool') - - -class RetrievePoolStats(quantumv20.ShowCommand): - """Retrieve stats for a given pool.""" - - resource = 'pool' - log = logging.getLogger(__name__ + '.RetrievePoolStats') - - def get_data(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - params = {} - if parsed_args.fields: - params = {'fields': parsed_args.fields} - - data = quantum_client.retrieve_pool_stats(parsed_args.id, **params) - self.format_output_data(data) - stats = data['stats'] - if 'stats' in data: - return zip(*sorted(stats.iteritems())) - else: - return None diff --git a/quantumclient/quantum/v2_0/lb/vip.py b/quantumclient/quantum/v2_0/lb/vip.py deleted file mode 100644 index 741b14d67..000000000 --- a/quantumclient/quantum/v2_0/lb/vip.py +++ /dev/null @@ -1,111 +0,0 @@ -# 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. -# -# @author: Ilya Shakhat, Mirantis Inc. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import logging - -from quantumclient.quantum import v2_0 as quantumv20 - - -class ListVip(quantumv20.ListCommand): - """List vips that belong to a given tenant.""" - - resource = 'vip' - log = logging.getLogger(__name__ + '.ListVip') - list_columns = ['id', 'name', 'algorithm', 'protocol', - 'admin_state_up', 'status'] - _formatters = {} - - -class ShowVip(quantumv20.ShowCommand): - """Show information of a given vip.""" - - resource = 'vip' - log = logging.getLogger(__name__ + '.ShowVip') - - -class CreateVip(quantumv20.CreateCommand): - """Create a vip""" - - resource = 'vip' - log = logging.getLogger(__name__ + '.CreateVip') - - def add_known_arguments(self, parser): - parser.add_argument( - 'pool_id', metavar='pool', - help='Pool id or name this vip belongs to') - parser.add_argument( - '--address', - help='IP address of the vip') - parser.add_argument( - '--admin-state-down', - default=True, 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') - parser.add_argument( - '--description', - help='description of the vip') - parser.add_argument( - '--name', - required=True, - help='name of the vip') - parser.add_argument( - '--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, - help='protocol for balancing') - parser.add_argument( - '--subnet-id', - required=True, - help='the subnet on which to allocate the vip address') - - def args2body(self, parsed_args): - _pool_id = quantumv20.find_resourceid_by_name_or_id( - self.get_client(), 'pool', parsed_args.pool_id) - body = { - self.resource: { - 'pool_id': _pool_id, - 'admin_state_up': parsed_args.admin_state_down, - }, - } - quantumv20.update_dict(parsed_args, body[self.resource], - ['address', 'connection_limit', 'description', - 'name', 'port', 'protocol', 'subnet_id', - 'tenant_id']) - return body - - -class UpdateVip(quantumv20.UpdateCommand): - """Update a given vip.""" - - resource = 'vip' - log = logging.getLogger(__name__ + '.UpdateVip') - - -class DeleteVip(quantumv20.DeleteCommand): - """Delete a given vip.""" - - resource = 'vip' - log = logging.getLogger(__name__ + '.DeleteVip') diff --git a/quantumclient/quantum/v2_0/network.py b/quantumclient/quantum/v2_0/network.py deleted file mode 100644 index dfc0171b6..000000000 --- a/quantumclient/quantum/v2_0/network.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import argparse -import logging - -from quantumclient.common import utils -from quantumclient.quantum.v2_0 import CreateCommand -from quantumclient.quantum.v2_0 import DeleteCommand -from quantumclient.quantum.v2_0 import ListCommand -from quantumclient.quantum.v2_0 import ShowCommand -from quantumclient.quantum.v2_0 import UpdateCommand - - -def _format_subnets(network): - try: - return '\n'.join([' '.join([s['id'], s.get('cidr', '')]) - for s in network['subnets']]) - except Exception: - return '' - - -class ListNetwork(ListCommand): - """List networks that belong to a given tenant.""" - - resource = 'network' - log = logging.getLogger(__name__ + '.ListNetwork') - _formatters = {'subnets': _format_subnets, } - list_columns = ['id', 'name', 'subnets'] - - def extend_list(self, data, parsed_args): - """Add subnet information to a network list""" - quantum_client = self.get_client() - search_opts = {'fields': ['id', 'cidr']} - subnets = quantum_client.list_subnets(**search_opts).get('subnets', []) - 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""" - - log = logging.getLogger(__name__ + '.ListExternalNetwork') - - 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(ShowCommand): - """Show information of a given network.""" - - resource = 'network' - log = logging.getLogger(__name__ + '.ShowNetwork') - - -class CreateNetwork(CreateCommand): - """Create a network for a given tenant.""" - - resource = 'network' - log = logging.getLogger(__name__ + '.CreateNetwork') - - 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( - '--admin_state_down', - action='store_false', - help=argparse.SUPPRESS) - parser.add_argument( - '--shared', - action='store_true', - default=argparse.SUPPRESS, - help='Set the network as shared') - parser.add_argument( - 'name', metavar='NAME', - help='Name of network to create') - - def args2body(self, parsed_args): - body = {'network': { - 'name': parsed_args.name, - 'admin_state_up': parsed_args.admin_state_down}, } - if parsed_args.tenant_id: - body['network'].update({'tenant_id': parsed_args.tenant_id}) - if hasattr(parsed_args, 'shared'): - body['network'].update({'shared': parsed_args.shared}) - return body - - -class DeleteNetwork(DeleteCommand): - """Delete a given network.""" - - log = logging.getLogger(__name__ + '.DeleteNetwork') - resource = 'network' - - -class UpdateNetwork(UpdateCommand): - """Update network's information.""" - - log = logging.getLogger(__name__ + '.UpdateNetwork') - resource = 'network' diff --git a/quantumclient/quantum/v2_0/nvp_qos_queue.py b/quantumclient/quantum/v2_0/nvp_qos_queue.py deleted file mode 100644 index 040ce161c..000000000 --- a/quantumclient/quantum/v2_0/nvp_qos_queue.py +++ /dev/null @@ -1,90 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 Nicira 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 logging - -from quantumclient.quantum import v2_0 as quantumv20 - - -class ListQoSQueue(quantumv20.ListCommand): - """List queues that belong to a given tenant.""" - - resource = 'qos_queue' - log = logging.getLogger(__name__ + '.ListQoSQueue') - _formatters = {} - list_columns = ['id', 'name', 'min', 'max', - 'qos_marking', 'dscp', 'default'] - - -class ShowQoSQueue(quantumv20.ShowCommand): - """Show information of a given queue.""" - - resource = 'qos_queue' - log = logging.getLogger(__name__ + '.ShowQoSQueue') - allow_names = True - - -class CreateQoSQueue(quantumv20.CreateCommand): - """Create a queue.""" - - resource = 'qos_queue' - log = logging.getLogger(__name__ + '.CreateQoSQueue') - - def add_known_arguments(self, parser): - parser.add_argument( - 'name', metavar='NAME', - help='Name of queue') - parser.add_argument( - '--min', - help='min-rate'), - parser.add_argument( - '--max', - help='max-rate'), - parser.add_argument( - '--qos-marking', - help='qos marking untrusted/trusted'), - parser.add_argument( - '--default', - default=False, - help=('If true all ports created with be the size of this queue' - ' if queue is not specified')), - parser.add_argument( - '--dscp', - help='Differentiated Services Code Point'), - - def args2body(self, parsed_args): - params = {'name': parsed_args.name, - 'default': parsed_args.default} - if parsed_args.min: - params['min'] = parsed_args.min - if parsed_args.max: - params['max'] = parsed_args.max - if parsed_args.qos_marking: - params['qos_marking'] = parsed_args.qos_marking - if parsed_args.dscp: - params['dscp'] = parsed_args.dscp - if parsed_args.tenant_id: - params['tenant_id'] = parsed_args.tenant_id - return {'qos_queue': params} - - -class DeleteQoSQueue(quantumv20.DeleteCommand): - """Delete a given queue.""" - - log = logging.getLogger(__name__ + '.DeleteQoSQueue') - resource = 'qos_queue' - allow_names = True diff --git a/quantumclient/quantum/v2_0/port.py b/quantumclient/quantum/v2_0/port.py deleted file mode 100644 index 753df0ec3..000000000 --- a/quantumclient/quantum/v2_0/port.py +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import argparse -import logging - -from quantumclient.common import utils -from quantumclient.quantum import v2_0 as quantumv20 -from quantumclient.quantum.v2_0 import CreateCommand -from quantumclient.quantum.v2_0 import DeleteCommand -from quantumclient.quantum.v2_0 import ListCommand -from quantumclient.quantum.v2_0 import ShowCommand -from quantumclient.quantum.v2_0 import UpdateCommand - - -def _format_fixed_ips(port): - try: - return '\n'.join([utils.dumps(ip) for ip in port['fixed_ips']]) - except Exception: - return '' - - -class ListPort(ListCommand): - """List ports that belong to a given tenant.""" - - resource = 'port' - log = logging.getLogger(__name__ + '.ListPort') - _formatters = {'fixed_ips': _format_fixed_ips, } - list_columns = ['id', 'name', 'mac_address', 'fixed_ips'] - - -class ListRouterPort(ListCommand): - """List ports that belong to a given tenant, with specified router""" - - resource = 'port' - log = logging.getLogger(__name__ + '.ListRouterPort') - _formatters = {'fixed_ips': _format_fixed_ips, } - list_columns = ['id', 'name', 'mac_address', 'fixed_ips'] - - 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 router to look up') - return parser - - def get_data(self, parsed_args): - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - _id = quantumv20.find_resourceid_by_name_or_id( - quantum_client, 'router', parsed_args.id) - self.values_specs.append('--device_id=%s' % _id) - return super(ListRouterPort, self).get_data(parsed_args) - - -class ShowPort(ShowCommand): - """Show information of a given port.""" - - resource = 'port' - log = logging.getLogger(__name__ + '.ShowPort') - - -class CreatePort(CreateCommand): - """Create a port for a given tenant.""" - - resource = 'port' - log = logging.getLogger(__name__ + '.CreatePort') - - def add_known_arguments(self, parser): - parser.add_argument( - '--name', - help='name of this port') - parser.add_argument( - '--admin-state-down', - default=True, action='store_false', - help='set admin state up to false') - parser.add_argument( - '--admin_state_down', - 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( - '--device-id', - help='device id of this port') - parser.add_argument( - '--device_id', - help=argparse.SUPPRESS) - parser.add_argument( - '--fixed-ip', - action='append', - help='desired IP for this port: ' - 'subnet_id=,ip_address=, ' - '(This option can be repeated.)') - parser.add_argument( - '--fixed_ip', - action='append', - help=argparse.SUPPRESS) - parser.add_argument( - '--security-group', metavar='SECURITY_GROUP', - default=[], action='append', dest='security_groups', - help='security group associated with the port ' - '(This option can be repeated)') - parser.add_argument( - 'network_id', metavar='NETWORK', - help='Network id or name this port belongs to') - - def args2body(self, parsed_args): - _network_id = quantumv20.find_resourceid_by_name_or_id( - self.get_client(), 'network', parsed_args.network_id) - body = {'port': {'admin_state_up': parsed_args.admin_state_down, - 'network_id': _network_id, }, } - if parsed_args.mac_address: - body['port'].update({'mac_address': parsed_args.mac_address}) - if parsed_args.device_id: - body['port'].update({'device_id': parsed_args.device_id}) - if parsed_args.tenant_id: - body['port'].update({'tenant_id': parsed_args.tenant_id}) - if parsed_args.name: - body['port'].update({'name': parsed_args.name}) - ips = [] - if parsed_args.fixed_ip: - for ip_spec in parsed_args.fixed_ip: - ip_dict = utils.str2dict(ip_spec) - if 'subnet_id' in ip_dict: - subnet_name_id = ip_dict['subnet_id'] - _subnet_id = quantumv20.find_resourceid_by_name_or_id( - self.get_client(), 'subnet', subnet_name_id) - ip_dict['subnet_id'] = _subnet_id - ips.append(ip_dict) - if ips: - body['port'].update({'fixed_ips': ips}) - - _sgids = [] - for sg in parsed_args.security_groups: - _sgids.append(quantumv20.find_resourceid_by_name_or_id( - self.get_client(), 'security_group', sg)) - if _sgids: - body['port']['security_groups'] = _sgids - - return body - - -class DeletePort(DeleteCommand): - """Delete a given port.""" - - resource = 'port' - log = logging.getLogger(__name__ + '.DeletePort') - - -class UpdatePort(UpdateCommand): - """Update port's information.""" - - resource = 'port' - log = logging.getLogger(__name__ + '.UpdatePort') - - def add_known_arguments(self, parser): - parser.add_argument( - '--no-security-groups', - default=False, action='store_true', - help='remove security groups from port') - - def args2body(self, parsed_args): - body = {'port': {}} - if parsed_args.no_security_groups: - body['port'].update({'security_groups': None}) - return body diff --git a/quantumclient/quantum/v2_0/quota.py b/quantumclient/quantum/v2_0/quota.py deleted file mode 100644 index 0e749491e..000000000 --- a/quantumclient/quantum/v2_0/quota.py +++ /dev/null @@ -1,221 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import argparse -import logging - -from cliff import lister -from cliff import show - -from quantumclient.common import exceptions -from quantumclient.common import utils -from quantumclient.quantum import v2_0 as quantumv20 -from quantumclient.quantum.v2_0 import QuantumCommand - - -def get_tenant_id(tenant_id, client): - return (tenant_id if tenant_id else - client.get_quotas_tenant()['tenant']['tenant_id']) - - -class DeleteQuota(QuantumCommand): - """Delete defined quotas of a given tenant.""" - - api = 'network' - resource = 'quota' - log = logging.getLogger(__name__ + '.DeleteQuota') - - 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) - return parser - - def run(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - tenant_id = get_tenant_id(parsed_args.tenant_id, - quantum_client) - obj_deleter = getattr(quantum_client, - "delete_%s" % self.resource) - obj_deleter(tenant_id) - print >>self.app.stdout, (_('Deleted %(resource)s: %(tenant_id)s') - % {'tenant_id': tenant_id, - 'resource': self.resource}) - return - - -class ListQuota(QuantumCommand, lister.Lister): - """List defined quotas of all tenants.""" - - api = 'network' - resource = 'quota' - log = logging.getLogger(__name__ + '.ListQuota') - _formatters = None - - def get_parser(self, prog_name): - parser = super(ListQuota, self).get_parser(prog_name) - return parser - - def get_data(self, parsed_args): - self.log.debug('get_data(%s)' % parsed_args) - quantum_client = self.get_client() - search_opts = {} - self.log.debug('search options: %s', search_opts) - quantum_client.format = parsed_args.request_format - obj_lister = getattr(quantum_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 ShowQuota(QuantumCommand, show.ShowOne): - """Show quotas of a given tenant - - """ - api = 'network' - resource = "quota" - log = logging.getLogger(__name__ + '.ShowQuota') - - def get_parser(self, prog_name): - parser = super(ShowQuota, 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) - return parser - - def get_data(self, parsed_args): - self.log.debug('get_data(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - tenant_id = get_tenant_id(parsed_args.tenant_id, - quantum_client) - params = {} - obj_shower = getattr(quantum_client, - "show_%s" % self.resource) - data = obj_shower(tenant_id, **params) - if self.resource in data: - for k, v in data[self.resource].iteritems(): - if isinstance(v, list): - value = "" - for _item in v: - if value: - value += "\n" - if isinstance(_item, dict): - value += utils.dumps(_item) - else: - value += str(_item) - data[self.resource][k] = value - elif v is None: - data[self.resource][k] = '' - return zip(*sorted(data[self.resource].iteritems())) - else: - return None - - -class UpdateQuota(QuantumCommand, show.ShowOne): - """Define tenant's quotas not to use defaults.""" - - resource = 'quota' - log = logging.getLogger(__name__ + '.UpdateQuota') - - 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 network quota') - parser.add_argument( - '--subnet', metavar='subnets', - help='the limit of subnet quota') - parser.add_argument( - '--port', metavar='ports', - help='the limit of port quota') - parser.add_argument( - '--router', metavar='routers', - help='the limit of router quota') - parser.add_argument( - '--floatingip', metavar='floatingips', - help='the limit of floating IP quota') - quantumv20.add_extra_argument( - parser, 'value_specs', - 'new values for the %s' % self.resource) - 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.QuantumClientException(message=message) - return return_value - - def get_data(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - quota = {} - for resource in ('network', 'subnet', 'port', 'router', 'floatingip'): - if getattr(parsed_args, resource): - quota[resource] = self._validate_int( - resource, - getattr(parsed_args, resource)) - value_specs = parsed_args.value_specs - if value_specs: - quota.update(quantumv20.parse_args_to_dict(value_specs)) - obj_updator = getattr(quantum_client, - "update_%s" % self.resource) - tenant_id = get_tenant_id(parsed_args.tenant_id, - quantum_client) - data = obj_updator(tenant_id, {self.resource: quota}) - if self.resource in data: - for k, v in data[self.resource].iteritems(): - if isinstance(v, list): - value = "" - for _item in v: - if value: - value += "\n" - if isinstance(_item, dict): - value += utils.dumps(_item) - else: - value += str(_item) - data[self.resource][k] = value - elif v is None: - data[self.resource][k] = '' - return zip(*sorted(data[self.resource].iteritems())) - else: - return None diff --git a/quantumclient/quantum/v2_0/router.py b/quantumclient/quantum/v2_0/router.py deleted file mode 100644 index a0de0c835..000000000 --- a/quantumclient/quantum/v2_0/router.py +++ /dev/null @@ -1,205 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import argparse -import logging - -from quantumclient.common import utils -from quantumclient.quantum import v2_0 as quantumv20 -from quantumclient.quantum.v2_0 import CreateCommand -from quantumclient.quantum.v2_0 import DeleteCommand -from quantumclient.quantum.v2_0 import ListCommand -from quantumclient.quantum.v2_0 import QuantumCommand -from quantumclient.quantum.v2_0 import ShowCommand -from quantumclient.quantum.v2_0 import UpdateCommand - - -def _format_external_gateway_info(router): - try: - return utils.dumps(router['external_gateway_info']) - except Exception: - return '' - - -class ListRouter(ListCommand): - """List routers that belong to a given tenant.""" - - resource = 'router' - log = logging.getLogger(__name__ + '.ListRouter') - _formatters = {'external_gateway_info': _format_external_gateway_info, } - list_columns = ['id', 'name', 'external_gateway_info'] - - -class ShowRouter(ShowCommand): - """Show information of a given router.""" - - resource = 'router' - log = logging.getLogger(__name__ + '.ShowRouter') - - -class CreateRouter(CreateCommand): - """Create a router for a given tenant.""" - - resource = 'router' - log = logging.getLogger(__name__ + '.CreateRouter') - _formatters = {'external_gateway_info': _format_external_gateway_info, } - - 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( - '--admin_state_down', - action='store_false', - help=argparse.SUPPRESS) - parser.add_argument( - 'name', metavar='NAME', - help='Name of router to create') - - def args2body(self, parsed_args): - body = {'router': { - 'name': parsed_args.name, - 'admin_state_up': parsed_args.admin_state_down, }, } - if parsed_args.tenant_id: - body['router'].update({'tenant_id': parsed_args.tenant_id}) - return body - - -class DeleteRouter(DeleteCommand): - """Delete a given router.""" - - log = logging.getLogger(__name__ + '.DeleteRouter') - resource = 'router' - - -class UpdateRouter(UpdateCommand): - """Update router's information.""" - - log = logging.getLogger(__name__ + '.UpdateRouter') - resource = 'router' - - -class RouterInterfaceCommand(QuantumCommand): - """Based class to Add/Remove router interface.""" - - api = 'network' - log = logging.getLogger(__name__ + '.AddInterfaceRouter') - resource = 'router' - - def get_parser(self, prog_name): - parser = super(RouterInterfaceCommand, self).get_parser(prog_name) - parser.add_argument( - 'router_id', metavar='router-id', - help='ID of the router') - parser.add_argument( - 'subnet_id', metavar='subnet-id', - help='ID of the internal subnet for the interface') - return parser - - -class AddInterfaceRouter(RouterInterfaceCommand): - """Add an internal network interface to a router.""" - - def run(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - #TODO(danwent): handle passing in port-id - _router_id = quantumv20.find_resourceid_by_name_or_id( - quantum_client, self.resource, parsed_args.router_id) - _subnet_id = quantumv20.find_resourceid_by_name_or_id( - quantum_client, 'subnet', parsed_args.subnet_id) - quantum_client.add_interface_router(_router_id, - {'subnet_id': _subnet_id}) - #TODO(danwent): print port ID that is added - print >>self.app.stdout, ( - _('Added interface to router %s') % parsed_args.router_id) - - -class RemoveInterfaceRouter(RouterInterfaceCommand): - """Remove an internal network interface from a router.""" - - def run(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - #TODO(danwent): handle passing in port-id - _router_id = quantumv20.find_resourceid_by_name_or_id( - quantum_client, self.resource, parsed_args.router_id) - _subnet_id = quantumv20.find_resourceid_by_name_or_id( - quantum_client, 'subnet', parsed_args.subnet_id) - quantum_client.remove_interface_router(_router_id, - {'subnet_id': _subnet_id}) - print >>self.app.stdout, ( - _('Removed interface from router %s') % parsed_args.router_id) - - -class SetGatewayRouter(QuantumCommand): - """Set the external network gateway for a router.""" - - log = logging.getLogger(__name__ + '.SetGatewayRouter') - api = 'network' - resource = 'router' - - def get_parser(self, prog_name): - parser = super(SetGatewayRouter, self).get_parser(prog_name) - parser.add_argument( - 'router_id', metavar='router-id', - help='ID of the router') - parser.add_argument( - 'external_network_id', metavar='external-network-id', - help='ID of the external network for the gateway') - return parser - - def run(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - _router_id = quantumv20.find_resourceid_by_name_or_id( - quantum_client, self.resource, parsed_args.router_id) - _ext_net_id = quantumv20.find_resourceid_by_name_or_id( - quantum_client, 'network', parsed_args.external_network_id) - quantum_client.add_gateway_router(_router_id, - {'network_id': _ext_net_id}) - print >>self.app.stdout, ( - _('Set gateway for router %s') % parsed_args.router_id) - - -class RemoveGatewayRouter(QuantumCommand): - """Remove an external network gateway from a router.""" - - log = logging.getLogger(__name__ + '.RemoveGatewayRouter') - api = 'network' - resource = 'router' - - def get_parser(self, prog_name): - parser = super(RemoveGatewayRouter, self).get_parser(prog_name) - parser.add_argument( - 'router_id', metavar='router-id', - help='ID of the router') - return parser - - def run(self, parsed_args): - self.log.debug('run(%s)' % parsed_args) - quantum_client = self.get_client() - quantum_client.format = parsed_args.request_format - _router_id = quantumv20.find_resourceid_by_name_or_id( - quantum_client, self.resource, parsed_args.router_id) - quantum_client.remove_gateway_router(_router_id) - print >>self.app.stdout, ( - _('Removed gateway from router %s') % parsed_args.router_id) diff --git a/quantumclient/quantum/v2_0/securitygroup.py b/quantumclient/quantum/v2_0/securitygroup.py deleted file mode 100644 index 9442cccda..000000000 --- a/quantumclient/quantum/v2_0/securitygroup.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import argparse -import logging - -from quantumclient.quantum import v2_0 as quantumv20 - - -class ListSecurityGroup(quantumv20.ListCommand): - """List security groups that belong to a given tenant.""" - - resource = 'security_group' - log = logging.getLogger(__name__ + '.ListSecurityGroup') - _formatters = {} - list_columns = ['id', 'name', 'description'] - - -class ShowSecurityGroup(quantumv20.ShowCommand): - """Show information of a given security group.""" - - resource = 'security_group' - log = logging.getLogger(__name__ + '.ShowSecurityGroup') - allow_names = True - - -class CreateSecurityGroup(quantumv20.CreateCommand): - """Create a security group.""" - - resource = 'security_group' - log = logging.getLogger(__name__ + '.CreateSecurityGroup') - - def add_known_arguments(self, parser): - parser.add_argument( - 'name', metavar='NAME', - help='Name of security group') - parser.add_argument( - '--description', - help='description of security group') - - def args2body(self, parsed_args): - body = {'security_group': { - 'name': parsed_args.name}} - if parsed_args.description: - body['security_group'].update( - {'description': parsed_args.description}) - if parsed_args.tenant_id: - body['security_group'].update({'tenant_id': parsed_args.tenant_id}) - return body - - -class DeleteSecurityGroup(quantumv20.DeleteCommand): - """Delete a given security group.""" - - log = logging.getLogger(__name__ + '.DeleteSecurityGroup') - resource = 'security_group' - allow_names = True - - -class ListSecurityGroupRule(quantumv20.ListCommand): - """List security group rules that belong to a given tenant.""" - - resource = 'security_group_rule' - log = logging.getLogger(__name__ + '.ListSecurityGroupRule') - _formatters = {} - list_columns = ['id', 'security_group_id', 'direction', 'protocol', - 'source_ip_prefix', 'source_group_id'] - replace_rules = {'security_group_id': 'security_group', - 'source_group_id': 'source_group'} - - 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 retrieve_list(self, parsed_args): - parsed_args.fields = self.replace_columns(parsed_args.fields, - self.replace_rules, - reverse=True) - return super(ListSecurityGroupRule, self).retrieve_list(parsed_args) - - def extend_list(self, data, parsed_args): - if parsed_args.no_nameconv: - return - quantum_client = self.get_client() - search_opts = {'fields': ['id', 'name']} - secgroups = quantum_client.list_security_groups(**search_opts) - secgroups = secgroups.get('security_groups', []) - sg_dict = dict([(sg['id'], sg['name']) - for sg in secgroups if sg['name']]) - for rule in data: - for key in self.replace_rules: - rule[key] = sg_dict.get(rule[key], rule[key]) - - def setup_columns(self, info, parsed_args): - 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: - cols = self.replace_columns(info[0], self.replace_rules) - parsed_args.columns = cols - return (cols, info[1]) - - -class ShowSecurityGroupRule(quantumv20.ShowCommand): - """Show information of a given security group rule.""" - - resource = 'security_group_rule' - log = logging.getLogger(__name__ + '.ShowSecurityGroupRule') - allow_names = False - - -class CreateSecurityGroupRule(quantumv20.CreateCommand): - """Create a security group rule.""" - - resource = 'security_group_rule' - log = logging.getLogger(__name__ + '.CreateSecurityGroupRule') - - def add_known_arguments(self, parser): - parser.add_argument( - 'security_group_id', metavar='SECURITY_GROUP', - help='Security group name or id to add rule.') - parser.add_argument( - '--direction', - default='ingress', choices=['ingress', 'egress'], - help='direction of traffic: ingress/egress') - parser.add_argument( - '--ethertype', - default='IPv4', - help='IPv4/IPv6') - parser.add_argument( - '--protocol', - help='protocol of packet') - parser.add_argument( - '--port-range-min', - help='starting port range') - parser.add_argument( - '--port_range_min', - help=argparse.SUPPRESS) - parser.add_argument( - '--port-range-max', - help='ending port range') - parser.add_argument( - '--port_range_max', - help=argparse.SUPPRESS) - parser.add_argument( - '--source-ip-prefix', - help='cidr to match on') - parser.add_argument( - '--source_ip_prefix', - help=argparse.SUPPRESS) - parser.add_argument( - '--source-group-id', metavar='SOURCE_GROUP', - help='source security group name or id to apply rule') - parser.add_argument( - '--source_group_id', - help=argparse.SUPPRESS) - - def args2body(self, parsed_args): - _security_group_id = quantumv20.find_resourceid_by_name_or_id( - self.get_client(), 'security_group', parsed_args.security_group_id) - body = {'security_group_rule': { - 'security_group_id': _security_group_id, - 'direction': parsed_args.direction, - 'ethertype': parsed_args.ethertype}} - if parsed_args.protocol: - body['security_group_rule'].update( - {'protocol': parsed_args.protocol}) - if parsed_args.port_range_min: - body['security_group_rule'].update( - {'port_range_min': parsed_args.port_range_min}) - if parsed_args.port_range_max: - body['security_group_rule'].update( - {'port_range_max': parsed_args.port_range_max}) - if parsed_args.source_ip_prefix: - body['security_group_rule'].update( - {'source_ip_prefix': parsed_args.source_ip_prefix}) - if parsed_args.source_group_id: - _source_group_id = quantumv20.find_resourceid_by_name_or_id( - self.get_client(), 'security_group', - parsed_args.source_group_id) - body['security_group_rule'].update( - {'source_group_id': _source_group_id}) - if parsed_args.tenant_id: - body['security_group_rule'].update( - {'tenant_id': parsed_args.tenant_id}) - return body - - -class DeleteSecurityGroupRule(quantumv20.DeleteCommand): - """Delete a given security group rule.""" - - log = logging.getLogger(__name__ + '.DeleteSecurityGroupRule') - resource = 'security_group_rule' - allow_names = False diff --git a/quantumclient/quantum/v2_0/subnet.py b/quantumclient/quantum/v2_0/subnet.py deleted file mode 100644 index 9339ff58b..000000000 --- a/quantumclient/quantum/v2_0/subnet.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import argparse -import logging - -from quantumclient.common import exceptions -from quantumclient.common import utils -from quantumclient.quantum import v2_0 as quantumv20 -from quantumclient.quantum.v2_0 import CreateCommand -from quantumclient.quantum.v2_0 import DeleteCommand -from quantumclient.quantum.v2_0 import ListCommand -from quantumclient.quantum.v2_0 import ShowCommand -from quantumclient.quantum.v2_0 import UpdateCommand - - -def _format_allocation_pools(subnet): - try: - return '\n'.join([utils.dumps(pool) for pool in - subnet['allocation_pools']]) - except Exception: - return '' - - -def _format_dns_nameservers(subnet): - try: - return '\n'.join([utils.dumps(server) for server in - subnet['dns_nameservers']]) - except Exception: - return '' - - -def _format_host_routes(subnet): - try: - return '\n'.join([utils.dumps(route) for route in - subnet['host_routes']]) - except Exception: - return '' - - -class ListSubnet(ListCommand): - """List networks that belong to a given tenant.""" - - resource = 'subnet' - log = logging.getLogger(__name__ + '.ListSubnet') - _formatters = {'allocation_pools': _format_allocation_pools, - 'dns_nameservers': _format_dns_nameservers, - 'host_routes': _format_host_routes, } - list_columns = ['id', 'name', 'cidr', 'allocation_pools'] - - -class ShowSubnet(ShowCommand): - """Show information of a given subnet.""" - - resource = 'subnet' - log = logging.getLogger(__name__ + '.ShowSubnet') - - -class CreateSubnet(CreateCommand): - """Create a subnet for a given tenant.""" - - resource = 'subnet' - log = logging.getLogger(__name__ + '.CreateSubnet') - - def add_known_arguments(self, parser): - parser.add_argument( - '--name', - help='name of this subnet') - parser.add_argument( - '--ip-version', - type=int, - default=4, choices=[4, 6], - help='IP version with default 4') - parser.add_argument( - '--ip_version', - type=int, - choices=[4, 6], - help=argparse.SUPPRESS) - parser.add_argument( - '--gateway', metavar='GATEWAY_IP', - help='gateway ip of this subnet') - parser.add_argument( - '--no-gateway', - default=False, action='store_true', - help='No distribution of gateway') - parser.add_argument( - '--allocation-pool', metavar='start=IP_ADDR,end=IP_ADDR', - action='append', dest='allocation_pools', type=utils.str2dict, - 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, - help=argparse.SUPPRESS) - parser.add_argument( - '--host-route', metavar='destination=CIDR,nexthop=IP_ADDR', - action='append', dest='host_routes', type=utils.str2dict, - 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( - 'network_id', metavar='NETWORK', - help='network id or name this subnet belongs to') - parser.add_argument( - 'cidr', metavar='CIDR', - help='cidr of subnet to create') - - def args2body(self, parsed_args): - _network_id = quantumv20.find_resourceid_by_name_or_id( - self.get_client(), 'network', parsed_args.network_id) - body = {'subnet': {'cidr': parsed_args.cidr, - 'network_id': _network_id, - 'ip_version': parsed_args.ip_version, }, } - - if parsed_args.gateway and parsed_args.no_gateway: - raise exceptions.CommandError("--gateway option and " - "--no-gateway option can " - "not be used same time") - if parsed_args.no_gateway: - body['subnet'].update({'gateway_ip': None}) - if parsed_args.gateway: - body['subnet'].update({'gateway_ip': parsed_args.gateway}) - if parsed_args.tenant_id: - body['subnet'].update({'tenant_id': parsed_args.tenant_id}) - if parsed_args.name: - body['subnet'].update({'name': parsed_args.name}) - if parsed_args.disable_dhcp: - body['subnet'].update({'enable_dhcp': False}) - if parsed_args.allocation_pools: - body['subnet']['allocation_pools'] = parsed_args.allocation_pools - if parsed_args.host_routes: - body['subnet']['host_routes'] = parsed_args.host_routes - if parsed_args.dns_nameservers: - body['subnet']['dns_nameservers'] = parsed_args.dns_nameservers - - return body - - -class DeleteSubnet(DeleteCommand): - """Delete a given subnet.""" - - resource = 'subnet' - log = logging.getLogger(__name__ + '.DeleteSubnet') - - -class UpdateSubnet(UpdateCommand): - """Update subnet's information.""" - - resource = 'subnet' - log = logging.getLogger(__name__ + '.UpdateSubnet') diff --git a/quantumclient/shell.py b/quantumclient/shell.py deleted file mode 100644 index 4b22e9837..000000000 --- a/quantumclient/shell.py +++ /dev/null @@ -1,605 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -""" -Command-line interface to the Quantum APIs -""" - -import argparse -import gettext -import logging -import os -import sys - -from cliff.app import App -from cliff.commandmanager import CommandManager - -from quantumclient.common import clientmanager -from quantumclient.common import exceptions as exc -from quantumclient.common import utils - - -VERSION = '2.0' -QUANTUM_API_VERSION = '2.0' - - -def run_command(cmd, cmd_parser, sub_argv): - _argv = sub_argv - index = -1 - values_specs = [] - if '--' in sub_argv: - index = sub_argv.index('--') - _argv = sub_argv[:index] - values_specs = sub_argv[index:] - known_args, _values_specs = cmd_parser.parse_known_args(_argv) - cmd.values_specs = (index == -1 and _values_specs or values_specs) - return cmd.run(known_args) - - -def env(*_vars, **kwargs): - """Search for the first defined of possibly many env vars - - Returns the first environment variable defined in vars, or - returns the default defined in kwargs. - - """ - for v in _vars: - value = os.environ.get(v, None) - if value: - return value - return kwargs.get('default', '') - - -COMMAND_V2 = { - 'net-list': utils.import_class( - 'quantumclient.quantum.v2_0.network.ListNetwork'), - 'net-external-list': utils.import_class( - 'quantumclient.quantum.v2_0.network.ListExternalNetwork'), - 'net-show': utils.import_class( - 'quantumclient.quantum.v2_0.network.ShowNetwork'), - 'net-create': utils.import_class( - 'quantumclient.quantum.v2_0.network.CreateNetwork'), - 'net-delete': utils.import_class( - 'quantumclient.quantum.v2_0.network.DeleteNetwork'), - 'net-update': utils.import_class( - 'quantumclient.quantum.v2_0.network.UpdateNetwork'), - 'subnet-list': utils.import_class( - 'quantumclient.quantum.v2_0.subnet.ListSubnet'), - 'subnet-show': utils.import_class( - 'quantumclient.quantum.v2_0.subnet.ShowSubnet'), - 'subnet-create': utils.import_class( - 'quantumclient.quantum.v2_0.subnet.CreateSubnet'), - 'subnet-delete': utils.import_class( - 'quantumclient.quantum.v2_0.subnet.DeleteSubnet'), - 'subnet-update': utils.import_class( - 'quantumclient.quantum.v2_0.subnet.UpdateSubnet'), - 'port-list': utils.import_class( - 'quantumclient.quantum.v2_0.port.ListPort'), - 'port-show': utils.import_class( - 'quantumclient.quantum.v2_0.port.ShowPort'), - 'port-create': utils.import_class( - 'quantumclient.quantum.v2_0.port.CreatePort'), - 'port-delete': utils.import_class( - 'quantumclient.quantum.v2_0.port.DeletePort'), - 'port-update': utils.import_class( - 'quantumclient.quantum.v2_0.port.UpdatePort'), - 'quota-list': utils.import_class( - 'quantumclient.quantum.v2_0.quota.ListQuota'), - 'quota-show': utils.import_class( - 'quantumclient.quantum.v2_0.quota.ShowQuota'), - 'quota-delete': utils.import_class( - 'quantumclient.quantum.v2_0.quota.DeleteQuota'), - 'quota-update': utils.import_class( - 'quantumclient.quantum.v2_0.quota.UpdateQuota'), - 'ext-list': utils.import_class( - 'quantumclient.quantum.v2_0.extension.ListExt'), - 'ext-show': utils.import_class( - 'quantumclient.quantum.v2_0.extension.ShowExt'), - 'router-list': utils.import_class( - 'quantumclient.quantum.v2_0.router.ListRouter'), - 'router-port-list': utils.import_class( - 'quantumclient.quantum.v2_0.port.ListRouterPort'), - 'router-show': utils.import_class( - 'quantumclient.quantum.v2_0.router.ShowRouter'), - 'router-create': utils.import_class( - 'quantumclient.quantum.v2_0.router.CreateRouter'), - 'router-delete': utils.import_class( - 'quantumclient.quantum.v2_0.router.DeleteRouter'), - 'router-update': utils.import_class( - 'quantumclient.quantum.v2_0.router.UpdateRouter'), - 'router-interface-add': utils.import_class( - 'quantumclient.quantum.v2_0.router.AddInterfaceRouter'), - 'router-interface-delete': utils.import_class( - 'quantumclient.quantum.v2_0.router.RemoveInterfaceRouter'), - 'router-gateway-set': utils.import_class( - 'quantumclient.quantum.v2_0.router.SetGatewayRouter'), - 'router-gateway-clear': utils.import_class( - 'quantumclient.quantum.v2_0.router.RemoveGatewayRouter'), - 'floatingip-list': utils.import_class( - 'quantumclient.quantum.v2_0.floatingip.ListFloatingIP'), - 'floatingip-show': utils.import_class( - 'quantumclient.quantum.v2_0.floatingip.ShowFloatingIP'), - 'floatingip-create': utils.import_class( - 'quantumclient.quantum.v2_0.floatingip.CreateFloatingIP'), - 'floatingip-delete': utils.import_class( - 'quantumclient.quantum.v2_0.floatingip.DeleteFloatingIP'), - 'floatingip-associate': utils.import_class( - 'quantumclient.quantum.v2_0.floatingip.AssociateFloatingIP'), - 'floatingip-disassociate': utils.import_class( - 'quantumclient.quantum.v2_0.floatingip.DisassociateFloatingIP'), - 'security-group-list': utils.import_class( - 'quantumclient.quantum.v2_0.securitygroup.ListSecurityGroup'), - 'security-group-show': utils.import_class( - 'quantumclient.quantum.v2_0.securitygroup.ShowSecurityGroup'), - 'security-group-create': utils.import_class( - 'quantumclient.quantum.v2_0.securitygroup.CreateSecurityGroup'), - 'security-group-delete': utils.import_class( - 'quantumclient.quantum.v2_0.securitygroup.DeleteSecurityGroup'), - 'security-group-rule-list': utils.import_class( - 'quantumclient.quantum.v2_0.securitygroup.ListSecurityGroupRule'), - 'security-group-rule-show': utils.import_class( - 'quantumclient.quantum.v2_0.securitygroup.ShowSecurityGroupRule'), - 'security-group-rule-create': utils.import_class( - 'quantumclient.quantum.v2_0.securitygroup.CreateSecurityGroupRule'), - 'security-group-rule-delete': utils.import_class( - 'quantumclient.quantum.v2_0.securitygroup.DeleteSecurityGroupRule'), - 'lb-vip-list': utils.import_class( - 'quantumclient.quantum.v2_0.lb.vip.ListVip'), - 'lb-vip-show': utils.import_class( - 'quantumclient.quantum.v2_0.lb.vip.ShowVip'), - 'lb-vip-create': utils.import_class( - 'quantumclient.quantum.v2_0.lb.vip.CreateVip'), - 'lb-vip-update': utils.import_class( - 'quantumclient.quantum.v2_0.lb.vip.UpdateVip'), - 'lb-vip-delete': utils.import_class( - 'quantumclient.quantum.v2_0.lb.vip.DeleteVip'), - 'lb-pool-list': utils.import_class( - 'quantumclient.quantum.v2_0.lb.pool.ListPool'), - 'lb-pool-show': utils.import_class( - 'quantumclient.quantum.v2_0.lb.pool.ShowPool'), - 'lb-pool-create': utils.import_class( - 'quantumclient.quantum.v2_0.lb.pool.CreatePool'), - 'lb-pool-update': utils.import_class( - 'quantumclient.quantum.v2_0.lb.pool.UpdatePool'), - 'lb-pool-delete': utils.import_class( - 'quantumclient.quantum.v2_0.lb.pool.DeletePool'), - 'lb-pool-stats': utils.import_class( - 'quantumclient.quantum.v2_0.lb.pool.RetrievePoolStats'), - 'lb-member-list': utils.import_class( - 'quantumclient.quantum.v2_0.lb.member.ListMember'), - 'lb-member-show': utils.import_class( - 'quantumclient.quantum.v2_0.lb.member.ShowMember'), - 'lb-member-create': utils.import_class( - 'quantumclient.quantum.v2_0.lb.member.CreateMember'), - 'lb-member-update': utils.import_class( - 'quantumclient.quantum.v2_0.lb.member.UpdateMember'), - 'lb-member-delete': utils.import_class( - 'quantumclient.quantum.v2_0.lb.member.DeleteMember'), - 'lb-healthmonitor-list': utils.import_class( - 'quantumclient.quantum.v2_0.lb.healthmonitor.ListHealthMonitor'), - 'lb-healthmonitor-show': utils.import_class( - 'quantumclient.quantum.v2_0.lb.healthmonitor.ShowHealthMonitor'), - 'lb-healthmonitor-create': utils.import_class( - 'quantumclient.quantum.v2_0.lb.healthmonitor.CreateHealthMonitor'), - 'lb-healthmonitor-update': utils.import_class( - 'quantumclient.quantum.v2_0.lb.healthmonitor.UpdateHealthMonitor'), - 'lb-healthmonitor-delete': utils.import_class( - 'quantumclient.quantum.v2_0.lb.healthmonitor.DeleteHealthMonitor'), - 'lb-healthmonitor-associate': utils.import_class( - 'quantumclient.quantum.v2_0.lb.healthmonitor.AssociateHealthMonitor'), - 'lb-healthmonitor-disassociate': utils.import_class( - 'quantumclient.quantum.v2_0.lb.healthmonitor' - '.DisassociateHealthMonitor'), - 'queue-create': utils.import_class( - 'quantumclient.quantum.v2_0.nvp_qos_queue.CreateQoSQueue'), - 'queue-delete': utils.import_class( - 'quantumclient.quantum.v2_0.nvp_qos_queue.DeleteQoSQueue'), - 'queue-show': utils.import_class( - 'quantumclient.quantum.v2_0.nvp_qos_queue.ShowQoSQueue'), - 'queue-list': utils.import_class( - 'quantumclient.quantum.v2_0.nvp_qos_queue.ListQoSQueue'), - 'agent-list': utils.import_class( - 'quantumclient.quantum.v2_0.agent.ListAgent'), - 'agent-show': utils.import_class( - 'quantumclient.quantum.v2_0.agent.ShowAgent'), - 'agent-delete': utils.import_class( - 'quantumclient.quantum.v2_0.agent.DeleteAgent'), - 'agent-update': utils.import_class( - 'quantumclient.quantum.v2_0.agent.UpdateAgent'), -} - -COMMANDS = {'2.0': COMMAND_V2} - - -class HelpAction(argparse.Action): - """Provide a custom action so the -h and --help options - to the main app will print a list of the commands. - - The commands are determined by checking the CommandManager - instance, passed in as the "default" value for the action. - """ - def __call__(self, parser, namespace, values, option_string=None): - outputs = [] - max_len = 0 - app = self.default - parser.print_help(app.stdout) - app.stdout.write('\nCommands for API v%s:\n' % app.api_version) - command_manager = app.command_manager - for name, ep in sorted(command_manager): - factory = ep.load() - cmd = factory(self, None) - one_liner = cmd.get_description().split('\n')[0] - outputs.append((name, one_liner)) - max_len = max(len(name), max_len) - for (name, one_liner) in outputs: - app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner)) - sys.exit(0) - - -class QuantumShell(App): - - CONSOLE_MESSAGE_FORMAT = '%(message)s' - DEBUG_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' - log = logging.getLogger(__name__) - - def __init__(self, apiversion): - super(QuantumShell, self).__init__( - description=__doc__.strip(), - version=VERSION, - command_manager=CommandManager('quantum.cli'), ) - self.commands = COMMANDS - for k, v in self.commands[apiversion].items(): - self.command_manager.add_command(k, v) - - # This is instantiated in initialize_app() only when using - # password flow auth - self.auth_client = None - self.api_version = apiversion - - def build_option_parser(self, description, version): - """Return an argparse option parser for this application. - - Subclasses may override this method to extend - the parser with more global options. - - :param description: full description of the application - :paramtype description: str - :param version: version number for the application - :paramtype version: str - """ - parser = argparse.ArgumentParser( - description=description, - add_help=False, ) - parser.add_argument( - '--version', - action='version', - version='%(prog)s {0}'.format(version), ) - parser.add_argument( - '-v', '--verbose', - action='count', - dest='verbose_level', - default=self.DEFAULT_VERBOSE_LEVEL, - help='Increase verbosity of output. Can be repeated.', ) - parser.add_argument( - '-q', '--quiet', - action='store_const', - dest='verbose_level', - const=0, - help='suppress output except warnings and errors', ) - parser.add_argument( - '-h', '--help', - action=HelpAction, - nargs=0, - default=self, # tricky - help="show this help message and exit", ) - parser.add_argument( - '--debug', - default=False, - action='store_true', - help='show tracebacks on errors', ) - # Global arguments - parser.add_argument( - '--os-auth-strategy', metavar='', - default=env('OS_AUTH_STRATEGY', default='keystone'), - help='Authentication strategy (Env: OS_AUTH_STRATEGY' - ', default keystone). For now, any other value will' - ' disable the authentication') - parser.add_argument( - '--os_auth_strategy', - help=argparse.SUPPRESS) - - parser.add_argument( - '--os-auth-url', metavar='', - default=env('OS_AUTH_URL'), - help='Authentication URL (Env: OS_AUTH_URL)') - parser.add_argument( - '--os_auth_url', - help=argparse.SUPPRESS) - - parser.add_argument( - '--os-tenant-name', metavar='', - default=env('OS_TENANT_NAME'), - help='Authentication tenant name (Env: OS_TENANT_NAME)') - parser.add_argument( - '--os_tenant_name', - help=argparse.SUPPRESS) - - parser.add_argument( - '--os-username', metavar='', - default=utils.env('OS_USERNAME'), - help='Authentication username (Env: OS_USERNAME)') - parser.add_argument( - '--os_username', - help=argparse.SUPPRESS) - - parser.add_argument( - '--os-password', metavar='', - default=utils.env('OS_PASSWORD'), - help='Authentication password (Env: OS_PASSWORD)') - parser.add_argument( - '--os_password', - help=argparse.SUPPRESS) - - parser.add_argument( - '--os-region-name', metavar='', - default=env('OS_REGION_NAME'), - help='Authentication region name (Env: OS_REGION_NAME)') - parser.add_argument( - '--os_region_name', - help=argparse.SUPPRESS) - - parser.add_argument( - '--os-token', metavar='', - default=env('OS_TOKEN'), - help='Defaults to env[OS_TOKEN]') - parser.add_argument( - '--os_token', - help=argparse.SUPPRESS) - - parser.add_argument( - '--os-url', metavar='', - default=env('OS_URL'), - help='Defaults to env[OS_URL]') - parser.add_argument( - '--os_url', - help=argparse.SUPPRESS) - - return parser - - def _bash_completion(self): - """ - Prints all of the commands and options to stdout so that the - quantum's bash-completion script doesn't have to hard code them. - """ - commands = set() - options = set() - for option, _action in self.parser._option_string_actions.items(): - options.add(option) - for command_name, command in self.command_manager: - commands.add(command_name) - cmd_factory = command.load() - cmd = cmd_factory(self, None) - cmd_parser = cmd.get_parser('') - for option, _action in cmd_parser._option_string_actions.items(): - options.add(option) - print ' '.join(commands | options) - - def run(self, argv): - """Equivalent to the main program for the application. - - :param argv: input arguments and options - :paramtype argv: list of str - """ - try: - index = 0 - command_pos = -1 - help_pos = -1 - help_command_pos = -1 - for arg in argv: - if arg == 'bash-completion': - self._bash_completion() - return 0 - if arg in self.commands[self.api_version]: - if command_pos == -1: - command_pos = index - elif arg in ('-h', '--help'): - if help_pos == -1: - help_pos = index - elif arg == 'help': - if help_command_pos == -1: - help_command_pos = index - index = index + 1 - if command_pos > -1 and help_pos > command_pos: - argv = ['help', argv[command_pos]] - if help_command_pos > -1 and command_pos == -1: - argv[help_command_pos] = '--help' - self.options, remainder = self.parser.parse_known_args(argv) - self.configure_logging() - self.interactive_mode = not remainder - self.initialize_app(remainder) - except Exception as err: - if self.options.debug: - self.log.exception(err) - raise - else: - self.log.error(err) - return 1 - result = 1 - if self.interactive_mode: - _argv = [sys.argv[0]] - sys.argv = _argv - result = self.interact() - else: - result = self.run_subcommand(remainder) - return result - - def run_subcommand(self, argv): - subcommand = self.command_manager.find_command(argv) - cmd_factory, cmd_name, sub_argv = subcommand - cmd = cmd_factory(self, self.options) - err = None - result = 1 - try: - self.prepare_to_run_command(cmd) - full_name = (cmd_name - if self.interactive_mode - else ' '.join([self.NAME, cmd_name]) - ) - cmd_parser = cmd.get_parser(full_name) - return run_command(cmd, cmd_parser, sub_argv) - except Exception as err: - if self.options.debug: - self.log.exception(err) - else: - self.log.error(err) - try: - self.clean_up(cmd, result, err) - except Exception as err2: - if self.options.debug: - self.log.exception(err2) - else: - self.log.error('Could not clean up: %s', err2) - if self.options.debug: - raise - else: - try: - self.clean_up(cmd, result, None) - except Exception as err3: - if self.options.debug: - self.log.exception(err3) - else: - self.log.error('Could not clean up: %s', err3) - return result - - def authenticate_user(self): - """Make sure the user has provided all of the authentication - info we need. - """ - if self.options.os_auth_strategy == 'keystone': - if self.options.os_token or self.options.os_url: - # Token flow auth takes priority - if not self.options.os_token: - raise exc.CommandError( - "You must provide a token via" - " either --os-token or env[OS_TOKEN]") - - if not self.options.os_url: - raise exc.CommandError( - "You must provide a service URL via" - " either --os-url or env[OS_URL]") - - else: - # Validate password flow auth - if not self.options.os_username: - raise exc.CommandError( - "You must provide a username via" - " either --os-username or env[OS_USERNAME]") - - if not self.options.os_password: - raise exc.CommandError( - "You must provide a password via" - " either --os-password or env[OS_PASSWORD]") - - if not (self.options.os_tenant_name): - raise exc.CommandError( - "You must provide a tenant_name via" - " either --os-tenant-name or via env[OS_TENANT_NAME]") - - if not self.options.os_auth_url: - raise exc.CommandError( - "You must provide an auth url via" - " either --os-auth-url or via env[OS_AUTH_URL]") - else: # not keystone - if not self.options.os_url: - raise exc.CommandError( - "You must provide a service URL via" - " either --os-url or env[OS_URL]") - - self.client_manager = clientmanager.ClientManager( - token=self.options.os_token, - url=self.options.os_url, - auth_url=self.options.os_auth_url, - tenant_name=self.options.os_tenant_name, - username=self.options.os_username, - password=self.options.os_password, - region_name=self.options.os_region_name, - api_version=self.api_version, - auth_strategy=self.options.os_auth_strategy, ) - return - - def initialize_app(self, argv): - """Global app init bits: - - * set up API versions - * validate authentication info - """ - - super(QuantumShell, self).initialize_app(argv) - - self.api_version = {'network': self.api_version} - - # If the user is not asking for help, make sure they - # have given us auth. - cmd_name = None - if argv: - cmd_info = self.command_manager.find_command(argv) - cmd_factory, cmd_name, sub_argv = cmd_info - if self.interactive_mode or cmd_name != 'help': - self.authenticate_user() - - def clean_up(self, cmd, result, err): - self.log.debug('clean_up %s', cmd.__class__.__name__) - if err: - self.log.debug('got an error: %s', err) - - def configure_logging(self): - """Create logging handlers for any log output. - """ - root_logger = logging.getLogger('') - - # Set up logging to a file - root_logger.setLevel(logging.DEBUG) - - # Send higher-level messages to the console via stderr - console = logging.StreamHandler(self.stderr) - console_level = {0: logging.WARNING, - 1: logging.INFO, - 2: logging.DEBUG, - }.get(self.options.verbose_level, logging.DEBUG) - console.setLevel(console_level) - if logging.DEBUG == console_level: - formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT) - else: - formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT) - console.setFormatter(formatter) - root_logger.addHandler(console) - return - - -def main(argv=sys.argv[1:]): - gettext.install('quantumclient', unicode=1) - try: - return QuantumShell(QUANTUM_API_VERSION).run(argv) - except exc.QuantumClientException: - return 1 - except Exception as e: - print e - return 1 - - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/quantumclient/tests/unit/lb/test_cli20_healthmonitor.py b/quantumclient/tests/unit/lb/test_cli20_healthmonitor.py deleted file mode 100644 index 657b24e73..000000000 --- a/quantumclient/tests/unit/lb/test_cli20_healthmonitor.py +++ /dev/null @@ -1,189 +0,0 @@ -# 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. -# -# @author: Ilya Shakhat, Mirantis Inc. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import sys - -from mox import ContainsKeyValue - -from quantumclient.quantum.v2_0.lb import healthmonitor -from quantumclient.tests.unit import test_cli20 - - -class CLITestV20LbHealthmonitor(test_cli20.CLITestV20Base): - def test_create_healthmonitor_with_all_params(self): - """lb-healthmonitor-create with mandatory params only""" - resource = 'health_monitor' - cmd = healthmonitor.CreateHealthMonitor(test_cli20.MyApp(sys.stdout), - None) - delay = '60' - max_retries = '2' - timeout = '10' - type = 'tcp' - tenant_id = 'my-tenant' - my_id = 'my-id' - args = ['--admin-state-down', - '--delay', delay, - '--max-retries', max_retries, - '--timeout', timeout, - '--type', type, - '--tenant-id', tenant_id] - position_names = ['admin_state_up', 'delay', 'max_retries', 'timeout', - 'type', 'tenant_id'] - position_values = [True, delay, max_retries, timeout, type, - tenant_id] - self._test_create_resource(resource, cmd, '', my_id, args, - position_names, position_values) - - def test_create_healthmonitor_with_all_params(self): - """lb-healthmonitor-create with all params set""" - resource = 'health_monitor' - cmd = healthmonitor.CreateHealthMonitor(test_cli20.MyApp(sys.stdout), - None) - admin_state_up = False - delay = '60' - expected_codes = '200-202,204' - http_method = 'HEAD' - max_retries = '2' - timeout = '10' - type = 'tcp' - tenant_id = 'my-tenant' - url_path = '/health' - my_id = 'my-id' - args = ['--admin-state-down', - '--delay', delay, - '--expected-codes', expected_codes, - '--http-method', http_method, - '--max-retries', max_retries, - '--timeout', timeout, - '--type', type, - '--tenant-id', tenant_id, - '--url-path', url_path] - position_names = ['admin_state_up', 'delay', - 'expected_codes', 'http_method', - 'max_retries', 'timeout', - 'type', 'tenant_id', 'url_path'] - position_values = [admin_state_up, delay, - expected_codes, http_method, - max_retries, timeout, - type, tenant_id, url_path] - self._test_create_resource(resource, cmd, '', my_id, args, - position_names, position_values) - - def test_list_healthmonitors(self): - """lb-healthmonitor-list""" - resources = "health_monitors" - cmd = healthmonitor.ListHealthMonitor(test_cli20.MyApp(sys.stdout), - None) - self._test_list_resources(resources, cmd, True) - - def test_show_healthmonitor_id(self): - """lb-healthmonitor-show test_id""" - resource = 'health_monitor' - cmd = healthmonitor.ShowHealthMonitor(test_cli20.MyApp(sys.stdout), - None) - args = ['--fields', 'id', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, args, ['id']) - - def test_show_healthmonitor_id_name(self): - """lb-healthmonitor-show""" - resource = 'health_monitor' - cmd = healthmonitor.ShowHealthMonitor(test_cli20.MyApp(sys.stdout), - None) - args = ['--fields', 'id', '--fields', 'name', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id', 'name']) - - def test_update_health_monitor(self): - """lb-healthmonitor-update myid --name myname --tags a b.""" - resource = 'health_monitor' - cmd = healthmonitor.UpdateHealthMonitor(test_cli20.MyApp(sys.stdout), - None) - self._test_update_resource(resource, cmd, 'myid', - ['myid', '--timeout', '5'], - {'timeout': '5', }) - - def test_delete_healthmonitor(self): - """lb-healthmonitor-delete my-id""" - resource = 'health_monitor' - cmd = healthmonitor.DeleteHealthMonitor(test_cli20.MyApp(sys.stdout), - None) - my_id = 'my-id' - args = [my_id] - self._test_delete_resource(resource, cmd, my_id, args) - - def test_associate_healthmonitor(self): - cmd = healthmonitor.AssociateHealthMonitor( - test_cli20.MyApp(sys.stdout), - None) - resource = 'health_monitor' - health_monitor_id = 'hm-id' - pool_id = 'p_id' - args = [health_monitor_id, pool_id] - - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - - body = {resource: {'id': health_monitor_id}} - result = {resource: {'id': health_monitor_id}, } - result_str = self.client.serialize(result) - - path = getattr(self.client, - "associate_pool_health_monitors_path") % pool_id - return_tup = (test_cli20.MyResp(200), result_str) - self.client.httpclient.request( - test_cli20.end_url(path), 'POST', - body=test_cli20.MyComparator(body, self.client), - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn(return_tup) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser('test_' + resource) - parsed_args = cmd_parser.parse_args(args) - cmd.run(parsed_args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - - def test_disassociate_healthmonitor(self): - cmd = healthmonitor.DisassociateHealthMonitor( - test_cli20.MyApp(sys.stdout), - None) - resource = 'health_monitor' - health_monitor_id = 'hm-id' - pool_id = 'p_id' - args = [health_monitor_id, pool_id] - - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - - path = getattr(self.client, - "disassociate_pool_health_monitors_path") % \ - {'pool': pool_id, 'health_monitor': health_monitor_id} - return_tup = (test_cli20.MyResp(204), None) - self.client.httpclient.request( - test_cli20.end_url(path), 'DELETE', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn(return_tup) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser('test_' + resource) - parsed_args = cmd_parser.parse_args(args) - cmd.run(parsed_args) - self.mox.VerifyAll() - self.mox.UnsetStubs() diff --git a/quantumclient/tests/unit/lb/test_cli20_member.py b/quantumclient/tests/unit/lb/test_cli20_member.py deleted file mode 100644 index 7155073a5..000000000 --- a/quantumclient/tests/unit/lb/test_cli20_member.py +++ /dev/null @@ -1,104 +0,0 @@ -# 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. -# -# @author: Ilya Shakhat, Mirantis Inc. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import sys - -from quantumclient.quantum.v2_0.lb import member -from quantumclient.tests.unit import test_cli20 - - -class CLITestV20LbMember(test_cli20.CLITestV20Base): - def test_create_member(self): - """lb-member-create with mandatory params only""" - resource = 'member' - cmd = member.CreateMember(test_cli20.MyApp(sys.stdout), None) - address = '10.0.0.1' - port = '8080' - tenant_id = 'my-tenant' - my_id = 'my-id' - pool_id = 'pool-id' - args = ['--address', address, '--port', port, - '--tenant-id', tenant_id, pool_id] - position_names = ['address', 'port', 'tenant_id', 'pool_id', - 'admin_state_up'] - position_values = [address, port, tenant_id, pool_id, - True] - self._test_create_resource(resource, cmd, None, my_id, args, - position_names, position_values, - admin_state_up=None) - - def test_create_member_all_params(self): - """lb-member-create with all available params""" - resource = 'member' - cmd = member.CreateMember(test_cli20.MyApp(sys.stdout), None) - address = '10.0.0.1' - admin_state_up = False - port = '8080' - weight = '1' - tenant_id = 'my-tenant' - my_id = 'my-id' - pool_id = 'pool-id' - args = ['--address', address, '--admin-state-down', - '--port', port, '--weight', weight, - '--tenant-id', tenant_id, pool_id] - position_names = ['address', 'admin_state_up', 'port', 'weight', - 'tenant_id', 'pool_id'] - position_values = [address, admin_state_up, port, weight, - tenant_id, pool_id] - self._test_create_resource(resource, cmd, None, my_id, args, - position_names, position_values, - admin_state_up=None) - - def test_list_members(self): - """lb-member-list""" - resources = "members" - cmd = member.ListMember(test_cli20.MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) - - def test_show_member_id(self): - """lb-member-show test_id""" - resource = 'member' - cmd = member.ShowMember(test_cli20.MyApp(sys.stdout), None) - args = ['--fields', 'id', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, args, ['id']) - - def test_show_member_id_name(self): - """lb-member-show""" - resource = 'member' - cmd = member.ShowMember(test_cli20.MyApp(sys.stdout), None) - args = ['--fields', 'id', '--fields', 'name', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id', 'name']) - - def test_update_member(self): - """lb-member-update myid --name myname --tags a b.""" - resource = 'member' - cmd = member.UpdateMember(test_cli20.MyApp(sys.stdout), None) - self._test_update_resource(resource, cmd, 'myid', - ['myid', '--name', 'myname', - '--tags', 'a', 'b'], - {'name': 'myname', 'tags': ['a', 'b'], }) - - def test_delete_member(self): - """lb-member-delete my-id""" - resource = 'member' - cmd = member.DeleteMember(test_cli20.MyApp(sys.stdout), None) - my_id = 'my-id' - args = [my_id] - self._test_delete_resource(resource, cmd, my_id, args) diff --git a/quantumclient/tests/unit/lb/test_cli20_pool.py b/quantumclient/tests/unit/lb/test_cli20_pool.py deleted file mode 100644 index ee2f4e3eb..000000000 --- a/quantumclient/tests/unit/lb/test_cli20_pool.py +++ /dev/null @@ -1,145 +0,0 @@ -# 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. -# -# @author: Ilya Shakhat, Mirantis Inc. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import sys - -from mox import ContainsKeyValue - -from quantumclient.quantum.v2_0.lb import pool -from quantumclient.tests.unit import test_cli20 - - -class CLITestV20LbPool(test_cli20.CLITestV20Base): - - def test_create_pool_with_mandatory_params(self): - """lb-pool-create with mandatory params only""" - resource = 'pool' - cmd = pool.CreatePool(test_cli20.MyApp(sys.stdout), None) - name = 'my-name' - lb_method = 'round-robin' - protocol = 'http' - subnet_id = 'subnet-id' - tenant_id = 'my-tenant' - my_id = 'my-id' - args = ['--lb-method', lb_method, - '--name', name, - '--protocol', protocol, - '--subnet-id', subnet_id, - '--tenant-id', tenant_id] - position_names = ['admin_state_up', 'lb_method', 'name', - 'protocol', 'subnet_id', 'tenant_id'] - position_values = [True, lb_method, name, - protocol, subnet_id, tenant_id] - self._test_create_resource(resource, cmd, name, my_id, args, - position_names, position_values) - - def test_create_pool_with_all_params(self): - """lb-pool-create with all params set""" - resource = 'pool' - cmd = pool.CreatePool(test_cli20.MyApp(sys.stdout), None) - name = 'my-name' - description = 'my-desc' - lb_method = 'round-robin' - protocol = 'http' - subnet_id = 'subnet-id' - tenant_id = 'my-tenant' - my_id = 'my-id' - args = ['--admin-state-down', - '--description', description, - '--lb-method', lb_method, - '--name', name, - '--protocol', protocol, - '--subnet-id', subnet_id, - '--tenant-id', tenant_id] - position_names = ['admin_state_up', 'description', 'lb_method', 'name', - 'protocol', 'subnet_id', 'tenant_id'] - position_values = [False, description, lb_method, name, - protocol, subnet_id, tenant_id] - self._test_create_resource(resource, cmd, name, my_id, args, - position_names, position_values) - - def test_list_pools(self): - """lb-pool-list""" - resources = "pools" - cmd = pool.ListPool(test_cli20.MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) - - def test_show_pool_id(self): - """lb-pool-show test_id""" - resource = 'pool' - cmd = pool.ShowPool(test_cli20.MyApp(sys.stdout), None) - args = ['--fields', 'id', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, args, ['id']) - - def test_show_pool_id_name(self): - """lb-pool-show""" - resource = 'pool' - cmd = pool.ShowPool(test_cli20.MyApp(sys.stdout), None) - args = ['--fields', 'id', '--fields', 'name', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id', 'name']) - - def test_update_pool(self): - """lb-pool-update myid --name newname --tags a b.""" - resource = 'pool' - cmd = pool.UpdatePool(test_cli20.MyApp(sys.stdout), None) - self._test_update_resource(resource, cmd, 'myid', - ['myid', '--name', 'newname'], - {'name': 'newname', }) - - def test_delete_pool(self): - """lb-pool-delete my-id""" - resource = 'pool' - cmd = pool.DeletePool(test_cli20.MyApp(sys.stdout), None) - my_id = 'my-id' - args = [my_id] - self._test_delete_resource(resource, cmd, my_id, args) - - def test_retrieve_pool_stats(self): - """lb-pool-stats test_id""" - resource = 'pool' - cmd = pool.RetrievePoolStats(test_cli20.MyApp(sys.stdout), None) - my_id = self.test_id - fields = ['bytes_in', 'bytes_out'] - args = ['--fields', 'bytes_in', '--fields', 'bytes_out', my_id] - - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - query = "&".join(["fields=%s" % field for field in fields]) - expected_res = {'stats': {'bytes_in': '1234', 'bytes_out': '4321'}} - resstr = self.client.serialize(expected_res) - path = getattr(self.client, "pool_path_stats") - return_tup = (test_cli20.MyResp(200), resstr) - self.client.httpclient.request( - test_cli20.end_url(path % my_id, query), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn(return_tup) - self.mox.ReplayAll() - - cmd_parser = cmd.get_parser("test_" + resource) - parsed_args = cmd_parser.parse_args(args) - cmd.run(parsed_args) - - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertTrue('bytes_in' in _str) - self.assertTrue('bytes_out' in _str) diff --git a/quantumclient/tests/unit/lb/test_cli20_vip.py b/quantumclient/tests/unit/lb/test_cli20_vip.py deleted file mode 100644 index 8b7d17621..000000000 --- a/quantumclient/tests/unit/lb/test_cli20_vip.py +++ /dev/null @@ -1,185 +0,0 @@ -# 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. -# -# @author: Ilya Shakhat, Mirantis Inc. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import sys - -from quantumclient.quantum.v2_0.lb import vip -from quantumclient.tests.unit import test_cli20 - - -class CLITestV20LbVip(test_cli20.CLITestV20Base): - def test_create_vip_with_mandatory_params(self): - """lb-vip-create with all mandatory params""" - resource = 'vip' - cmd = vip.CreateVip(test_cli20.MyApp(sys.stdout), None) - pool_id = 'my-pool-id' - name = 'my-name' - subnet_id = 'subnet-id' - port = '1000' - protocol = 'tcp' - tenant_id = 'my-tenant' - my_id = 'my-id' - args = ['--name', name, - '--port', port, - '--protocol', protocol, - '--subnet-id', subnet_id, - '--tenant-id', tenant_id, - pool_id] - position_names = ['pool_id', 'name', 'port', 'protocol', - 'subnet_id', 'tenant_id'] - position_values = [pool_id, name, port, protocol, - subnet_id, tenant_id] - self._test_create_resource(resource, cmd, name, my_id, args, - position_names, position_values, - admin_state_up=True) - - def test_create_vip_with_all_params(self): - """lb-vip-create with all params""" - resource = 'vip' - cmd = vip.CreateVip(test_cli20.MyApp(sys.stdout), None) - pool_id = 'my-pool-id' - name = 'my-name' - description = 'my-desc' - address = '10.0.0.2' - admin_state = False - connection_limit = '1000' - subnet_id = 'subnet-id' - port = '80' - protocol = 'tcp' - tenant_id = 'my-tenant' - my_id = 'my-id' - args = ['--name', name, - '--description', description, - '--address', address, - '--admin-state-down', - '--connection-limit', connection_limit, - '--port', port, - '--protocol', protocol, - '--subnet-id', subnet_id, - '--tenant-id', tenant_id, - pool_id] - position_names = ['pool_id', 'name', 'description', 'address', - 'admin_state_up', 'connection_limit', 'port', - 'protocol', 'subnet_id', - 'tenant_id'] - position_values = [pool_id, name, description, address, - admin_state, connection_limit, port, - protocol, subnet_id, - tenant_id] - self._test_create_resource(resource, cmd, name, my_id, args, - position_names, position_values) - - def test_create_vip_with_session_persistence_params(self): - """lb-vip-create with mandatory and session-persistence params""" - resource = 'vip' - cmd = vip.CreateVip(test_cli20.MyApp(sys.stdout), None) - pool_id = 'my-pool-id' - name = 'my-name' - subnet_id = 'subnet-id' - port = '1000' - protocol = 'tcp' - tenant_id = 'my-tenant' - my_id = 'my-id' - args = ['--name', name, - '--port', port, - '--protocol', protocol, - '--subnet-id', subnet_id, - '--tenant-id', tenant_id, - pool_id, - '--session-persistence', 'type=dict', - 'type=cookie,cookie_name=pie', - '--optional-param', 'any'] - position_names = ['pool_id', 'name', 'port', 'protocol', - 'subnet_id', 'tenant_id', 'optional_param'] - position_values = [pool_id, name, port, protocol, - subnet_id, tenant_id, 'any'] - extra_body = { - 'session_persistence': { - 'type': 'cookie', - 'cookie_name': 'pie', - }, - } - self._test_create_resource(resource, cmd, name, my_id, args, - position_names, position_values, - admin_state_up=True, extra_body=extra_body) - - def test_list_vips(self): - """lb-vip-list""" - resources = "vips" - cmd = vip.ListVip(test_cli20.MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) - - def test_show_vip_id(self): - """lb-vip-show test_id""" - resource = 'vip' - cmd = vip.ShowVip(test_cli20.MyApp(sys.stdout), None) - args = ['--fields', 'id', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, args, ['id']) - - def test_show_vip_id_name(self): - """lb-vip-show""" - resource = 'vip' - cmd = vip.ShowVip(test_cli20.MyApp(sys.stdout), None) - args = ['--fields', 'id', '--fields', 'name', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id', 'name']) - - def test_update_vip(self): - """lb-vip-update myid --name myname --tags a b.""" - resource = 'vip' - cmd = vip.UpdateVip(test_cli20.MyApp(sys.stdout), None) - self._test_update_resource(resource, cmd, 'myid', - ['myid', '--name', 'myname', - '--tags', 'a', 'b'], - {'name': 'myname', 'tags': ['a', 'b'], }) - - def test_update_vip_with_session_persistence(self): - resource = 'vip' - cmd = vip.UpdateVip(test_cli20.MyApp(sys.stdout), None) - body = { - 'session_persistence': { - 'type': 'source', - }, - } - args = ['myid', '--session-persistence', 'type=dict', - 'type=source'] - self._test_update_resource(resource, cmd, 'myid', args, body) - - def test_update_vip_with_session_persistence_and_name(self): - resource = 'vip' - cmd = vip.UpdateVip(test_cli20.MyApp(sys.stdout), None) - body = { - 'name': 'newname', - 'session_persistence': { - 'type': 'cookie', - 'cookie_name': 'pie', - }, - } - args = ['myid', '--name', 'newname', - '--session-persistence', 'type=dict', - 'type=cookie,cookie_name=pie'] - self._test_update_resource(resource, cmd, 'myid', args, body) - - def test_delete_vip(self): - """lb-vip-delete my-id""" - resource = 'vip' - cmd = vip.DeleteVip(test_cli20.MyApp(sys.stdout), None) - my_id = 'my-id' - args = [my_id] - self._test_delete_resource(resource, cmd, my_id, args) diff --git a/quantumclient/tests/unit/test_auth.py b/quantumclient/tests/unit/test_auth.py deleted file mode 100644 index ba1aa0b6a..000000000 --- a/quantumclient/tests/unit/test_auth.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2012 NEC Corporation -# All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import httplib2 -import json -import unittest -import uuid - -import mox -from mox import ContainsKeyValue, IsA, StrContains -import testtools - -from quantumclient.client import HTTPClient -from quantumclient.common import exceptions - - -USERNAME = 'testuser' -TENANT_NAME = 'testtenant' -PASSWORD = 'password' -AUTH_URL = 'authurl' -ENDPOINT_URL = 'localurl' -TOKEN = 'tokentoken' -REGION = 'RegionTest' - -KS_TOKEN_RESULT = { - 'access': { - 'token': {'id': TOKEN, - 'expires': '2012-08-11T07:49:01Z', - 'tenant': {'id': str(uuid.uuid1())}}, - 'user': {'id': str(uuid.uuid1())}, - 'serviceCatalog': [ - {'endpoints_links': [], - 'endpoints': [{'adminURL': ENDPOINT_URL, - 'internalURL': ENDPOINT_URL, - 'publicURL': ENDPOINT_URL, - 'region': REGION}], - 'type': 'network', - 'name': 'Quantum Service'} - ] - } -} - - -class CLITestAuthKeystone(testtools.TestCase): - - def setUp(self): - """Prepare the test environment""" - super(CLITestAuthKeystone, self).setUp() - self.mox = mox.Mox() - self.client = HTTPClient(username=USERNAME, tenant_name=TENANT_NAME, - password=PASSWORD, auth_url=AUTH_URL, - region_name=REGION) - self.addCleanup(self.mox.VerifyAll) - self.addCleanup(self.mox.UnsetStubs) - - def test_get_token(self): - self.mox.StubOutWithMock(self.client, "request") - - res200 = self.mox.CreateMock(httplib2.Response) - res200.status = 200 - - self.client.request(AUTH_URL + '/tokens', 'POST', - body=IsA(str), headers=IsA(dict)).\ - AndReturn((res200, json.dumps(KS_TOKEN_RESULT))) - self.client.request(StrContains(ENDPOINT_URL + '/resource'), 'GET', - headers=ContainsKeyValue('X-Auth-Token', TOKEN)).\ - AndReturn((res200, '')) - self.mox.ReplayAll() - - self.client.do_request('/resource', 'GET') - self.assertEqual(self.client.endpoint_url, ENDPOINT_URL) - self.assertEqual(self.client.auth_token, TOKEN) - self.assertEqual(self.client.token_retrieved, True) - - def test_already_token_retrieved(self): - self.mox.StubOutWithMock(self.client, "request") - - self.client.auth_token = TOKEN - self.client.endpoint_url = ENDPOINT_URL - self.client.token_retrieved = True - - res200 = self.mox.CreateMock(httplib2.Response) - res200.status = 200 - - self.client.request(StrContains(ENDPOINT_URL + '/resource'), 'GET', - headers=ContainsKeyValue('X-Auth-Token', TOKEN)).\ - AndReturn((res200, '')) - self.mox.ReplayAll() - - self.client.do_request('/resource', 'GET') - - def test_refresh_token(self): - self.mox.StubOutWithMock(self.client, "request") - - self.client.auth_token = TOKEN - self.client.endpoint_url = ENDPOINT_URL - self.client.token_retrieved = True - - res200 = self.mox.CreateMock(httplib2.Response) - res200.status = 200 - res401 = self.mox.CreateMock(httplib2.Response) - res401.status = 401 - - # If a token is expired, quantum server retruns 401 - self.client.request(StrContains(ENDPOINT_URL + '/resource'), 'GET', - headers=ContainsKeyValue('X-Auth-Token', TOKEN)).\ - AndReturn((res401, '')) - self.client.request(AUTH_URL + '/tokens', 'POST', - body=IsA(str), headers=IsA(dict)).\ - AndReturn((res200, json.dumps(KS_TOKEN_RESULT))) - self.client.request(StrContains(ENDPOINT_URL + '/resource'), 'GET', - headers=ContainsKeyValue('X-Auth-Token', TOKEN)).\ - AndReturn((res200, '')) - self.mox.ReplayAll() - self.client.do_request('/resource', 'GET') diff --git a/quantumclient/tests/unit/test_casual_args.py b/quantumclient/tests/unit/test_casual_args.py deleted file mode 100644 index fcfff9331..000000000 --- a/quantumclient/tests/unit/test_casual_args.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import testtools - -from quantumclient.common import exceptions -from quantumclient.quantum import v2_0 as quantumV20 - - -class CLITestArgs(testtools.TestCase): - - def test_empty(self): - _mydict = quantumV20.parse_args_to_dict([]) - self.assertEqual({}, _mydict) - - def test_default_bool(self): - _specs = ['--my_bool', '--arg1', 'value1'] - _mydict = quantumV20.parse_args_to_dict(_specs) - self.assertTrue(_mydict['my_bool']) - - def test_bool_true(self): - _specs = ['--my-bool', 'type=bool', 'true', '--arg1', 'value1'] - _mydict = quantumV20.parse_args_to_dict(_specs) - self.assertTrue(_mydict['my_bool']) - - def test_bool_false(self): - _specs = ['--my_bool', 'type=bool', 'false', '--arg1', 'value1'] - _mydict = quantumV20.parse_args_to_dict(_specs) - self.assertFalse(_mydict['my_bool']) - - def test_nargs(self): - _specs = ['--tag', 'x', 'y', '--arg1', 'value1'] - _mydict = quantumV20.parse_args_to_dict(_specs) - self.assertTrue('x' in _mydict['tag']) - self.assertTrue('y' in _mydict['tag']) - - def test_badarg(self): - _specs = ['--tag=t', 'x', 'y', '--arg1', 'value1'] - self.assertRaises(exceptions.CommandError, - quantumV20.parse_args_to_dict, _specs) - - def test_badarg_with_minus(self): - _specs = ['--arg1', 'value1', '-D'] - self.assertRaises(exceptions.CommandError, - quantumV20.parse_args_to_dict, _specs) - - def test_goodarg_with_minus_number(self): - _specs = ['--arg1', 'value1', '-1', '-1.0'] - _mydict = quantumV20.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, - quantumV20.parse_args_to_dict, _specs) - - def test_badarg_early_type_specification(self): - _specs = ['type=dict', 'key=value'] - self.assertRaises(exceptions.CommandError, - quantumV20.parse_args_to_dict, _specs) - - def test_arg(self): - _specs = ['--tag=t', '--arg1', 'value1'] - self.assertEqual('value1', - quantumV20.parse_args_to_dict(_specs)['arg1']) - - def test_dict_arg(self): - _specs = ['--tag=t', '--arg1', 'type=dict', 'key1=value1,key2=value2'] - arg1 = quantumV20.parse_args_to_dict(_specs)['arg1'] - self.assertEqual('value1', arg1['key1']) - self.assertEqual('value2', arg1['key2']) - - def test_dict_arg_with_attribute_named_type(self): - _specs = ['--tag=t', '--arg1', 'type=dict', 'type=value1,key2=value2'] - arg1 = quantumV20.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 = quantumV20.parse_args_to_dict(_specs)['arg1'] - self.assertEqual('value1', arg1[0]['key1']) - self.assertEqual('value2', arg1[0]['key2']) diff --git a/quantumclient/tests/unit/test_cli20.py b/quantumclient/tests/unit/test_cli20.py deleted file mode 100644 index d2abaf94e..000000000 --- a/quantumclient/tests/unit/test_cli20.py +++ /dev/null @@ -1,357 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import fixtures -import mox -from mox import Comparator -from mox import ContainsKeyValue -import testtools - -from quantumclient import shell -from quantumclient.v2_0.client import Client - - -API_VERSION = "2.0" -FORMAT = 'json' -TOKEN = 'testtoken' -ENDURL = 'localurl' - - -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 MyResp(object): - def __init__(self, status): - self.status = status - - -class MyApp(object): - def __init__(self, _stdout): - self.stdout = _stdout - - -def end_url(path, query=None): - _url_str = ENDURL + "/v" + API_VERSION + path + "." + FORMAT - return query and _url_str + "?" + query or _url_str - - -class MyUrlComparator(Comparator): - def __init__(self, lhs, client): - self.lhs = lhs - self.client = client - - def equals(self, rhs): - return str(self) == rhs - - def __str__(self): - if self.client and self.client.format != FORMAT: - lhs_parts = self.lhs.split("?", 1) - if len(lhs_parts) == 2: - lhs = ("%s%s?%s" % (lhs_parts[0][:-4], - self.client.format, - lhs_parts[1])) - else: - lhs = ("%s%s" % (lhs_parts[0][:-4], - self.client.format)) - return lhs - return self.lhs - - def __repr__(self): - return str(self) - - -class MyComparator(Comparator): - def __init__(self, lhs, client): - self.lhs = lhs - self.client = client - - def _com_dict(self, lhs, rhs): - if len(lhs) != len(rhs): - return False - for key, value in lhs.iteritems(): - if key not in rhs: - return False - rhs_value = rhs[key] - if not self._com(value, rhs_value): - return False - return True - - def _com_list(self, lhs, rhs): - if len(lhs) != len(rhs): - return False - for lhs_value in lhs: - if lhs_value not in rhs: - return False - return True - - def _com(self, lhs, rhs): - if lhs is None: - return rhs is None - if isinstance(lhs, dict): - if not isinstance(rhs, dict): - return False - return self._com_dict(lhs, rhs) - if isinstance(lhs, list): - if not isinstance(rhs, list): - return False - return self._com_list(lhs, rhs) - if isinstance(lhs, tuple): - if not isinstance(rhs, tuple): - return False - return self._com_list(lhs, rhs) - return lhs == rhs - - def equals(self, rhs): - if self.client: - rhs = self.client.deserialize(rhs, 200) - return self._com(self.lhs, rhs) - - def __repr__(self): - return str(self.lhs) - - -class CLITestV20Base(testtools.TestCase): - - test_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' - - def _find_resourceid(self, client, resource, name_or_id): - return name_or_id - - def setUp(self): - """Prepare the test environment""" - super(CLITestV20Base, self).setUp() - self.mox = mox.Mox() - self.endurl = ENDURL - self.client = Client(token=TOKEN, endpoint_url=self.endurl) - self.fake_stdout = FakeStdout() - self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.fake_stdout)) - self.useFixture(fixtures.MonkeyPatch( - 'quantumclient.quantum.v2_0.find_resourceid_by_name_or_id', - self._find_resourceid)) - - def _test_create_resource(self, resource, cmd, - name, myid, args, - position_names, position_values, tenant_id=None, - tags=None, admin_state_up=True, shared=False, - extra_body=None): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - non_admin_status_resources = ['subnet', 'floatingip', 'security_group', - 'security_group_rule', 'qos_queue'] - if (resource in non_admin_status_resources): - body = {resource: {}, } - else: - body = {resource: {'admin_state_up': admin_state_up, }, } - if tenant_id: - body[resource].update({'tenant_id': tenant_id}) - if tags: - body[resource].update({'tags': tags}) - if shared: - body[resource].update({'shared': shared}) - if extra_body: - body[resource].update(extra_body) - - for i in xrange(len(position_names)): - body[resource].update({position_names[i]: position_values[i]}) - ress = {resource: - {'id': myid}, } - if name: - ress[resource].update({'name': name}) - resstr = self.client.serialize(ress) - # url method body - path = getattr(self.client, resource + "s_path") - self.client.httpclient.request( - end_url(path), 'POST', - body=MyComparator(body, self.client), - headers=ContainsKeyValue('X-Auth-Token', - TOKEN)).AndReturn((MyResp(200), - resstr)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser('create_' + resource) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertTrue(myid in _str) - if name: - self.assertTrue(name in _str) - - def _test_list_columns(self, cmd, resources_collection, - resources_out, args=['-f', 'json']): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - resstr = self.client.serialize(resources_out) - - path = getattr(self.client, resources_collection + "_path") - self.client.httpclient.request( - end_url(path), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - TOKEN)).AndReturn((MyResp(200), resstr)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("list_" + resources_collection) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - - def _test_list_resources(self, resources, cmd, detail=False, tags=[], - fields_1=[], fields_2=[]): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - reses = {resources: [{'id': 'myid1', }, - {'id': 'myid2', }, ], } - resstr = self.client.serialize(reses) - # url method body - query = "" - args = detail and ['-D', ] or [] - if fields_1: - for field in fields_1: - args.append('--fields') - args.append(field) - - if tags: - args.append('--') - args.append("--tag") - for tag in tags: - if query: - query += "&tag=" + tag - else: - query = "tag=" + tag - args.append(tag) - if (not tags) and fields_2: - args.append('--') - if fields_2: - args.append("--fields") - for field in fields_2: - args.append(field) - if detail: - query = query and query + '&verbose=True' or 'verbose=True' - fields_1.extend(fields_2) - for field in fields_1: - if query: - query += "&fields=" + field - else: - query = "fields=" + field - path = getattr(self.client, resources + "_path") - self.client.httpclient.request( - MyUrlComparator(end_url(path, query), self.client), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - TOKEN)).AndReturn((MyResp(200), resstr)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("list_" + resources) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertTrue('myid1' in _str) - - def _test_update_resource(self, resource, cmd, myid, args, extrafields): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - body = {resource: extrafields} - path = getattr(self.client, resource + "_path") - self.client.httpclient.request( - MyUrlComparator(end_url(path % myid), self.client), 'PUT', - body=MyComparator(body, self.client), - headers=ContainsKeyValue('X-Auth-Token', - TOKEN)).AndReturn((MyResp(204), None)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("update_" + resource) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertTrue(myid in _str) - - def _test_show_resource(self, resource, cmd, myid, args, fields=[]): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - query = "&".join(["fields=%s" % field for field in fields]) - expected_res = {resource: - {'id': myid, - 'name': 'myname', }, } - resstr = self.client.serialize(expected_res) - path = getattr(self.client, resource + "_path") - self.client.httpclient.request( - end_url(path % myid, query), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - TOKEN)).AndReturn((MyResp(200), resstr)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("show_" + resource) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertTrue(myid in _str) - self.assertTrue('myname' in _str) - - def _test_delete_resource(self, resource, cmd, myid, args): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - path = getattr(self.client, resource + "_path") - self.client.httpclient.request( - end_url(path % myid), 'DELETE', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - TOKEN)).AndReturn((MyResp(204), None)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("delete_" + resource) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertTrue(myid in _str) - - def _test_update_resource_action(self, resource, cmd, myid, action, args, - body): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - path = getattr(self.client, resource + "_path") - path_action = '%s/%s' % (myid, action) - self.client.httpclient.request( - end_url(path % path_action), 'PUT', - body=MyComparator(body, self.client), - headers=ContainsKeyValue('X-Auth-Token', - TOKEN)).AndReturn((MyResp(204), None)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("delete_" + resource) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertTrue(myid in _str) diff --git a/quantumclient/tests/unit/test_cli20_floatingips.py b/quantumclient/tests/unit/test_cli20_floatingips.py deleted file mode 100644 index 7b5912c00..000000000 --- a/quantumclient/tests/unit/test_cli20_floatingips.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Red Hat -# 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 quantumclient.common import exceptions -from quantumclient.quantum.v2_0.floatingip import AssociateFloatingIP -from quantumclient.quantum.v2_0.floatingip import CreateFloatingIP -from quantumclient.quantum.v2_0.floatingip import DeleteFloatingIP -from quantumclient.quantum.v2_0.floatingip import DisassociateFloatingIP -from quantumclient.quantum.v2_0.floatingip import ListFloatingIP -from quantumclient.quantum.v2_0.floatingip import ShowFloatingIP -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp - - -class CLITestV20FloatingIps(CLITestV20Base): - def test_create_floatingip(self): - """Create floatingip: fip1.""" - resource = 'floatingip' - cmd = CreateFloatingIP(MyApp(sys.stdout), None) - name = 'fip1' - myid = 'myid' - args = [name] - position_names = ['floating_network_id'] - position_values = [name] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_floatingip_and_port(self): - """Create floatingip: fip1.""" - resource = 'floatingip' - cmd = CreateFloatingIP(MyApp(sys.stdout), None) - name = 'fip1' - myid = 'myid' - pid = 'mypid' - args = [name, '--port_id', pid] - position_names = ['floating_network_id', 'port_id'] - position_values = [name, pid] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - # Test dashed options - args = [name, '--port-id', pid] - position_names = ['floating_network_id', 'port_id'] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_floatingip_and_port_and_address(self): - """Create floatingip: fip1 with a given port and address""" - resource = 'floatingip' - cmd = CreateFloatingIP(MyApp(sys.stdout), None) - name = 'fip1' - myid = 'myid' - pid = 'mypid' - addr = '10.0.0.99' - args = [name, '--port_id', pid, '--fixed_ip_address', addr] - position_names = ['floating_network_id', 'port_id', 'fixed_ip_address'] - position_values = [name, pid, addr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - # Test dashed options - args = [name, '--port-id', pid, '--fixed-ip-address', addr] - position_names = ['floating_network_id', 'port_id', 'fixed_ip_address'] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_list_floatingips(self): - """list floatingips: -D.""" - resources = 'floatingips' - cmd = ListFloatingIP(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) - - def test_delete_floatingip(self): - """Delete floatingip: fip1""" - resource = 'floatingip' - cmd = DeleteFloatingIP(MyApp(sys.stdout), None) - myid = 'myid' - args = [myid] - self._test_delete_resource(resource, cmd, myid, args) - - def test_show_floatingip(self): - """Show floatingip: --fields id.""" - resource = 'floatingip' - cmd = ShowFloatingIP(MyApp(sys.stdout), None) - args = ['--fields', 'id', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id']) - - def test_disassociate_ip(self): - """Disassociate floating IP: myid""" - resource = 'floatingip' - cmd = DisassociateFloatingIP(MyApp(sys.stdout), None) - args = ['myid'] - self._test_update_resource(resource, cmd, 'myid', - args, {"port_id": None} - ) - - def test_associate_ip(self): - """Associate floating IP: myid portid""" - resource = 'floatingip' - cmd = AssociateFloatingIP(MyApp(sys.stdout), None) - args = ['myid', 'portid'] - self._test_update_resource(resource, cmd, 'myid', - args, {"port_id": "portid"} - ) diff --git a/quantumclient/tests/unit/test_cli20_network.py b/quantumclient/tests/unit/test_cli20_network.py deleted file mode 100644 index 6bc9ebd4c..000000000 --- a/quantumclient/tests/unit/test_cli20_network.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import sys - -import mox -from mox import (ContainsKeyValue, IgnoreArg, IsA) - -from quantumclient.common import exceptions -from quantumclient.common import utils -from quantumclient.quantum.v2_0.network import CreateNetwork -from quantumclient.quantum.v2_0.network import DeleteNetwork -from quantumclient.quantum.v2_0.network import ListExternalNetwork -from quantumclient.quantum.v2_0.network import ListNetwork -from quantumclient.quantum.v2_0.network import ShowNetwork -from quantumclient.quantum.v2_0.network import UpdateNetwork -from quantumclient import shell -from quantumclient.tests.unit import test_cli20 -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp - - -class CLITestV20Network(CLITestV20Base): - def test_create_network(self): - """Create net: myname.""" - resource = 'network' - cmd = CreateNetwork(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - args = [name, ] - position_names = ['name', ] - position_values = [name, ] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_network_tenant(self): - """Create net: --tenant_id tenantid myname.""" - resource = 'network' - cmd = CreateNetwork(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - args = ['--tenant_id', 'tenantid', name] - position_names = ['name', ] - position_values = [name, ] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - # Test dashed options - args = ['--tenant-id', 'tenantid', name] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_network_tags(self): - """Create net: myname --tags a b.""" - resource = 'network' - cmd = CreateNetwork(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - args = [name, '--tags', 'a', 'b'] - position_names = ['name', ] - position_values = [name, ] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tags=['a', 'b']) - - def test_create_network_state(self): - """Create net: --admin_state_down myname.""" - resource = 'network' - cmd = CreateNetwork(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - args = ['--admin_state_down', name, ] - position_names = ['name', ] - position_values = [name, ] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - admin_state_up=False) - - # Test dashed options - args = ['--admin-state-down', name, ] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - admin_state_up=False) - - def test_list_nets_empty_with_column(self): - resources = "networks" - cmd = ListNetwork(MyApp(sys.stdout), None) - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - self.mox.StubOutWithMock(ListNetwork, "extend_list") - ListNetwork.extend_list(IsA(list), IgnoreArg()) - cmd.get_client().MultipleTimes().AndReturn(self.client) - reses = {resources: []} - resstr = self.client.serialize(reses) - # url method body - query = "id=myfakeid" - args = ['-c', 'id', '--', '--id', 'myfakeid'] - path = getattr(self.client, resources + "_path") - self.client.httpclient.request( - test_cli20.end_url(path, query), 'GET', - body=None, - headers=test_cli20.ContainsKeyValue( - 'X-Auth-Token', - test_cli20.TOKEN)).AndReturn( - (test_cli20.MyResp(200), resstr)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("list_" + resources) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertEquals('\n', _str) - - def _test_list_networks(self, cmd, detail=False, tags=[], - fields_1=[], fields_2=[]): - resources = "networks" - self.mox.StubOutWithMock(ListNetwork, "extend_list") - ListNetwork.extend_list(IsA(list), IgnoreArg()) - self._test_list_resources(resources, cmd, detail, tags, - fields_1, fields_2) - - def test_list_nets_detail(self): - """list nets: -D.""" - cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_networks(cmd, True) - - def test_list_nets_tags(self): - """List nets: -- --tags a b.""" - cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_networks(cmd, tags=['a', 'b']) - - def test_list_nets_detail_tags(self): - """List nets: -D -- --tags a b.""" - resources = "networks" - cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_networks(cmd, detail=True, tags=['a', 'b']) - - def _test_list_nets_extend_subnets(self, data, expected): - def setup_list_stub(resources, data, query): - reses = {resources: data} - resstr = self.client.serialize(reses) - resp = (test_cli20.MyResp(200), resstr) - path = getattr(self.client, resources + '_path') - self.client.httpclient.request( - test_cli20.end_url(path, query), 'GET', - body=None, - headers=ContainsKeyValue( - 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(resp) - - resources = "networks" - cmd = ListNetwork(test_cli20.MyApp(sys.stdout), None) - self.mox.StubOutWithMock(cmd, 'get_client') - self.mox.StubOutWithMock(self.client.httpclient, 'request') - cmd.get_client().AndReturn(self.client) - setup_list_stub('networks', data, '') - cmd.get_client().AndReturn(self.client) - setup_list_stub('subnets', - [{'id': 'mysubid1', 'cidr': '192.168.1.0/24'}, - {'id': 'mysubid2', 'cidr': '172.16.0.0/24'}, - {'id': 'mysubid3', 'cidr': '10.1.1.0/24'}], - query='fields=id&fields=cidr') - self.mox.ReplayAll() - - args = [] - cmd_parser = cmd.get_parser('list_networks') - parsed_args = cmd_parser.parse_args(args) - result = cmd.get_data(parsed_args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _result = [x for x in result[1]] - self.assertEqual(len(_result), len(expected)) - for res, exp in zip(_result, expected): - self.assertEqual(len(res), len(exp)) - for a, b in zip(res, exp): - self.assertEqual(a, b) - - def test_list_nets_extend_subnets(self): - data = [{'id': 'netid1', 'name': 'net1', 'subnets': ['mysubid1']}, - {'id': 'netid2', 'name': 'net2', 'subnets': ['mysubid2', - 'mysubid3']}] - # id, name, subnets - expected = [('netid1', 'net1', 'mysubid1 192.168.1.0/24'), - ('netid2', 'net2', - 'mysubid2 172.16.0.0/24\nmysubid3 10.1.1.0/24')] - self._test_list_nets_extend_subnets(data, expected) - - def test_list_nets_extend_subnets_no_subnet(self): - data = [{'id': 'netid1', 'name': 'net1', 'subnets': ['mysubid1']}, - {'id': 'netid2', 'name': 'net2', 'subnets': ['mysubid4']}] - # id, name, subnets - expected = [('netid1', 'net1', 'mysubid1 192.168.1.0/24'), - ('netid2', 'net2', 'mysubid4 ')] - self._test_list_nets_extend_subnets(data, expected) - - def test_list_nets_fields(self): - """List nets: --fields a --fields b -- --fields c d.""" - resources = "networks" - cmd = ListNetwork(MyApp(sys.stdout), None) - self._test_list_networks(cmd, - fields_1=['a', 'b'], fields_2=['c', 'd']) - - def _test_list_nets_columns(self, cmd, returned_body, - args=['-f', 'json']): - resources = 'networks' - self.mox.StubOutWithMock(ListNetwork, "extend_list") - ListNetwork.extend_list(IsA(list), IgnoreArg()) - self._test_list_columns(cmd, resources, returned_body, args=args) - - def test_list_nets_defined_column(self): - cmd = ListNetwork(MyApp(sys.stdout), None) - returned_body = {"networks": [{"name": "buildname3", - "id": "id3", - "tenant_id": "tenant_3", - "subnets": []}]} - self._test_list_nets_columns(cmd, returned_body, - args=['-f', 'json', '-c', 'id']) - _str = self.fake_stdout.make_string() - returned_networks = utils.loads(_str) - self.assertEquals(1, len(returned_networks)) - network = returned_networks[0] - self.assertEquals(1, len(network)) - self.assertEquals("id", network.keys()[0]) - - def test_list_nets_with_default_column(self): - cmd = ListNetwork(MyApp(sys.stdout), None) - returned_body = {"networks": [{"name": "buildname3", - "id": "id3", - "tenant_id": "tenant_3", - "subnets": []}]} - self._test_list_nets_columns(cmd, returned_body) - _str = self.fake_stdout.make_string() - returned_networks = utils.loads(_str) - self.assertEquals(1, len(returned_networks)) - network = returned_networks[0] - self.assertEquals(3, len(network)) - self.assertEquals(0, len(set(network) ^ - set(cmd.list_columns))) - - def test_list_external_nets_empty_with_column(self): - resources = "networks" - cmd = ListExternalNetwork(MyApp(sys.stdout), None) - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - self.mox.StubOutWithMock(ListNetwork, "extend_list") - ListNetwork.extend_list(IsA(list), IgnoreArg()) - cmd.get_client().MultipleTimes().AndReturn(self.client) - reses = {resources: []} - resstr = self.client.serialize(reses) - # url method body - query = "router%3Aexternal=True&id=myfakeid" - args = ['-c', 'id', '--', '--id', 'myfakeid'] - path = getattr(self.client, resources + "_path") - self.client.httpclient.request( - test_cli20.end_url(path, query), 'GET', - body=None, - headers=test_cli20.ContainsKeyValue( - 'X-Auth-Token', - test_cli20.TOKEN)).AndReturn( - (test_cli20.MyResp(200), resstr)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("list_" + resources) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - self.assertEquals('\n', _str) - - def _test_list_external_nets(self, resources, cmd, - detail=False, tags=[], - fields_1=[], fields_2=[]): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - self.mox.StubOutWithMock(ListNetwork, "extend_list") - ListNetwork.extend_list(IsA(list), IgnoreArg()) - cmd.get_client().MultipleTimes().AndReturn(self.client) - reses = {resources: [{'id': 'myid1', }, - {'id': 'myid2', }, ], } - - resstr = self.client.serialize(reses) - - # url method body - query = "" - args = detail and ['-D', ] or [] - if fields_1: - for field in fields_1: - args.append('--fields') - args.append(field) - if tags: - args.append('--') - args.append("--tag") - for tag in tags: - args.append(tag) - if (not tags) and fields_2: - args.append('--') - if fields_2: - args.append("--fields") - for field in fields_2: - args.append(field) - fields_1.extend(fields_2) - for field in fields_1: - if query: - query += "&fields=" + field - else: - query = "fields=" + field - if query: - query += '&router%3Aexternal=True' - else: - query += 'router%3Aexternal=True' - for tag in tags: - if query: - query += "&tag=" + tag - else: - query = "tag=" + tag - if detail: - query = query and query + '&verbose=True' or 'verbose=True' - path = getattr(self.client, resources + "_path") - - self.client.httpclient.request( - test_cli20.end_url(path, query), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn( - (test_cli20.MyResp(200), resstr)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("list_" + resources) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - - self.assertTrue('myid1' in _str) - - def test_list_external_nets_detail(self): - """list external nets: -D.""" - resources = "networks" - cmd = ListExternalNetwork(MyApp(sys.stdout), None) - self._test_list_external_nets(resources, cmd, True) - - def test_list_external_nets_tags(self): - """List external nets: -- --tags a b.""" - resources = "networks" - cmd = ListExternalNetwork(MyApp(sys.stdout), None) - self._test_list_external_nets(resources, - cmd, tags=['a', 'b']) - - def test_list_external_nets_detail_tags(self): - """List external nets: -D -- --tags a b.""" - resources = "networks" - cmd = ListExternalNetwork(MyApp(sys.stdout), None) - self._test_list_external_nets(resources, cmd, - detail=True, tags=['a', 'b']) - - def test_list_externel_nets_fields(self): - """List external nets: --fields a --fields b -- --fields c d.""" - resources = "networks" - cmd = ListExternalNetwork(MyApp(sys.stdout), None) - self._test_list_external_nets(resources, cmd, - fields_1=['a', 'b'], - fields_2=['c', 'd']) - - def test_update_network_exception(self): - """Update net: myid.""" - resource = 'network' - cmd = UpdateNetwork(MyApp(sys.stdout), None) - self.assertRaises(exceptions.CommandError, self._test_update_resource, - resource, cmd, 'myid', ['myid'], {}) - - def test_update_network(self): - """Update net: myid --name myname --tags a b.""" - resource = 'network' - cmd = UpdateNetwork(MyApp(sys.stdout), None) - self._test_update_resource(resource, cmd, 'myid', - ['myid', '--name', 'myname', - '--tags', 'a', 'b'], - {'name': 'myname', 'tags': ['a', 'b'], } - ) - - def test_show_network(self): - """Show net: --fields id --fields name myid.""" - resource = 'network' - cmd = ShowNetwork(MyApp(sys.stdout), None) - args = ['--fields', 'id', '--fields', 'name', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, args, - ['id', 'name']) - - def test_delete_network(self): - """Delete net: myid.""" - resource = 'network' - cmd = DeleteNetwork(MyApp(sys.stdout), None) - myid = 'myid' - args = [myid] - self._test_delete_resource(resource, cmd, myid, args) diff --git a/quantumclient/tests/unit/test_cli20_nvp_queue.py b/quantumclient/tests/unit/test_cli20_nvp_queue.py deleted file mode 100644 index 96ed11a0d..000000000 --- a/quantumclient/tests/unit/test_cli20_nvp_queue.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 Nicira Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import sys - -from quantumclient.quantum.v2_0 import nvp_qos_queue as qos -from quantumclient.tests.unit import test_cli20 - - -class CLITestV20NvpQosQueue(test_cli20.CLITestV20Base): - def test_create_qos_queue(self): - """Create a qos queue.""" - resource = 'qos_queue' - cmd = qos.CreateQoSQueue( - test_cli20.MyApp(sys.stdout), None) - myid = 'myid' - name = 'my_queue' - default = False - args = ['--default', default, name] - position_names = ['name', 'default'] - position_values = [name, default] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_qos_queue_all_values(self): - """Create a qos queue.""" - resource = 'qos_queue' - cmd = qos.CreateQoSQueue( - test_cli20.MyApp(sys.stdout), None) - myid = 'myid' - name = 'my_queue' - default = False - min = '10' - max = '40' - qos_marking = 'untrusted' - dscp = '0' - args = ['--default', default, '--min', min, '--max', max, - '--qos-marking', qos_marking, '--dscp', dscp, name] - position_names = ['name', 'default', 'min', 'max', 'qos_marking', - 'dscp'] - position_values = [name, default, min, max, qos_marking, dscp] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_list_qos_queue(self): - resources = "qos_queues" - cmd = qos.ListQoSQueue( - test_cli20.MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) - - def test_show_qos_queue_id(self): - resource = 'qos_queue' - cmd = qos.ShowQoSQueue( - test_cli20.MyApp(sys.stdout), None) - args = ['--fields', 'id', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id']) - - def test_delete_qos_queue(self): - resource = 'qos_queue' - cmd = qos.DeleteQoSQueue( - test_cli20.MyApp(sys.stdout), None) - myid = 'myid' - args = [myid] - self._test_delete_resource(resource, cmd, myid, args) diff --git a/quantumclient/tests/unit/test_cli20_port.py b/quantumclient/tests/unit/test_cli20_port.py deleted file mode 100644 index faec572ea..000000000 --- a/quantumclient/tests/unit/test_cli20_port.py +++ /dev/null @@ -1,285 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import sys - -from mox import ContainsKeyValue - -from quantumclient import shell -from quantumclient.quantum.v2_0.port import CreatePort -from quantumclient.quantum.v2_0.port import DeletePort -from quantumclient.quantum.v2_0.port import ListPort -from quantumclient.quantum.v2_0.port import ListRouterPort -from quantumclient.quantum.v2_0.port import ShowPort -from quantumclient.quantum.v2_0.port import UpdatePort -from quantumclient.tests.unit import test_cli20 -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp - - -class CLITestV20Port(CLITestV20Base): - - def test_create_port(self): - """Create port: netid.""" - resource = 'port' - cmd = CreatePort(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - args = [netid] - position_names = ['network_id'] - position_values = [] - position_values.extend([netid]) - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_port_full(self): - """Create port: --mac_address mac --device_id deviceid netid.""" - resource = 'port' - cmd = CreatePort(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - args = ['--mac_address', 'mac', '--device_id', 'deviceid', netid] - position_names = ['network_id', 'mac_address', 'device_id'] - position_values = [netid, 'mac', 'deviceid'] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - # Test dashed options - args = ['--mac-address', 'mac', '--device-id', 'deviceid', netid] - position_names = ['network_id', 'mac_address', 'device_id'] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_port_tenant(self): - """Create port: --tenant_id tenantid netid.""" - resource = 'port' - cmd = CreatePort(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - args = ['--tenant_id', 'tenantid', netid, ] - position_names = ['network_id'] - position_values = [] - position_values.extend([netid]) - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - # Test dashed options - args = ['--tenant-id', 'tenantid', netid, ] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_port_tags(self): - """Create port: netid mac_address device_id --tags a b.""" - resource = 'port' - cmd = CreatePort(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - args = [netid, '--tags', 'a', 'b'] - position_names = ['network_id'] - position_values = [] - position_values.extend([netid]) - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tags=['a', 'b']) - - def test_create_port_secgroup(self): - """Create port: --security-group sg1_id netid""" - resource = 'port' - cmd = CreatePort(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - args = ['--security-group', 'sg1_id', netid] - position_names = ['network_id', 'security_groups'] - position_values = [netid, ['sg1_id']] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_port_secgroups(self): - """Create port: netid - - The are - --security-group sg1_id --security-group sg2_id - """ - resource = 'port' - cmd = CreatePort(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - args = ['--security-group', 'sg1_id', - '--security-group', 'sg2_id', - netid] - position_names = ['network_id', 'security_groups'] - position_values = [netid, ['sg1_id', 'sg2_id']] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_list_ports(self): - """List ports: -D.""" - resources = "ports" - cmd = ListPort(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) - - def test_list_ports_tags(self): - """List ports: -- --tags a b.""" - resources = "ports" - cmd = ListPort(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, tags=['a', 'b']) - - def test_list_ports_detail_tags(self): - """List ports: -D -- --tags a b.""" - resources = "ports" - cmd = ListPort(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, detail=True, tags=['a', 'b']) - - def test_list_ports_fields(self): - """List ports: --fields a --fields b -- --fields c d.""" - resources = "ports" - cmd = ListPort(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, - fields_1=['a', 'b'], fields_2=['c', 'd']) - - def _test_list_router_port(self, resources, cmd, - myid, detail=False, tags=[], - fields_1=[], fields_2=[]): - self.mox.StubOutWithMock(cmd, "get_client") - self.mox.StubOutWithMock(self.client.httpclient, "request") - cmd.get_client().MultipleTimes().AndReturn(self.client) - reses = {resources: [{'id': 'myid1', }, - {'id': 'myid2', }, ], } - - resstr = self.client.serialize(reses) - - # url method body - query = "" - args = detail and ['-D', ] or [] - - if fields_1: - for field in fields_1: - args.append('--fields') - args.append(field) - args.append(myid) - if tags: - args.append('--') - args.append("--tag") - for tag in tags: - args.append(tag) - if (not tags) and fields_2: - args.append('--') - if fields_2: - args.append("--fields") - for field in fields_2: - args.append(field) - fields_1.extend(fields_2) - for field in fields_1: - if query: - query += "&fields=" + field - else: - query = "fields=" + field - - for tag in tags: - if query: - query += "&tag=" + tag - else: - query = "tag=" + tag - if detail: - query = query and query + '&verbose=True' or 'verbose=True' - query = query and query + '&device_id=%s' or 'device_id=%s' - path = getattr(self.client, resources + "_path") - self.client.httpclient.request( - test_cli20.end_url(path, query % myid), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn( - (test_cli20.MyResp(200), resstr)) - self.mox.ReplayAll() - cmd_parser = cmd.get_parser("list_" + resources) - shell.run_command(cmd, cmd_parser, args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - _str = self.fake_stdout.make_string() - - self.assertTrue('myid1' in _str) - - def test_list_router_ports(self): - """List router ports: -D.""" - resources = "ports" - cmd = ListRouterPort(MyApp(sys.stdout), None) - self._test_list_router_port(resources, cmd, - self.test_id, True) - - def test_list_router_ports_tags(self): - """List router ports: -- --tags a b.""" - resources = "ports" - cmd = ListRouterPort(MyApp(sys.stdout), None) - self._test_list_router_port(resources, cmd, - self.test_id, tags=['a', 'b']) - - def test_list_router_ports_detail_tags(self): - """List router ports: -D -- --tags a b.""" - resources = "ports" - cmd = ListRouterPort(MyApp(sys.stdout), None) - self._test_list_router_port(resources, cmd, self.test_id, - detail=True, tags=['a', 'b']) - - def test_list_router_ports_fields(self): - """List ports: --fields a --fields b -- --fields c d.""" - resources = "ports" - cmd = ListRouterPort(MyApp(sys.stdout), None) - self._test_list_router_port(resources, cmd, self.test_id, - fields_1=['a', 'b'], - fields_2=['c', 'd']) - - def test_update_port(self): - """Update port: myid --name myname --tags a b.""" - resource = 'port' - cmd = UpdatePort(MyApp(sys.stdout), None) - self._test_update_resource(resource, cmd, 'myid', - ['myid', '--name', 'myname', - '--tags', 'a', 'b'], - {'name': 'myname', 'tags': ['a', 'b'], } - ) - - def test_update_port_security_group_off(self): - """Update port: --no-security-groups myid.""" - resource = 'port' - cmd = UpdatePort(MyApp(sys.stdout), None) - self._test_update_resource(resource, cmd, 'myid', - ['--no-security-groups', 'myid'], - {'security_groups': None}) - - def test_show_port(self): - """Show port: --fields id --fields name myid.""" - resource = 'port' - cmd = ShowPort(MyApp(sys.stdout), None) - args = ['--fields', 'id', '--fields', 'name', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id', 'name']) - - def test_delete_port(self): - """Delete port: myid.""" - resource = 'port' - cmd = DeletePort(MyApp(sys.stdout), None) - myid = 'myid' - args = [myid] - self._test_delete_resource(resource, cmd, myid, args) diff --git a/quantumclient/tests/unit/test_cli20_router.py b/quantumclient/tests/unit/test_cli20_router.py deleted file mode 100644 index 502365db3..000000000 --- a/quantumclient/tests/unit/test_cli20_router.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright 2012 Nicira, 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import sys - -from quantumclient.common import exceptions -from quantumclient.quantum.v2_0.router import AddInterfaceRouter -from quantumclient.quantum.v2_0.router import CreateRouter -from quantumclient.quantum.v2_0.router import DeleteRouter -from quantumclient.quantum.v2_0.router import ListRouter -from quantumclient.quantum.v2_0.router import RemoveGatewayRouter -from quantumclient.quantum.v2_0.router import RemoveInterfaceRouter -from quantumclient.quantum.v2_0.router import SetGatewayRouter -from quantumclient.quantum.v2_0.router import ShowRouter -from quantumclient.quantum.v2_0.router import UpdateRouter -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp - - -class CLITestV20Router(CLITestV20Base): - def test_create_router(self): - """Create router: router1.""" - resource = 'router' - cmd = CreateRouter(MyApp(sys.stdout), None) - name = 'router1' - myid = 'myid' - args = [name, ] - position_names = ['name', ] - position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_router_tenant(self): - """Create router: --tenant_id tenantid myname.""" - resource = 'router' - cmd = CreateRouter(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - args = ['--tenant_id', 'tenantid', name] - position_names = ['name', ] - position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_router_admin_state(self): - """Create router: --admin_state_down myname.""" - resource = 'router' - cmd = CreateRouter(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - args = ['--admin_state_down', name, ] - position_names = ['name', ] - position_values = [name, ] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - admin_state_up=False) - - def test_list_routers_detail(self): - """list routers: -D.""" - resources = "routers" - cmd = ListRouter(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) - - def test_update_router_exception(self): - """Update router: myid.""" - resource = 'router' - cmd = UpdateRouter(MyApp(sys.stdout), None) - self.assertRaises(exceptions.CommandError, self._test_update_resource, - resource, cmd, 'myid', ['myid'], {}) - - def test_update_router(self): - """Update router: myid --name myname --tags a b.""" - resource = 'router' - cmd = UpdateRouter(MyApp(sys.stdout), None) - self._test_update_resource(resource, cmd, 'myid', - ['myid', '--name', 'myname'], - {'name': 'myname'} - ) - - def test_delete_router(self): - """Delete router: myid.""" - resource = 'router' - cmd = DeleteRouter(MyApp(sys.stdout), None) - myid = 'myid' - args = [myid] - self._test_delete_resource(resource, cmd, myid, args) - - def test_show_router(self): - """Show router: myid.""" - resource = 'router' - cmd = ShowRouter(MyApp(sys.stdout), None) - args = ['--fields', 'id', '--fields', 'name', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, args, - ['id', 'name']) - - def test_add_interface(self): - """Add interface to router: myid subnetid""" - resource = 'router' - cmd = AddInterfaceRouter(MyApp(sys.stdout), None) - args = ['myid', 'subnetid'] - self._test_update_resource_action(resource, cmd, 'myid', - 'add_router_interface', - args, - {'subnet_id': 'subnetid'} - ) - - def test_del_interface(self): - """Delete interface from router: myid subnetid""" - resource = 'router' - cmd = RemoveInterfaceRouter(MyApp(sys.stdout), None) - args = ['myid', 'subnetid'] - self._test_update_resource_action(resource, cmd, 'myid', - 'remove_router_interface', - args, - {'subnet_id': 'subnetid'} - ) - - def test_set_gateway(self): - """Set external gateway for router: myid externalid""" - resource = 'router' - cmd = SetGatewayRouter(MyApp(sys.stdout), None) - args = ['myid', 'externalid'] - self._test_update_resource(resource, cmd, 'myid', - args, - {"external_gateway_info": - {"network_id": "externalid"}} - ) - - def test_remove_gateway(self): - """Remove external gateway from router: externalid""" - resource = 'router' - cmd = RemoveGatewayRouter(MyApp(sys.stdout), None) - args = ['externalid'] - self._test_update_resource(resource, cmd, 'externalid', - args, {"external_gateway_info": {}} - ) diff --git a/quantumclient/tests/unit/test_cli20_securitygroup.py b/quantumclient/tests/unit/test_cli20_securitygroup.py deleted file mode 100644 index 7a2e941b8..000000000 --- a/quantumclient/tests/unit/test_cli20_securitygroup.py +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Red Hat -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import sys - -import mox - -from quantumclient.quantum.v2_0 import securitygroup -from quantumclient.tests.unit import test_cli20 - - -class CLITestV20SecurityGroups(test_cli20.CLITestV20Base): - def test_create_security_group(self): - """Create security group: webservers.""" - resource = 'security_group' - cmd = securitygroup.CreateSecurityGroup( - test_cli20.MyApp(sys.stdout), None) - name = 'webservers' - myid = 'myid' - args = [name, ] - position_names = ['name'] - position_values = [name] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_security_group_tenant(self): - """Create security group: webservers.""" - resource = 'security_group' - cmd = securitygroup.CreateSecurityGroup( - test_cli20.MyApp(sys.stdout), None) - name = 'webservers' - description = 'my webservers' - myid = 'myid' - args = ['--tenant_id', 'tenant_id', '--description', description, name] - position_names = ['name', 'description'] - position_values = [name, description] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenant_id') - - def test_create_security_group_with_description(self): - """Create security group: webservers.""" - resource = 'security_group' - cmd = securitygroup.CreateSecurityGroup( - test_cli20.MyApp(sys.stdout), None) - name = 'webservers' - description = 'my webservers' - myid = 'myid' - args = [name, '--description', description] - position_names = ['name', 'description'] - position_values = [name, description] - self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_list_security_groups(self): - resources = "security_groups" - cmd = securitygroup.ListSecurityGroup( - test_cli20.MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) - - def test_show_security_group_id(self): - resource = 'security_group' - cmd = securitygroup.ShowSecurityGroup( - test_cli20.MyApp(sys.stdout), None) - args = ['--fields', 'id', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id']) - - def test_show_security_group_id_name(self): - resource = 'security_group' - cmd = securitygroup.ShowSecurityGroup( - test_cli20.MyApp(sys.stdout), None) - args = ['--fields', 'id', '--fields', 'name', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id', 'name']) - - def test_delete_security_group(self): - """Delete security group: myid.""" - resource = 'security_group' - cmd = securitygroup.DeleteSecurityGroup( - test_cli20.MyApp(sys.stdout), None) - myid = 'myid' - args = [myid] - self._test_delete_resource(resource, cmd, myid, args) - - def test_create_security_group_rule_full(self): - """Create security group rule""" - resource = 'security_group_rule' - cmd = securitygroup.CreateSecurityGroupRule( - test_cli20.MyApp(sys.stdout), None) - myid = 'myid' - direction = 'ingress' - ethertype = 'IPv4' - protocol = 'tcp' - port_range_min = '22' - port_range_max = '22' - source_ip_prefix = '10.0.0.0/24' - security_group_id = '1' - source_group_id = '1' - args = ['--source_ip_prefix', source_ip_prefix, '--direction', - direction, '--ethertype', ethertype, '--protocol', protocol, - '--port_range_min', port_range_min, '--port_range_max', - port_range_max, '--source_group_id', source_group_id, - security_group_id] - position_names = ['source_ip_prefix', 'direction', 'ethertype', - 'protocol', 'port_range_min', 'port_range_max', - 'source_group_id', 'security_group_id'] - position_values = [source_ip_prefix, direction, ethertype, protocol, - port_range_min, port_range_max, source_group_id, - security_group_id] - self._test_create_resource(resource, cmd, None, myid, args, - position_names, position_values) - - def test_delete_security_group_rule(self): - """Delete security group rule: myid.""" - resource = 'security_group_rule' - cmd = securitygroup.DeleteSecurityGroupRule( - test_cli20.MyApp(sys.stdout), None) - myid = 'myid' - args = [myid] - self._test_delete_resource(resource, cmd, myid, args) - - def test_list_security_group_rules(self): - resources = "security_group_rules" - cmd = securitygroup.ListSecurityGroupRule( - test_cli20.MyApp(sys.stdout), None) - self.mox.StubOutWithMock(securitygroup.ListSecurityGroupRule, - "extend_list") - securitygroup.ListSecurityGroupRule.extend_list(mox.IsA(list), - mox.IgnoreArg()) - self._test_list_resources(resources, cmd, True) - - def test_show_security_group_rule(self): - resource = 'security_group_rule' - cmd = securitygroup.ShowSecurityGroupRule( - test_cli20.MyApp(sys.stdout), None) - args = ['--fields', 'id', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id']) - - def _test_list_security_group_rules_extend(self, data=None, expected=None, - args=[], conv=True, - query_field=False): - def setup_list_stub(resources, data, query): - reses = {resources: data} - resstr = self.client.serialize(reses) - resp = (test_cli20.MyResp(200), resstr) - path = getattr(self.client, resources + '_path') - self.client.httpclient.request( - test_cli20.end_url(path, query), 'GET', - body=None, - headers=mox.ContainsKeyValue( - 'X-Auth-Token', test_cli20.TOKEN)).AndReturn(resp) - - # Setup the default data - _data = {'cols': ['id', 'security_group_id', 'source_group_id'], - 'data': [('ruleid1', 'myid1', 'myid1'), - ('ruleid2', 'myid2', 'myid3'), - ('ruleid3', 'myid2', 'myid2')]} - _expected = {'cols': ['id', 'security_group', 'source_group'], - 'data': [('ruleid1', 'group1', 'group1'), - ('ruleid2', 'group2', 'group3'), - ('ruleid3', 'group2', 'group2')]} - if data is None: - data = _data - list_data = [dict(zip(data['cols'], d)) for d in data['data']] - if expected is None: - expected = {} - expected['cols'] = expected.get('cols', _expected['cols']) - expected['data'] = expected.get('data', _expected['data']) - - resources = "security_group_rules" - cmd = securitygroup.ListSecurityGroupRule( - test_cli20.MyApp(sys.stdout), None) - self.mox.StubOutWithMock(cmd, 'get_client') - self.mox.StubOutWithMock(self.client.httpclient, 'request') - cmd.get_client().AndReturn(self.client) - query = '' - if query_field: - query = '&'.join(['fields=' + f for f in data['cols']]) - setup_list_stub('security_group_rules', list_data, query) - if conv: - cmd.get_client().AndReturn(self.client) - setup_list_stub('security_groups', - [{'id': 'myid1', 'name': 'group1'}, - {'id': 'myid2', 'name': 'group2'}, - {'id': 'myid3', 'name': 'group3'}], - query='fields=id&fields=name') - self.mox.ReplayAll() - - cmd_parser = cmd.get_parser('list_security_group_rules') - parsed_args = cmd_parser.parse_args(args) - result = cmd.get_data(parsed_args) - self.mox.VerifyAll() - self.mox.UnsetStubs() - # Check columns - self.assertEqual(result[0], expected['cols']) - # Check data - _result = [x for x in result[1]] - self.assertEqual(len(_result), len(expected['data'])) - for res, exp in zip(_result, expected['data']): - self.assertEqual(len(res), len(exp)) - self.assertEqual(res, exp) - - def test_list_security_group_rules_extend_source_id(self): - self._test_list_security_group_rules_extend() - - def test_list_security_group_rules_extend_no_nameconv(self): - expected = {'cols': ['id', 'security_group_id', 'source_group_id'], - 'data': [('ruleid1', 'myid1', 'myid1'), - ('ruleid2', 'myid2', 'myid3'), - ('ruleid3', 'myid2', 'myid2')]} - args = ['--no-nameconv'] - self._test_list_security_group_rules_extend(expected=expected, - args=args, conv=False) - - def test_list_security_group_rules_extend_with_columns(self): - args = '-c id -c security_group_id -c source_group_id'.split() - self._test_list_security_group_rules_extend(args=args) - - def test_list_security_group_rules_extend_with_columns_no_id(self): - args = '-c id -c security_group -c source_group'.split() - self._test_list_security_group_rules_extend(args=args) - - def test_list_security_group_rules_extend_with_fields(self): - args = '-F id -F security_group_id -F source_group_id'.split() - self._test_list_security_group_rules_extend(args=args, - query_field=True) - - def test_list_security_group_rules_extend_with_fields_no_id(self): - args = '-F id -F security_group -F source_group'.split() - self._test_list_security_group_rules_extend(args=args, - query_field=True) diff --git a/quantumclient/tests/unit/test_cli20_subnet.py b/quantumclient/tests/unit/test_cli20_subnet.py deleted file mode 100644 index 9b9650e16..000000000 --- a/quantumclient/tests/unit/test_cli20_subnet.py +++ /dev/null @@ -1,380 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import sys - -from quantumclient.quantum.v2_0.subnet import CreateSubnet -from quantumclient.quantum.v2_0.subnet import DeleteSubnet -from quantumclient.quantum.v2_0.subnet import ListSubnet -from quantumclient.quantum.v2_0.subnet import ShowSubnet -from quantumclient.quantum.v2_0.subnet import UpdateSubnet -from quantumclient.tests.unit.test_cli20 import CLITestV20Base -from quantumclient.tests.unit.test_cli20 import MyApp - - -class CLITestV20Subnet(CLITestV20Base): - - def test_create_subnet(self): - """Create subnet: --gateway gateway netid cidr.""" - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'cidrvalue' - gateway = 'gatewayvalue' - args = ['--gateway', gateway, netid, cidr] - position_names = ['ip_version', 'network_id', 'cidr', 'gateway_ip'] - position_values = [4, netid, cidr, gateway] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_subnet_with_no_gateway(self): - """Create subnet: --no-gateway netid cidr""" - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'cidrvalue' - args = ['--no-gateway', netid, cidr] - position_names = ['ip_version', 'network_id', 'cidr', 'gateway_ip'] - position_values = [4, netid, cidr, None] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - - def test_create_subnet_with_bad_gateway_option(self): - """Create sbunet: --no-gateway netid cidr""" - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'cidrvalue' - gateway = 'gatewayvalue' - args = ['--gateway', gateway, '--no-gateway', netid, cidr] - position_names = ['ip_version', 'network_id', 'cidr', 'gateway_ip'] - position_values = [4, netid, cidr, None] - try: - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values) - except: - return - self.fail('No exception for bad gateway option') - - def test_create_subnet_tenant(self): - """Create subnet: --tenant_id tenantid netid cidr.""" - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = ['--tenant_id', 'tenantid', netid, cidr] - position_names = ['ip_version', 'network_id', 'cidr'] - position_values = [4, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_subnet_tags(self): - """Create subnet: netid cidr --tags a b.""" - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = [netid, cidr, '--tags', 'a', 'b'] - position_names = ['ip_version', 'network_id', 'cidr'] - position_values = [4, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tags=['a', 'b']) - - def test_create_subnet_allocation_pool(self): - """Create subnet: --tenant_id tenantid netid cidr. - The is --allocation_pool start=1.1.1.10,end=1.1.1.20 - """ - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = ['--tenant_id', 'tenantid', - '--allocation_pool', 'start=1.1.1.10,end=1.1.1.20', - netid, cidr] - position_names = ['ip_version', 'allocation_pools', 'network_id', - 'cidr'] - pool = [{'start': '1.1.1.10', 'end': '1.1.1.20'}] - position_values = [4, pool, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_subnet_allocation_pools(self): - """Create subnet: --tenant-id tenantid netid cidr. - The are --allocation_pool start=1.1.1.10,end=1.1.1.20 and - --allocation_pool start=1.1.1.30,end=1.1.1.40 - """ - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = ['--tenant_id', 'tenantid', - '--allocation_pool', 'start=1.1.1.10,end=1.1.1.20', - '--allocation_pool', 'start=1.1.1.30,end=1.1.1.40', - netid, cidr] - position_names = ['ip_version', 'allocation_pools', 'network_id', - 'cidr'] - pools = [{'start': '1.1.1.10', 'end': '1.1.1.20'}, - {'start': '1.1.1.30', 'end': '1.1.1.40'}] - position_values = [4, pools, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_subnet_host_route(self): - """Create subnet: --tenant_id tenantid netid cidr. - The is - --host-route destination=172.16.1.0/24,nexthop=1.1.1.20 - """ - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = ['--tenant_id', 'tenantid', - '--host-route', 'destination=172.16.1.0/24,nexthop=1.1.1.20', - netid, cidr] - position_names = ['ip_version', 'host_routes', 'network_id', - 'cidr'] - route = [{'destination': '172.16.1.0/24', 'nexthop': '1.1.1.20'}] - position_values = [4, route, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_subnet_host_routes(self): - """Create subnet: --tenant-id tenantid netid cidr. - The are - --host-route destination=172.16.1.0/24,nexthop=1.1.1.20 and - --host-route destination=172.17.7.0/24,nexthop=1.1.1.40 - """ - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = ['--tenant_id', 'tenantid', - '--host-route', 'destination=172.16.1.0/24,nexthop=1.1.1.20', - '--host-route', 'destination=172.17.7.0/24,nexthop=1.1.1.40', - netid, cidr] - position_names = ['ip_version', 'host_routes', 'network_id', - 'cidr'] - routes = [{'destination': '172.16.1.0/24', 'nexthop': '1.1.1.20'}, - {'destination': '172.17.7.0/24', 'nexthop': '1.1.1.40'}] - position_values = [4, routes, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_subnet_dns_nameservers(self): - """Create subnet: --tenant-id tenantid netid cidr. - The are - --dns-nameserver 1.1.1.20 and --dns-nameserver 1.1.1.40 - """ - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = ['--tenant_id', 'tenantid', - '--dns-nameserver', '1.1.1.20', - '--dns-nameserver', '1.1.1.40', - netid, cidr] - position_names = ['ip_version', 'dns_nameservers', 'network_id', - 'cidr'] - nameservers = ['1.1.1.20', '1.1.1.40'] - position_values = [4, nameservers, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_subnet_with_disable_dhcp(self): - """Create subnet: --tenant-id tenantid --disable-dhcp netid cidr.""" - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = ['--tenant_id', 'tenantid', - '--disable-dhcp', - netid, cidr] - position_names = ['ip_version', 'enable_dhcp', 'network_id', - 'cidr'] - position_values = [4, False, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_subnet_merge_single_plurar(self): - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = ['--tenant_id', 'tenantid', - '--allocation-pool', 'start=1.1.1.10,end=1.1.1.20', - netid, cidr, - '--allocation-pools', 'list=true', 'type=dict', - 'start=1.1.1.30,end=1.1.1.40'] - position_names = ['ip_version', 'allocation_pools', 'network_id', - 'cidr'] - pools = [{'start': '1.1.1.10', 'end': '1.1.1.20'}, - {'start': '1.1.1.30', 'end': '1.1.1.40'}] - position_values = [4, pools, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_subnet_merge_plurar(self): - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = ['--tenant_id', 'tenantid', - netid, cidr, - '--allocation-pools', 'list=true', 'type=dict', - 'start=1.1.1.30,end=1.1.1.40'] - position_names = ['ip_version', 'allocation_pools', 'network_id', - 'cidr'] - pools = [{'start': '1.1.1.30', 'end': '1.1.1.40'}] - position_values = [4, pools, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_create_subnet_merge_single_single(self): - resource = 'subnet' - cmd = CreateSubnet(MyApp(sys.stdout), None) - name = 'myname' - myid = 'myid' - netid = 'netid' - cidr = 'prefixvalue' - args = ['--tenant_id', 'tenantid', - '--allocation-pool', 'start=1.1.1.10,end=1.1.1.20', - netid, cidr, - '--allocation-pool', - 'start=1.1.1.30,end=1.1.1.40'] - position_names = ['ip_version', 'allocation_pools', 'network_id', - 'cidr'] - pools = [{'start': '1.1.1.10', 'end': '1.1.1.20'}, - {'start': '1.1.1.30', 'end': '1.1.1.40'}] - position_values = [4, pools, netid, cidr] - _str = self._test_create_resource(resource, cmd, name, myid, args, - position_names, position_values, - tenant_id='tenantid') - - def test_list_subnets_detail(self): - """List subnets: -D.""" - resources = "subnets" - cmd = ListSubnet(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, True) - - def test_list_subnets_tags(self): - """List subnets: -- --tags a b.""" - resources = "subnets" - cmd = ListSubnet(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, tags=['a', 'b']) - - def test_list_subnets_known_option_after_unknown(self): - """List subnets: -- --tags a b --request-format xml.""" - resources = "subnets" - cmd = ListSubnet(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, tags=['a', 'b']) - - def test_list_subnets_detail_tags(self): - """List subnets: -D -- --tags a b.""" - resources = "subnets" - cmd = ListSubnet(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, detail=True, tags=['a', 'b']) - - def test_list_subnets_fields(self): - """List subnets: --fields a --fields b -- --fields c d.""" - resources = "subnets" - cmd = ListSubnet(MyApp(sys.stdout), None) - self._test_list_resources(resources, cmd, - fields_1=['a', 'b'], fields_2=['c', 'd']) - - def test_update_subnet(self): - """Update subnet: myid --name myname --tags a b.""" - resource = 'subnet' - cmd = UpdateSubnet(MyApp(sys.stdout), None) - self._test_update_resource(resource, cmd, 'myid', - ['myid', '--name', 'myname', - '--tags', 'a', 'b'], - {'name': 'myname', 'tags': ['a', 'b'], } - ) - - def test_update_subnet_known_option_before_id(self): - """Update subnet: --request-format json myid --name myname.""" - # --request-format xml is known option - resource = 'subnet' - cmd = UpdateSubnet(MyApp(sys.stdout), None) - self._test_update_resource(resource, cmd, 'myid', - ['--request-format', 'json', - 'myid', '--name', 'myname'], - {'name': 'myname', } - ) - - def test_update_subnet_known_option_after_id(self): - """Update subnet: myid --name myname --request-format json.""" - # --request-format xml is known option - resource = 'subnet' - cmd = UpdateSubnet(MyApp(sys.stdout), None) - self._test_update_resource(resource, cmd, 'myid', - ['myid', '--name', 'myname', - '--request-format', 'json'], - {'name': 'myname', } - ) - - def test_show_subnet(self): - """Show subnet: --fields id --fields name myid.""" - resource = 'subnet' - cmd = ShowSubnet(MyApp(sys.stdout), None) - args = ['--fields', 'id', '--fields', 'name', self.test_id] - self._test_show_resource(resource, cmd, self.test_id, - args, ['id', 'name']) - - def test_delete_subnet(self): - """Delete subnet: subnetid.""" - resource = 'subnet' - cmd = DeleteSubnet(MyApp(sys.stdout), None) - myid = 'myid' - args = [myid] - self._test_delete_resource(resource, cmd, myid, args) diff --git a/quantumclient/tests/unit/test_name_or_id.py b/quantumclient/tests/unit/test_name_or_id.py deleted file mode 100644 index 3b01a1c9b..000000000 --- a/quantumclient/tests/unit/test_name_or_id.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import uuid - -import mox -from mox import ContainsKeyValue -import testtools - -from quantumclient.common import exceptions -from quantumclient.quantum import v2_0 as quantumv20 -from quantumclient.tests.unit import test_cli20 -from quantumclient.v2_0.client import Client - - -class CLITestNameorID(testtools.TestCase): - - def setUp(self): - """Prepare the test environment""" - super(CLITestNameorID, self).setUp() - self.mox = mox.Mox() - self.endurl = test_cli20.ENDURL - self.client = Client(token=test_cli20.TOKEN, endpoint_url=self.endurl) - self.addCleanup(self.mox.VerifyAll) - self.addCleanup(self.mox.UnsetStubs) - - def test_get_id_from_id(self): - _id = str(uuid.uuid4()) - reses = {'networks': [{'id': _id, }, ], } - resstr = self.client.serialize(reses) - self.mox.StubOutWithMock(self.client.httpclient, "request") - path = getattr(self.client, "networks_path") - self.client.httpclient.request( - test_cli20.end_url(path, "fields=id&id=" + _id), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn( - (test_cli20.MyResp(200), resstr)) - self.mox.ReplayAll() - returned_id = quantumv20.find_resourceid_by_name_or_id( - self.client, 'network', _id) - self.assertEqual(_id, returned_id) - - def test_get_id_from_id_then_name_empty(self): - _id = str(uuid.uuid4()) - reses = {'networks': [{'id': _id, }, ], } - resstr = self.client.serialize(reses) - resstr1 = self.client.serialize({'networks': []}) - self.mox.StubOutWithMock(self.client.httpclient, "request") - path = getattr(self.client, "networks_path") - self.client.httpclient.request( - test_cli20.end_url(path, "fields=id&id=" + _id), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn( - (test_cli20.MyResp(200), resstr1)) - self.client.httpclient.request( - test_cli20.end_url(path, "fields=id&name=" + _id), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn( - (test_cli20.MyResp(200), resstr)) - self.mox.ReplayAll() - returned_id = quantumv20.find_resourceid_by_name_or_id( - self.client, 'network', _id) - self.assertEqual(_id, returned_id) - - def test_get_id_from_name(self): - name = 'myname' - _id = str(uuid.uuid4()) - reses = {'networks': [{'id': _id, }, ], } - resstr = self.client.serialize(reses) - self.mox.StubOutWithMock(self.client.httpclient, "request") - path = getattr(self.client, "networks_path") - self.client.httpclient.request( - test_cli20.end_url(path, "fields=id&name=" + name), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn( - (test_cli20.MyResp(200), resstr)) - self.mox.ReplayAll() - returned_id = quantumv20.find_resourceid_by_name_or_id( - self.client, 'network', name) - self.assertEqual(_id, returned_id) - - def test_get_id_from_name_multiple(self): - name = 'myname' - reses = {'networks': [{'id': str(uuid.uuid4())}, - {'id': str(uuid.uuid4())}]} - resstr = self.client.serialize(reses) - self.mox.StubOutWithMock(self.client.httpclient, "request") - path = getattr(self.client, "networks_path") - self.client.httpclient.request( - test_cli20.end_url(path, "fields=id&name=" + name), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn( - (test_cli20.MyResp(200), resstr)) - self.mox.ReplayAll() - try: - quantumv20.find_resourceid_by_name_or_id( - self.client, 'network', name) - except exceptions.QuantumClientException as ex: - self.assertTrue('Multiple' in ex.message) - - def test_get_id_from_name_notfound(self): - name = 'myname' - reses = {'networks': []} - resstr = self.client.serialize(reses) - self.mox.StubOutWithMock(self.client.httpclient, "request") - path = getattr(self.client, "networks_path") - self.client.httpclient.request( - test_cli20.end_url(path, "fields=id&name=" + name), 'GET', - body=None, - headers=ContainsKeyValue('X-Auth-Token', - test_cli20.TOKEN)).AndReturn( - (test_cli20.MyResp(200), resstr)) - self.mox.ReplayAll() - try: - quantumv20.find_resourceid_by_name_or_id( - self.client, 'network', name) - except exceptions.QuantumClientException as ex: - self.assertTrue('Unable to find' in ex.message) - self.assertEqual(404, ex.status_code) diff --git a/quantumclient/v2_0/__init__.py b/quantumclient/v2_0/__init__.py deleted file mode 100644 index 63c3905e2..000000000 --- a/quantumclient/v2_0/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. diff --git a/quantumclient/v2_0/client.py b/quantumclient/v2_0/client.py deleted file mode 100644 index 883b208be..000000000 --- a/quantumclient/v2_0/client.py +++ /dev/null @@ -1,857 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import httplib -import logging -import time -import urllib - -from quantumclient.client import HTTPClient -from quantumclient.common import _ -from quantumclient.common import constants -from quantumclient.common import exceptions -from quantumclient.common import serializer - - -_logger = logging.getLogger(__name__) - - -def exception_handler_v20(status_code, error_content): - """ Exception handler for API v2.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_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 - bad_quantum_error_flag = False - if error_dict: - # If QuantumError key is found, it will definitely contain - # a 'message' and 'type' keys? - try: - error_type = error_dict['type'] - error_message = (error_dict['message'] + "\n" + - error_dict['detail']) - except Exception: - bad_quantum_error_flag = True - if not bad_quantum_error_flag: - ex = None - try: - # raise the appropriate error! - ex = quantum_errors[error_type](message=error_message) - ex.args = ([dict(status_code=status_code, - message=error_message)], ) - except Exception: - pass - if ex: - raise ex - else: - raise exceptions.QuantumClientException(status_code=status_code, - message=error_dict) - else: - message = None - if isinstance(error_content, dict): - message = error_content.get('message', None) - if message: - raise exceptions.QuantumClientException(status_code=status_code, - message=message) - - # If we end up here the exception was not a quantum error - msg = "%s-%s" % (status_code, error_content) - raise exceptions.QuantumClientException(status_code=status_code, - message=msg) - - -class APIParamsCall(object): - """A Decorator to add support for format and tenant overriding - and filters - """ - def __init__(self, function): - self.function = function - - def __get__(self, instance, owner): - def with_params(*args, **kwargs): - _format = instance.format - if 'format' in kwargs: - instance.format = kwargs['format'] - ret = self.function(instance, *args, **kwargs) - instance.format = _format - return ret - return with_params - - -class Client(object): - """Client for the OpenStack Quantum v2.0 API. - - :param string username: Username for authentication. (optional) - :param string password: Password for authentication. (optional) - :param string token: Token for authentication. (optional) - :param string tenant_name: Tenant name. (optional) - :param string auth_url: Keystone service endpoint for authorization. - :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 quantum - 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 insecure: ssl certificate validation. (optional) - - Example:: - - >>> from quantumclient.v2_0 import client - >>> quantum = client.Client(username=USER, - password=PASS, - tenant_name=TENANT_NAME, - auth_url=KEYSTONE_URL) - - >>> nets = quantum.list_networks() - ... - - """ - - networks_path = "/networks" - network_path = "/networks/%s" - ports_path = "/ports" - port_path = "/ports/%s" - subnets_path = "/subnets" - subnet_path = "/subnets/%s" - quotas_path = "/quotas" - quota_path = "/quotas/%s" - exts_path = "/extensions" - ext_path = "/extensions/%s" - routers_path = "/routers" - router_path = "/routers/%s" - floatingips_path = "/floatingips" - floatingip_path = "/floatingips/%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" - 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" - - # 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', - 'vips': 'vip', - 'pools': 'pool', - 'members': 'member', - 'health_monitors': 'health_monitor', - 'quotas': 'quota', - } - - def get_attr_metadata(self): - if self.format == 'json': - return {} - old_request_format = self.format - self.format = 'json' - exts = self.list_extensions()['extensions'] - self.format = old_request_format - ns = dict([(ext['alias'], ext['namespace']) for ext in exts]) - self.EXTED_PLURALS.update(constants.PLURALS) - return {'plurals': self.EXTED_PLURALS, - 'xmlns': constants.XML_NS_V20, - constants.EXT_NS: ns} - - @APIParamsCall - def get_quotas_tenant(self, **_params): - """Fetch tenant info in server's context for - following quota operation.""" - return self.get(self.quota_path % 'tenant', params=_params) - - @APIParamsCall - def list_quotas(self, **_params): - """Fetch all tenants' quotas.""" - return self.get(self.quotas_path, params=_params) - - @APIParamsCall - def show_quota(self, tenant_id, **_params): - """Fetch information of a certain tenant's quotas.""" - return self.get(self.quota_path % (tenant_id), params=_params) - - @APIParamsCall - def update_quota(self, tenant_id, body=None): - """Update a tenant's quotas.""" - return self.put(self.quota_path % (tenant_id), body=body) - - @APIParamsCall - def delete_quota(self, tenant_id): - """Delete the specified tenant's quota values.""" - return self.delete(self.quota_path % (tenant_id)) - - @APIParamsCall - def list_extensions(self, **_params): - """Fetch a list of all exts on server side.""" - return self.get(self.exts_path, params=_params) - - @APIParamsCall - def show_extension(self, ext_alias, **_params): - """Fetch a list of all exts on server side.""" - return self.get(self.ext_path % ext_alias, params=_params) - - @APIParamsCall - def list_ports(self, **_params): - """ - Fetches a list of all networks for a tenant - """ - # Pass filters in "params" argument to do_request - return self.get(self.ports_path, params=_params) - - @APIParamsCall - def show_port(self, port, **_params): - """ - Fetches information of a certain network - """ - return self.get(self.port_path % (port), params=_params) - - @APIParamsCall - def create_port(self, body=None): - """ - Creates a new port - """ - return self.post(self.ports_path, body=body) - - @APIParamsCall - def update_port(self, port, body=None): - """ - Updates a port - """ - return self.put(self.port_path % (port), body=body) - - @APIParamsCall - def delete_port(self, port): - """ - Deletes the specified port - """ - return self.delete(self.port_path % (port)) - - @APIParamsCall - def list_networks(self, **_params): - """ - Fetches a list of all networks for a tenant - """ - # Pass filters in "params" argument to do_request - return self.get(self.networks_path, params=_params) - - @APIParamsCall - def show_network(self, network, **_params): - """ - Fetches information of a certain network - """ - return self.get(self.network_path % (network), params=_params) - - @APIParamsCall - def create_network(self, body=None): - """ - Creates a new network - """ - return self.post(self.networks_path, body=body) - - @APIParamsCall - def update_network(self, network, body=None): - """ - Updates a network - """ - return self.put(self.network_path % (network), body=body) - - @APIParamsCall - def delete_network(self, network): - """ - Deletes the specified network - """ - return self.delete(self.network_path % (network)) - - @APIParamsCall - def list_subnets(self, **_params): - """ - Fetches a list of all networks for a tenant - """ - return self.get(self.subnets_path, params=_params) - - @APIParamsCall - def show_subnet(self, subnet, **_params): - """ - Fetches information of a certain subnet - """ - return self.get(self.subnet_path % (subnet), params=_params) - - @APIParamsCall - def create_subnet(self, body=None): - """ - Creates a new subnet - """ - return self.post(self.subnets_path, body=body) - - @APIParamsCall - def update_subnet(self, subnet, body=None): - """ - Updates a subnet - """ - return self.put(self.subnet_path % (subnet), body=body) - - @APIParamsCall - def delete_subnet(self, subnet): - """ - Deletes the specified subnet - """ - return self.delete(self.subnet_path % (subnet)) - - @APIParamsCall - def list_routers(self, **_params): - """ - Fetches a list of all routers for a tenant - """ - # Pass filters in "params" argument to do_request - return self.get(self.routers_path, params=_params) - - @APIParamsCall - def show_router(self, router, **_params): - """ - Fetches information of a certain router - """ - return self.get(self.router_path % (router), params=_params) - - @APIParamsCall - def create_router(self, body=None): - """ - Creates a new router - """ - return self.post(self.routers_path, body=body) - - @APIParamsCall - def update_router(self, router, body=None): - """ - Updates a router - """ - return self.put(self.router_path % (router), body=body) - - @APIParamsCall - def delete_router(self, router): - """ - Deletes the specified router - """ - return self.delete(self.router_path % (router)) - - @APIParamsCall - 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) - - @APIParamsCall - 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) - - @APIParamsCall - 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}}) - - @APIParamsCall - 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': {}}}) - - @APIParamsCall - def list_floatingips(self, **_params): - """ - Fetches a list of all floatingips for a tenant - """ - # Pass filters in "params" argument to do_request - return self.get(self.floatingips_path, params=_params) - - @APIParamsCall - def show_floatingip(self, floatingip, **_params): - """ - Fetches information of a certain floatingip - """ - return self.get(self.floatingip_path % (floatingip), params=_params) - - @APIParamsCall - def create_floatingip(self, body=None): - """ - Creates a new floatingip - """ - return self.post(self.floatingips_path, body=body) - - @APIParamsCall - def update_floatingip(self, floatingip, body=None): - """ - Updates a floatingip - """ - return self.put(self.floatingip_path % (floatingip), body=body) - - @APIParamsCall - def delete_floatingip(self, floatingip): - """ - Deletes the specified floatingip - """ - return self.delete(self.floatingip_path % (floatingip)) - - @APIParamsCall - def create_security_group(self, body=None): - """ - Creates a new security group - """ - return self.post(self.security_groups_path, body=body) - - @APIParamsCall - def list_security_groups(self, **_params): - """ - Fetches a list of all security groups for a tenant - """ - return self.get(self.security_groups_path, params=_params) - - @APIParamsCall - 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) - - @APIParamsCall - def delete_security_group(self, security_group): - """ - Deletes the specified security group - """ - return self.delete(self.security_group_path % (security_group)) - - @APIParamsCall - def create_security_group_rule(self, body=None): - """ - Creates a new security group rule - """ - return self.post(self.security_group_rules_path, body=body) - - @APIParamsCall - 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)) - - @APIParamsCall - def list_security_group_rules(self, **_params): - """ - Fetches a list of all security group rules for a tenant - """ - return self.get(self.security_group_rules_path, params=_params) - - @APIParamsCall - 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) - - @APIParamsCall - def list_vips(self, **_params): - """ - Fetches a list of all load balancer vips for a tenant - """ - # Pass filters in "params" argument to do_request - return self.get(self.vips_path, params=_params) - - @APIParamsCall - def show_vip(self, vip, **_params): - """ - Fetches information of a certain load balancer vip - """ - return self.get(self.vip_path % (vip), params=_params) - - @APIParamsCall - def create_vip(self, body=None): - """ - Creates a new load balancer vip - """ - return self.post(self.vips_path, body=body) - - @APIParamsCall - def update_vip(self, vip, body=None): - """ - Updates a load balancer vip - """ - return self.put(self.vip_path % (vip), body=body) - - @APIParamsCall - def delete_vip(self, vip): - """ - Deletes the specified load balancer vip - """ - return self.delete(self.vip_path % (vip)) - - @APIParamsCall - def list_pools(self, **_params): - """ - Fetches a list of all load balancer pools for a tenant - """ - # Pass filters in "params" argument to do_request - return self.get(self.pools_path, params=_params) - - @APIParamsCall - def show_pool(self, pool, **_params): - """ - Fetches information of a certain load balancer pool - """ - return self.get(self.pool_path % (pool), params=_params) - - @APIParamsCall - def create_pool(self, body=None): - """ - Creates a new load balancer pool - """ - return self.post(self.pools_path, body=body) - - @APIParamsCall - def update_pool(self, pool, body=None): - """ - Updates a load balancer pool - """ - return self.put(self.pool_path % (pool), body=body) - - @APIParamsCall - def delete_pool(self, pool): - """ - Deletes the specified load balancer pool - """ - return self.delete(self.pool_path % (pool)) - - @APIParamsCall - 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) - - @APIParamsCall - def list_members(self, **_params): - """ - Fetches a list of all load balancer members for a tenant - """ - # Pass filters in "params" argument to do_request - return self.get(self.members_path, params=_params) - - @APIParamsCall - def show_member(self, member, **_params): - """ - Fetches information of a certain load balancer member - """ - return self.get(self.member_path % (member), params=_params) - - @APIParamsCall - def create_member(self, body=None): - """ - Creates a new load balancer member - """ - return self.post(self.members_path, body=body) - - @APIParamsCall - def update_member(self, member, body=None): - """ - Updates a load balancer member - """ - return self.put(self.member_path % (member), body=body) - - @APIParamsCall - def delete_member(self, member): - """ - Deletes the specified load balancer member - """ - return self.delete(self.member_path % (member)) - - @APIParamsCall - def list_health_monitors(self, **_params): - """ - Fetches a list of all load balancer health monitors for a tenant - """ - # Pass filters in "params" argument to do_request - return self.get(self.health_monitors_path, params=_params) - - @APIParamsCall - 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) - - @APIParamsCall - def create_health_monitor(self, body=None): - """ - Creates a new load balancer health monitor - """ - return self.post(self.health_monitors_path, body=body) - - @APIParamsCall - 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) - - @APIParamsCall - def delete_health_monitor(self, health_monitor): - """ - Deletes the specified load balancer health monitor - """ - return self.delete(self.health_monitor_path % (health_monitor)) - - @APIParamsCall - 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) - - @APIParamsCall - 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) - - @APIParamsCall - def create_qos_queue(self, body=None): - """ - Creates a new queue - """ - return self.post(self.qos_queues_path, body=body) - - @APIParamsCall - def list_qos_queues(self, **_params): - """ - Fetches a list of all queues for a tenant - """ - return self.get(self.qos_queues_path, params=_params) - - @APIParamsCall - def show_qos_queue(self, queue, **_params): - """ - Fetches information of a certain queue - """ - return self.get(self.qos_queue_path % (queue), - params=_params) - - @APIParamsCall - def delete_qos_queue(self, queue): - """ - Deletes the specified queue - """ - return self.delete(self.qos_queue_path % (queue)) - - @APIParamsCall - def list_agents(self, **_params): - """ - Fetches agents - """ - # Pass filters in "params" argument to do_request - return self.get(self.agents_path, params=_params) - - @APIParamsCall - def show_agent(self, agent, **_params): - """ - Fetches information of a certain agent - """ - return self.get(self.agent_path % (agent), params=_params) - - @APIParamsCall - def update_agent(self, agent, body=None): - """ - Updates an agent - """ - return self.put(self.agent_path % (agent), body=body) - - @APIParamsCall - def delete_agent(self, agent): - """ - Deletes the specified agent - """ - return self.delete(self.agent_path % (agent)) - - def __init__(self, **kwargs): - """ Initialize a new client for the Quantum v2.0 API. """ - super(Client, self).__init__() - self.httpclient = HTTPClient(**kwargs) - self.version = '2.0' - self.format = 'json' - self.action_prefix = "/v%s" % (self.version) - self.retries = 0 - self.retry_interval = 1 - - def _handle_fault_response(self, status_code, response_body): - # Create exception with HTTP status code and message - _logger.debug("Error message: %s", response_body) - # Add deserialized error message to exception arguments - try: - des_error_body = self.deserialize(response_body, status_code) - except: - # If unable to deserialized body it is probably not a - # Quantum error - des_error_body = {'message': response_body} - # Raise the appropriate exception - exception_handler_v20(status_code, des_error_body) - - def do_request(self, method, action, body=None, headers=None, params=None): - # Add format and tenant_id - action += ".%s" % self.format - action = self.action_prefix + action - if type(params) is dict and params: - action += '?' + urllib.urlencode(params, doseq=1) - if body: - body = self.serialize(body) - self.httpclient.content_type = self.content_type() - resp, replybody = self.httpclient.do_request(action, method, body=body) - status_code = self.get_status_code(resp) - if status_code in (httplib.OK, - httplib.CREATED, - httplib.ACCEPTED, - httplib.NO_CONTENT): - return self.deserialize(replybody, status_code) - else: - self._handle_fault_response(status_code, replybody) - - 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.Serializer( - self.get_attr_metadata()).serialize(data, self.content_type()) - else: - raise Exception("unable to serialize object of type = '%s'" % - type(data)) - - def deserialize(self, data, status_code): - """ - Deserializes an xml or json string into a dictionary - """ - if status_code == 204: - return data - return serializer.Serializer(self.get_attr_metadata()).deserialize( - data, self.content_type())['body'] - - def content_type(self, _format=None): - """ - Returns the mime-type for either 'xml' or 'json'. Defaults to the - currently set format - """ - _format = _format or self.format - return "application/%s" % (_format) - - def retry_request(self, method, action, body=None, - headers=None, params=None): - """ - Call do_request with the default retry configuration. Only - idempotent requests should retry failed connection attempts. - - :raises: ConnectionFailed if the maximum # of retries is exceeded - """ - max_attempts = self.retries + 1 - for i in 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: - _logger.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) 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 11d2c4422..102a204d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,70 @@ -[build_sphinx] -all_files = 1 -build-dir = doc/build -source-dir = doc/source +[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 + + 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 + + network_onboard_subnets = neutronclient.osc.v2.subnet_onboard.subnet_onboard:NetworkOnboardSubnets diff --git a/setup.py b/setup.py index c3d851cbd..cd35c3c35 100644 --- a/setup.py +++ b/setup.py @@ -1,81 +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. +# 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 -import sys import setuptools -from quantumclient.openstack.common import setup - -Name = 'python-quantumclient' -Url = "https://launchpad.net/quantum" -Version = setup.get_post_version('quantumclient') -License = 'Apache License 2.0' -Author = 'OpenStack Quantum Project' -AuthorEmail = 'openstack-dev@lists.launchpad.net' -Maintainer = '' -Summary = 'CLI and python client library for OpenStack Quantum' -ShortDescription = Summary -Description = Summary - -dependency_links = setup.parse_dependency_links() -tests_require = setup.parse_requirements(['tools/test-requires']) - -EagerResources = [ -] - -ProjectScripts = [ -] - -PackageData = { -} - - setuptools.setup( - name=Name, - version=Version, - url=Url, - author=Author, - author_email=AuthorEmail, - description=ShortDescription, - long_description=Description, - license=License, - classifiers=[ - 'Environment :: OpenStack', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - ], - scripts=ProjectScripts, - dependency_links=dependency_links, - install_requires=setup.parse_requirements(), - tests_require=tests_require, - cmdclass=setup.get_cmdclass(), - include_package_data=False, - packages=setuptools.find_packages('.'), - package_data=PackageData, - eager_resources=EagerResources, - entry_points={ - 'console_scripts': [ - 'quantum = quantumclient.shell:main', - ] - }, -) + 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/pip-requires b/tools/pip-requires deleted file mode 100644 index cfa62bb55..000000000 --- a/tools/pip-requires +++ /dev/null @@ -1,7 +0,0 @@ -argparse -cliff>=1.2.1 -httplib2 -iso8601 -prettytable>=0.6,<0.7 -pyparsing>=1.5.6,<2.0 -simplejson diff --git a/tools/quantum.bash_completion b/tools/quantum.bash_completion deleted file mode 100644 index 311f53309..000000000 --- a/tools/quantum.bash_completion +++ /dev/null @@ -1,27 +0,0 @@ -_quantum_opts="" # lazy init -_quantum_flags="" # lazy init -_quantum_opts_exp="" # lazy init -_quantum() -{ - local cur prev nbc cflags - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - - if [ "x$_quantum_opts" == "x" ] ; then - nbc="`quantum bash-completion`" - _quantum_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" - _quantum_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" - _quantum_opts_exp="`echo "$_quantum_opts" | sed -e "s/\s/|/g"`" - fi - - if [[ " ${COMP_WORDS[@]} " =~ " "($_quantum_opts_exp)" " && "$prev" != "help" ]] ; then - COMPLETION_CACHE=~/.quantumclient/*/*-cache - cflags="$_quantum_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') - COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) - else - COMPREPLY=($(compgen -W "${_quantum_opts}" -- ${cur})) - fi - return 0 -} -complete -F _quantum quantum diff --git a/tools/test-requires b/tools/test-requires deleted file mode 100644 index b82e805ed..000000000 --- a/tools/test-requires +++ /dev/null @@ -1,11 +0,0 @@ -cliff-tablib>=1.0 -coverage -discover -distribute>=0.6.24 -fixtures>=0.3.12 -mox -pep8 -python-subunit -sphinx>=1.1.2 -testrepository>=0.0.13 -testtools>=0.9.22 diff --git a/tox.ini b/tox.ini index dbb367d4d..a939f8b65 100644 --- a/tox.ini +++ b/tox.ini @@ -1,23 +1,94 @@ [tox] -envlist = py26,py27,pep8 +envlist = py39,pep8 +minversion = 3.18.0 +skipsdist = False +ignore_basepython_conflict = True [testenv] +basepython = python3 setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=C - -deps = -r{toxinidir}/tools/test-requires -commands = python setup.py testr --testr-args='{posargs}' + 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 --repeat --show-source --ignore=E125 --exclude=.venv,.tox,dist,doc . +commands = + flake8 + {[testenv:bandit]commands} +distribute = false [testenv:venv] commands = {posargs} [testenv:cover] -commands = python setup.py testr --coverage --testr-args='{posargs}' +setenv = + {[testenv]setenv} + PYTHON=coverage run --source neutronclient --parallel-mode +commands = + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report + +[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: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: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 + +[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 + +# 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 -[tox:jenkins] -downloadcache = ~/cache/pip +[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