diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 000000000..94e40afbb
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,7 @@
+[run]
+branch = True
+source = neutronclient
+omit = neutronclient/tests/*
+
+[report]
+ignore_errors = True
diff --git a/.gitignore b/.gitignore
index 15eae7e22..abdf7b022 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,16 +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
+.coverage
+.stestr/
+.tox/
+.venv/
diff --git a/.gitreview b/.gitreview
index 40817febd..faf5d1f5b 100644
--- a/.gitreview
+++ b/.gitreview
@@ -1,4 +1,4 @@
[gerrit]
-host=review.openstack.org
+host=review.opendev.org
port=29418
-project=openstack/python-quantumclient.git
+project=openstack/python-neutronclient.git
diff --git a/.pylintrc b/.pylintrc
index bd677fd6a..7e4a0716c 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -20,7 +20,7 @@ variable-rgx=[a-z_][a-z0-9_]{0,30}$
argument-rgx=[a-z_][a-z0-9_]{1,30}$
# Method names should be at least 3 characters long
-# and be lowecased with underscores
+# and be lowercased with underscores
method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$
# Module names matching quantum-* are ok (files in bin/)
diff --git a/.stestr.conf b/.stestr.conf
new file mode 100644
index 000000000..875bb1461
--- /dev/null
+++ b/.stestr.conf
@@ -0,0 +1,3 @@
+[DEFAULT]
+test_path=${OS_TEST_PATH:-./neutronclient/tests/unit}
+top_dir=./
diff --git a/.zuul.yaml b/.zuul.yaml
new file mode 100644
index 000000000..7c8822186
--- /dev/null
+++ b/.zuul.yaml
@@ -0,0 +1,39 @@
+- project:
+ templates:
+ - openstack-cover-jobs
+ - openstack-python3-jobs
+ - publish-openstack-docs-pti
+ - check-requirements
+ - lib-forward-testing-python3
+ - release-notes-jobs-python3
+ - openstackclient-plugin-jobs
+ experimental:
+ jobs:
+ - neutronclient-grenade-neutron-lib:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^setup.cfg$
+
+- job:
+ name: neutronclient-grenade-neutron-lib
+ parent: grenade
+ description: |
+ neutron-lib grenade job.
+ The version of this job on the current branch is py3 based,
+ while any branch before ussuri needs to use the py2 version,
+ which is defined in openstack-zuul-jobs with the old name
+ (legacy-grenade-dsvm-neutron-libs).
+ Users of this job needs to pay attention of the version used.
+ Former names for this job were:
+ * legacy-grenade-dsvm-neutron-libs
+ * neutron-lib-grenade-dsvm
+ required-projects:
+ - openstack/keystoneauth
+ - openstack/neutron
+ - openstack/neutron-lib
+ - openstack/python-cinderclient
+ - openstack/python-glanceclient
+ - openstack/python-ironicclient
+ - openstack/python-keystoneclient
+ - openstack/python-neutronclient
+ - openstack/python-novaclient
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 000000000..b726f1d6b
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,16 @@
+If you would like to contribute to the development of OpenStack,
+you must follow the steps documented at:
+
+ http://docs.openstack.org/infra/manual/developers.html#development-workflow
+
+Once those steps have been completed, changes to OpenStack
+should be submitted for review via the Gerrit tool, following
+the workflow documented at:
+
+ http://docs.openstack.org/infra/manual/developers.html#development-workflow
+
+Pull requests submitted through GitHub will be ignored.
+
+Bugs should be filed on Launchpad, not GitHub:
+
+ https://bugs.launchpad.net/python-neutronclient
diff --git a/HACKING.rst b/HACKING.rst
index d99a07f9a..7230971af 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -1,187 +1,26 @@
-QuantumClient Style Commandments
+Neutron Style Commandments
================================
-- Step 1: Read http://www.python.org/dev/peps/pep-0008/
-- Step 2: Read http://www.python.org/dev/peps/pep-0008/ again
-- Step 3: Read on
-
-
-General
--------
-- Put two newlines between top-level code (funcs, classes, etc)
-- Put one newline between methods in classes and anywhere else
-- Do not write "except:", use "except Exception:" at the very least
-- Include your name with TODOs as in "#TODO(termie)"
-- Do not shadow a built-in or reserved word. Example::
-
- def list():
- return [1, 2, 3]
-
- mylist = list() # BAD, shadows `list` built-in
-
- class Foo(object):
- def list(self):
- return [1, 2, 3]
-
- mylist = Foo().list() # OKAY, does not shadow built-in
-
-
-Imports
--------
-- Do not make relative imports
-- Order your imports by the full module path
-- Organize your imports according to the following template
-
-Example::
-
- # vim: tabstop=4 shiftwidth=4 softtabstop=4
- {{stdlib imports in human alphabetical order}}
- \n
- {{third-party lib imports in human alphabetical order}}
- \n
- {{quantum imports in human alphabetical order}}
- \n
- \n
- {{begin your code}}
-
-
-Human Alphabetical Order Examples
----------------------------------
-Example::
-
- import httplib
- import logging
- import random
- import StringIO
- import time
- import unittest
-
- import eventlet
- import webob.exc
-
- import quantum.api.networks
- from quantum.api import ports
- from quantum.db import models
- from quantum.extensions import multiport
- import quantum.manager
- from quantum import service
-
-
-Docstrings
-----------
-Example::
-
- """A one line docstring looks like this and ends in a period."""
-
-
- """A multiline docstring has a one-line summary, less than 80 characters.
-
- Then a new paragraph after a newline that explains in more detail any
- general information about the function, class or method. Example usages
- are also great to have here if it is a complex class for function.
-
- When writing the docstring for a class, an extra line should be placed
- after the closing quotations. For more in-depth explanations for these
- decisions see http://www.python.org/dev/peps/pep-0257/
-
- If you are going to describe parameters and return values, use Sphinx, the
- appropriate syntax is as follows.
-
- :param foo: the foo parameter
- :param bar: the bar parameter
- :returns: return_type -- description of the return value
- :returns: description of the return value
- :raises: AttributeError, KeyError
- """
-
-
-Dictionaries/Lists
-------------------
-If a dictionary (dict) or list object is longer than 80 characters, its items
-should be split with newlines. Embedded iterables should have their items
-indented. Additionally, the last item in the dictionary should have a trailing
-comma. This increases readability and simplifies future diffs.
-
-Example::
-
- my_dictionary = {
- "image": {
- "name": "Just a Snapshot",
- "size": 2749573,
- "properties": {
- "user_id": 12,
- "arch": "x86_64",
- },
- "things": [
- "thing_one",
- "thing_two",
- ],
- "status": "ACTIVE",
- },
- }
-
-
-Calling Methods
----------------
-Calls to methods 80 characters or longer should format each argument with
-newlines. This is not a requirement, but a guideline::
-
- unnecessarily_long_function_name('string one',
- 'string two',
- kwarg1=constants.ACTIVE,
- kwarg2=['a', 'b', 'c'])
-
-
-Rather than constructing parameters inline, it is better to break things up::
-
- list_of_strings = [
- 'what_a_long_string',
- 'not as long',
- ]
-
- dict_of_numbers = {
- 'one': 1,
- 'two': 2,
- 'twenty four': 24,
- }
-
- object_one.call_a_method('string three',
- 'string four',
- kwarg1=list_of_strings,
- kwarg2=dict_of_numbers)
-
-
-Internationalization (i18n) Strings
------------------------------------
-In order to support multiple languages, we have a mechanism to support
-automatic translations of exception and log strings.
-
-Example::
-
- msg = _("An error occurred")
- raise HTTPBadRequest(explanation=msg)
-
-If you have a variable to place within the string, first internationalize the
-template string then do the replacement.
-
-Example::
-
- msg = _("Missing parameter: %s") % ("flavor",)
- LOG.error(msg)
-
-If you have multiple variables to place in the string, use keyword parameters.
-This helps our translators reorder parameters when needed.
-
-Example::
-
- msg = _("The server with id %(s_id)s has no key %(m_key)s")
- LOG.error(msg % {"s_id": "1234", "m_key": "imageId"})
-
-
-Creating Unit Tests
--------------------
-For every new feature, unit tests should be created that both test and
-(implicitly) document the usage of said feature. If submitting a patch for a
-bug that had no unit test, a new passing unit test should be added. If a
-submitted bug fix does have a unit test, be sure to add a new one that fails
-without the patch and passes with the patch.
+- Step 1: Read the OpenStack Style Commandments
+ https://docs.openstack.org/hacking/latest/
+- Step 2: Read on
+
+
+Running Tests
+-------------
+The testing system is based on a combination of tox and testr. The canonical
+approach to running tests is to simply run the command `tox`. This will
+create virtual environments, populate them with depenedencies and run all of
+the tests that OpenStack CI systems run. Behind the scenes, tox is running
+`testr run --parallel`, but is set up such that you can supply any additional
+testr arguments that are needed to tox. For example, you can run:
+`tox -- --analyze-isolation` to cause tox to tell testr to add
+--analyze-isolation to its argument list.
+
+It is also possible to run the tests inside of a virtual environment
+you have created, or it is possible that you have all of the dependencies
+installed locally already. In this case, you can interact with the testr
+command directly. Running `testr run` will run the entire test suite. `testr
+run --parallel` will run it in parallel (this is the default incantation tox
+uses.) More information about testr can be found at:
+http://wiki.openstack.org/testr
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 648953813..000000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,7 +0,0 @@
-include tox.ini
-include LICENSE README HACKING.rst
-include version.py
-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 8032d6914..c36d482ed 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -1,17 +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
-This is a client for OpenStack Network API. Contents:
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ License for the specific language governing permissions and limitations
+ under the License.
+
+ Convention for heading levels in Neutron devref:
+ ======= Heading 0 (reserved for the title in a document)
+ ------- Heading 1
+ ~~~~~~~ Heading 2
+ +++++++ Heading 3
+ ''''''' Heading 4
+ (Avoid deeper levels because they do not render well.)
+
+==================================
+python-neutronclient documentation
+==================================
+
+This is a client for OpenStack Networking API. It provides
+:doc:`Python API bindings ` (the neutronclient module).
+
+There is
+`OpenStack Client (OSC) `__.
+CLI which support the Networking API.
+
+User Documentation
+------------------
.. toctree::
- :maxdepth: 1
+ :maxdepth: 2
+
+ cli/index
+ reference/index
+
+Contributor Guide
+-----------------
+
+In the :doc:`Contributor Guide `, you will find
+information on neutronclient's lower level programming details or APIs
+as well as the transition to OpenStack client.
+
+.. toctree::
+ :maxdepth: 2
+
+ contributor/index
- api/autoindex
+.. note::
-Indices and tables
-==================
+ 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).
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
+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/neutronclient/_i18n.py b/neutronclient/_i18n.py
new file mode 100644
index 000000000..50ac3c5bf
--- /dev/null
+++ b/neutronclient/_i18n.py
@@ -0,0 +1,31 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import oslo_i18n
+
+
+DOMAIN = 'neutronclient'
+
+_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN)
+
+# The translation function using the well-known name "_"
+_ = _translators.primary
+
+# The contextual translation function using the name "_C"
+_C = _translators.contextual_form
+
+# The plural translation function using the name "_P"
+_P = _translators.plural_form
+
+
+def get_available_languages():
+ return oslo_i18n.get_available_languages(DOMAIN)
diff --git a/neutronclient/client.py b/neutronclient/client.py
new file mode 100644
index 000000000..e25f575f3
--- /dev/null
+++ b/neutronclient/client.py
@@ -0,0 +1,439 @@
+# Copyright 2012 OpenStack Foundation.
+# All Rights Reserved
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import logging
+import os
+
+import debtcollector.renames
+from keystoneauth1 import access
+from keystoneauth1 import adapter
+from oslo_serialization import jsonutils
+from oslo_utils import importutils
+import requests
+
+from neutronclient._i18n import _
+from neutronclient.common import exceptions
+from neutronclient.common import utils
+
+osprofiler_web = importutils.try_import("osprofiler.web")
+
+_logger = logging.getLogger(__name__)
+
+if os.environ.get('NEUTRONCLIENT_DEBUG'):
+ ch = logging.StreamHandler()
+ _logger.setLevel(logging.DEBUG)
+ _logger.addHandler(ch)
+ _requests_log_level = logging.DEBUG
+else:
+ _requests_log_level = logging.WARNING
+
+logging.getLogger("requests").setLevel(_requests_log_level)
+MAX_URI_LEN = 8192
+USER_AGENT = 'python-neutronclient'
+REQ_ID_HEADER = 'X-OpenStack-Request-ID'
+
+
+class HTTPClient(object):
+ """Handles the REST calls and responses, include authn."""
+
+ CONTENT_TYPE = 'application/json'
+
+ @debtcollector.renames.renamed_kwarg(
+ 'tenant_id', 'project_id', replace=True)
+ @debtcollector.renames.renamed_kwarg(
+ 'tenant_name', 'project_name', replace=True)
+ def __init__(self, username=None, user_id=None,
+ project_name=None, project_id=None,
+ password=None, auth_url=None,
+ token=None, region_name=None, timeout=None,
+ endpoint_url=None, insecure=False,
+ endpoint_type='publicURL',
+ auth_strategy='keystone', ca_cert=None, cert=None,
+ log_credentials=False, service_type='network',
+ global_request_id=None, **kwargs):
+
+ self.username = username
+ self.user_id = user_id
+ self.project_name = project_name
+ self.project_id = project_id
+ self.password = password
+ self.auth_url = auth_url.rstrip('/') if auth_url else None
+ self.service_type = service_type
+ self.endpoint_type = endpoint_type
+ self.region_name = region_name
+ self.timeout = timeout
+ self.auth_token = token
+ self.auth_tenant_id = None
+ self.auth_user_id = None
+ self.endpoint_url = endpoint_url
+ self.auth_strategy = auth_strategy
+ self.log_credentials = log_credentials
+ self.global_request_id = global_request_id
+ self.cert = cert
+ if insecure:
+ self.verify_cert = False
+ else:
+ self.verify_cert = ca_cert if ca_cert else True
+
+ def _cs_request(self, *args, **kwargs):
+ kargs = {}
+ kargs.setdefault('headers', kwargs.get('headers', {}))
+ kargs['headers']['User-Agent'] = USER_AGENT
+
+ if 'body' in kwargs:
+ kargs['body'] = kwargs['body']
+
+ if self.log_credentials:
+ log_kargs = kargs
+ else:
+ log_kargs = self._strip_credentials(kargs)
+
+ utils.http_log_req(_logger, args, log_kargs)
+ try:
+ resp, body = self.request(*args, **kargs)
+ except requests.exceptions.SSLError as e:
+ raise exceptions.SslCertificateValidationError(reason=str(e))
+ except Exception as e:
+ # Wrap the low-level connection error (socket timeout, redirect
+ # limit, decompression error, etc) into our custom high-level
+ # connection exception (it is excepted in the upper layers of code)
+ _logger.debug("throwing ConnectionFailed : %s", e)
+ raise exceptions.ConnectionFailed(reason=str(e))
+ utils.http_log_resp(_logger, resp, body)
+
+ # log request-id for each api call
+ request_id = resp.headers.get('x-openstack-request-id')
+ if request_id:
+ _logger.debug('%(method)s call to neutron for '
+ '%(url)s used request id '
+ '%(response_request_id)s',
+ {'method': resp.request.method,
+ 'url': resp.url,
+ 'response_request_id': request_id})
+
+ if resp.status_code == 401:
+ raise exceptions.Unauthorized(message=body)
+ return resp, body
+
+ def _strip_credentials(self, kwargs):
+ if kwargs.get('body') and self.password:
+ log_kwargs = kwargs.copy()
+ log_kwargs['body'] = kwargs['body'].replace(self.password,
+ 'REDACTED')
+ return log_kwargs
+ else:
+ return kwargs
+
+ def authenticate_and_fetch_endpoint_url(self):
+ if not self.auth_token:
+ self.authenticate()
+ elif not self.endpoint_url:
+ self.endpoint_url = self._get_endpoint_url()
+
+ def request(self, url, method, body=None, headers=None, **kwargs):
+ """Request without authentication."""
+
+ content_type = kwargs.pop('content_type', None) or 'application/json'
+ headers = headers or {}
+ headers.setdefault('Accept', content_type)
+
+ if body:
+ headers.setdefault('Content-Type', content_type)
+
+ if self.global_request_id:
+ headers.setdefault(REQ_ID_HEADER, self.global_request_id)
+
+ headers['User-Agent'] = USER_AGENT
+ # NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any
+ # headers in case if osprofiler is not initialized.
+ if osprofiler_web:
+ headers.update(osprofiler_web.get_trace_id_headers())
+
+ resp = requests.request(
+ method,
+ url,
+ data=body,
+ headers=headers,
+ verify=self.verify_cert,
+ cert=self.cert,
+ timeout=self.timeout,
+ **kwargs)
+
+ return resp, resp.text
+
+ def _check_uri_length(self, action):
+ uri_len = len(self.endpoint_url) + len(action)
+ if uri_len > MAX_URI_LEN:
+ raise exceptions.RequestURITooLong(
+ excess=uri_len - MAX_URI_LEN)
+
+ def do_request(self, url, method, **kwargs):
+ # Ensure client always has correct uri - do not guesstimate anything
+ self.authenticate_and_fetch_endpoint_url()
+ self._check_uri_length(url)
+
+ # Perform the request once. If we get a 401 back then it
+ # might be because the auth token expired, so try to
+ # re-authenticate and try again. If it still fails, bail.
+ try:
+ kwargs['headers'] = kwargs.get('headers') or {}
+ if self.auth_token is None:
+ self.auth_token = ""
+ kwargs['headers']['X-Auth-Token'] = self.auth_token
+ resp, body = self._cs_request(self.endpoint_url + url, method,
+ **kwargs)
+ return resp, body
+ except exceptions.Unauthorized:
+ self.authenticate()
+ kwargs['headers'] = kwargs.get('headers') or {}
+ kwargs['headers']['X-Auth-Token'] = self.auth_token
+ resp, body = self._cs_request(
+ self.endpoint_url + url, method, **kwargs)
+ return resp, body
+
+ def _extract_service_catalog(self, body):
+ """Set the client's service catalog from the response data."""
+ self.auth_ref = access.create(body=body)
+ self.service_catalog = self.auth_ref.service_catalog
+ self.auth_token = self.auth_ref.auth_token
+ self.auth_tenant_id = self.auth_ref.tenant_id
+ self.auth_user_id = self.auth_ref.user_id
+
+ if not self.endpoint_url:
+ self.endpoint_url = self.service_catalog.url_for(
+ region_name=self.region_name,
+ service_type=self.service_type,
+ interface=self.endpoint_type)
+
+ def _authenticate_keystone(self):
+ if self.user_id:
+ creds = {'userId': self.user_id,
+ 'password': self.password}
+ else:
+ creds = {'username': self.username,
+ 'password': self.password}
+
+ if self.project_id:
+ body = {'auth': {'passwordCredentials': creds,
+ 'tenantId': self.project_id, }, }
+ else:
+ body = {'auth': {'passwordCredentials': creds,
+ 'tenantName': self.project_name, }, }
+
+ if self.auth_url is None:
+ raise exceptions.NoAuthURLProvided()
+
+ token_url = self.auth_url + "/tokens"
+ resp, resp_body = self._cs_request(token_url, "POST",
+ body=jsonutils.dumps(body),
+ content_type="application/json",
+ allow_redirects=True)
+ if resp.status_code != 200:
+ raise exceptions.Unauthorized(message=resp_body)
+ if resp_body:
+ try:
+ resp_body = jsonutils.loads(resp_body)
+ except ValueError:
+ pass
+ else:
+ resp_body = None
+ self._extract_service_catalog(resp_body)
+
+ def _authenticate_noauth(self):
+ if not self.endpoint_url:
+ message = _('For "noauth" authentication strategy, the endpoint '
+ 'must be specified either in the constructor or '
+ 'using --os-url')
+ raise exceptions.Unauthorized(message=message)
+
+ def authenticate(self):
+ if self.auth_strategy == 'keystone':
+ self._authenticate_keystone()
+ elif self.auth_strategy == 'noauth':
+ self._authenticate_noauth()
+ else:
+ err_msg = _('Unknown auth strategy: %s') % self.auth_strategy
+ raise exceptions.Unauthorized(message=err_msg)
+
+ def _get_endpoint_url(self):
+ if self.auth_url is None:
+ raise exceptions.NoAuthURLProvided()
+
+ url = self.auth_url + '/tokens/%s/endpoints' % self.auth_token
+ try:
+ resp, body = self._cs_request(url, "GET")
+ except exceptions.Unauthorized:
+ # rollback to authenticate() to handle case when neutron client
+ # is initialized just before the token is expired
+ self.authenticate()
+ return self.endpoint_url
+
+ body = jsonutils.loads(body)
+ for endpoint in body.get('endpoints', []):
+ if (endpoint['type'] == 'network' and
+ endpoint.get('region') == self.region_name):
+ if self.endpoint_type not in endpoint:
+ raise exceptions.EndpointTypeNotFound(
+ type_=self.endpoint_type)
+ return endpoint[self.endpoint_type]
+
+ raise exceptions.EndpointNotFound()
+
+ def get_auth_info(self):
+ return {'auth_token': self.auth_token,
+ 'auth_tenant_id': self.auth_tenant_id,
+ 'auth_user_id': self.auth_user_id,
+ 'endpoint_url': self.endpoint_url}
+
+ def get_auth_ref(self):
+ return getattr(self, 'auth_ref', None)
+
+
+class SessionClient(adapter.Adapter):
+
+ def request(self, *args, **kwargs):
+ kwargs.setdefault('authenticated', False)
+ kwargs.setdefault('raise_exc', False)
+
+ content_type = kwargs.pop('content_type', None) or 'application/json'
+
+ headers = kwargs.get('headers') or {}
+ headers.setdefault('Accept', content_type)
+
+ # NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any
+ # headers in case if osprofiler is not initialized.
+ if osprofiler_web:
+ headers.update(osprofiler_web.get_trace_id_headers())
+
+ try:
+ kwargs.setdefault('data', kwargs.pop('body'))
+ except KeyError:
+ pass
+
+ if kwargs.get('data'):
+ headers.setdefault('Content-Type', content_type)
+
+ kwargs['headers'] = headers
+ resp = super(SessionClient, self).request(*args, **kwargs)
+ return resp, resp.text
+
+ def _check_uri_length(self, url):
+ uri_len = len(self.endpoint_url) + len(url)
+ if uri_len > MAX_URI_LEN:
+ raise exceptions.RequestURITooLong(
+ excess=uri_len - MAX_URI_LEN)
+
+ def do_request(self, url, method, **kwargs):
+ kwargs.setdefault('authenticated', True)
+ self._check_uri_length(url)
+ return self.request(url, method, **kwargs)
+
+ @property
+ def endpoint_url(self):
+ # NOTE(jamielennox): This is used purely by the CLI and should be
+ # removed when the CLI gets smarter.
+ return self.get_endpoint()
+
+ @property
+ def auth_token(self):
+ # NOTE(jamielennox): This is used purely by the CLI and should be
+ # removed when the CLI gets smarter.
+ return self.get_token()
+
+ def authenticate(self):
+ # NOTE(jamielennox): This is used purely by the CLI and should be
+ # removed when the CLI gets smarter.
+ self.get_token()
+
+ def get_auth_info(self):
+ auth_info = {'auth_token': self.auth_token,
+ 'endpoint_url': self.endpoint_url}
+
+ # NOTE(jamielennox): This is the best we can do here. It will work
+ # with identity plugins which is the primary case but we should
+ # deprecate it's usage as much as possible.
+ try:
+ get_access = (self.auth or self.session.auth).get_access
+ except AttributeError:
+ pass
+ else:
+ auth_ref = get_access(self.session)
+
+ auth_info['auth_tenant_id'] = auth_ref.project_id
+ auth_info['auth_user_id'] = auth_ref.user_id
+
+ return auth_info
+
+ def get_auth_ref(self):
+ return self.session.auth.get_auth_ref(self.session)
+
+
+# FIXME(bklei): Should refactor this to use kwargs and only
+# explicitly list arguments that are not None.
+@debtcollector.renames.renamed_kwarg('tenant_id', 'project_id', replace=True)
+@debtcollector.renames.renamed_kwarg(
+ 'tenant_name', 'project_name', replace=True)
+def construct_http_client(username=None,
+ user_id=None,
+ project_name=None,
+ project_id=None,
+ password=None,
+ auth_url=None,
+ token=None,
+ region_name=None,
+ timeout=None,
+ endpoint_url=None,
+ insecure=False,
+ endpoint_type='public',
+ log_credentials=None,
+ auth_strategy='keystone',
+ ca_cert=None,
+ cert=None,
+ service_type='network',
+ session=None,
+ global_request_id=None,
+ **kwargs):
+
+ if session:
+ kwargs.setdefault('user_agent', USER_AGENT)
+ kwargs.setdefault('interface', endpoint_type)
+ return SessionClient(session=session,
+ service_type=service_type,
+ region_name=region_name,
+ global_request_id=global_request_id,
+ **kwargs)
+ else:
+ # FIXME(bklei): username and password are now optional. Need
+ # to test that they were provided in this mode. Should also
+ # refactor to use kwargs.
+ return HTTPClient(username=username,
+ password=password,
+ project_id=project_id,
+ project_name=project_name,
+ user_id=user_id,
+ auth_url=auth_url,
+ token=token,
+ endpoint_url=endpoint_url,
+ insecure=insecure,
+ timeout=timeout,
+ region_name=region_name,
+ endpoint_type=endpoint_type,
+ service_type=service_type,
+ ca_cert=ca_cert,
+ cert=cert,
+ log_credentials=log_credentials,
+ auth_strategy=auth_strategy,
+ global_request_id=global_request_id)
diff --git a/quantumclient/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/neutronclient/common/constants.py b/neutronclient/common/constants.py
new file mode 100644
index 000000000..ccb22cd80
--- /dev/null
+++ b/neutronclient/common/constants.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2012 OpenStack Foundation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+TYPE_BOOL = "bool"
+TYPE_INT = "int"
+TYPE_LONG = "long"
+TYPE_FLOAT = "float"
+TYPE_LIST = "list"
+TYPE_DICT = "dict"
+
+PLURALS = {'networks': 'network',
+ 'ports': 'port',
+ 'subnets': 'subnet',
+ 'subnetpools': 'subnetpool',
+ 'dns_nameservers': 'dns_nameserver',
+ 'host_routes': 'host_route',
+ 'allocation_pools': 'allocation_pool',
+ 'fixed_ips': 'fixed_ip',
+ 'extensions': 'extension'}
diff --git a/neutronclient/common/exceptions.py b/neutronclient/common/exceptions.py
new file mode 100644
index 000000000..443f78155
--- /dev/null
+++ b/neutronclient/common/exceptions.py
@@ -0,0 +1,260 @@
+# Copyright 2011 VMware, Inc
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_utils import encodeutils
+
+from neutronclient._i18n import _
+
+"""
+Neutron base exception handling.
+
+Exceptions are classified into three categories:
+* Exceptions corresponding to exceptions from neutron server:
+ This type of exceptions should inherit one of exceptions
+ in HTTP_EXCEPTION_MAP.
+* Exceptions from client library:
+ This type of exceptions should inherit NeutronClientException.
+* Exceptions from CLI code:
+ This type of exceptions should inherit NeutronCLIError.
+"""
+
+
+# NOTE: This method is defined here to avoid
+# an import loop between common.utils and this module.
+def _safe_decode_dict(kwargs):
+ for k, v in kwargs.items():
+ kwargs[k] = encodeutils.safe_decode(v)
+ return kwargs
+
+
+class NeutronException(Exception):
+ """Base Neutron Exception.
+
+ To correctly use this class, inherit from it and define
+ a 'message' property. That message will get printf'd
+ with the keyword arguments provided to the constructor.
+ """
+ message = _("An unknown exception occurred.")
+
+ def __init__(self, message=None, **kwargs):
+ if message:
+ self.message = message
+ try:
+ self._error_string = self.message % _safe_decode_dict(kwargs)
+ except Exception:
+ # at least get the core message out if something happened
+ self._error_string = self.message
+
+ def __str__(self):
+ return self._error_string
+
+
+class NeutronClientException(NeutronException):
+ """Base exception which exceptions from Neutron are mapped into.
+
+ NOTE: on the client side, we use different exception types in order
+ to allow client library users to handle server exceptions in try...except
+ blocks. The actual error message is the one generated on the server side.
+ """
+
+ status_code = 0
+ req_ids_msg = _("Neutron server returns request_ids: %s")
+ request_ids = []
+
+ def __init__(self, message=None, **kwargs):
+ self.request_ids = kwargs.get('request_ids')
+ if 'status_code' in kwargs:
+ self.status_code = kwargs['status_code']
+ if self.request_ids:
+ req_ids_msg = self.req_ids_msg % self.request_ids
+ if message:
+ message = _('%(msg)s\n%(id)s') % {'msg': message,
+ 'id': req_ids_msg}
+ else:
+ message = req_ids_msg
+ super(NeutronClientException, self).__init__(message, **kwargs)
+
+
+# Base exceptions from Neutron
+
+class BadRequest(NeutronClientException):
+ status_code = 400
+
+
+class Unauthorized(NeutronClientException):
+ status_code = 401
+ message = _("Unauthorized: bad credentials.")
+
+
+class Forbidden(NeutronClientException):
+ status_code = 403
+ message = _("Forbidden: your credentials don't give you access to this "
+ "resource.")
+
+
+class NotFound(NeutronClientException):
+ status_code = 404
+
+
+class Conflict(NeutronClientException):
+ status_code = 409
+
+
+class InternalServerError(NeutronClientException):
+ status_code = 500
+
+
+class ServiceUnavailable(NeutronClientException):
+ status_code = 503
+
+
+HTTP_EXCEPTION_MAP = {
+ 400: BadRequest,
+ 401: Unauthorized,
+ 403: Forbidden,
+ 404: NotFound,
+ 409: Conflict,
+ 500: InternalServerError,
+ 503: ServiceUnavailable,
+}
+
+
+# Exceptions mapped to Neutron server exceptions
+# These are defined if a user of client library needs specific exception.
+# Exception name should be + 'Client'
+# e.g., NetworkNotFound -> NetworkNotFoundClient
+
+class NetworkNotFoundClient(NotFound):
+ pass
+
+
+class PortNotFoundClient(NotFound):
+ pass
+
+
+class StateInvalidClient(BadRequest):
+ pass
+
+
+class NetworkInUseClient(Conflict):
+ pass
+
+
+class PortInUseClient(Conflict):
+ pass
+
+
+class IpAddressInUseClient(Conflict):
+ pass
+
+
+class IpAddressAlreadyAllocatedClient(Conflict):
+ pass
+
+
+class InvalidIpForNetworkClient(BadRequest):
+ pass
+
+
+class InvalidIpForSubnetClient(BadRequest):
+ pass
+
+
+class OverQuotaClient(Conflict):
+ pass
+
+
+class IpAddressGenerationFailureClient(Conflict):
+ pass
+
+
+class MacAddressInUseClient(Conflict):
+ pass
+
+
+class HostNotCompatibleWithFixedIpsClient(Conflict):
+ pass
+
+
+class ExternalIpAddressExhaustedClient(BadRequest):
+ pass
+
+
+# Exceptions from client library
+
+class NoAuthURLProvided(Unauthorized):
+ message = _("auth_url was not provided to the Neutron client")
+
+
+class EndpointNotFound(NeutronClientException):
+ message = _("Could not find Service or Region in Service Catalog.")
+
+
+class EndpointTypeNotFound(NeutronClientException):
+ message = _("Could not find endpoint type %(type_)s in Service Catalog.")
+
+
+class AmbiguousEndpoints(NeutronClientException):
+ message = _("Found more than one matching endpoint in Service Catalog: "
+ "%(matching_endpoints)")
+
+
+class RequestURITooLong(NeutronClientException):
+ """Raised when a request fails with HTTP error 414."""
+
+ def __init__(self, **kwargs):
+ self.excess = kwargs.get('excess', 0)
+ super(RequestURITooLong, self).__init__(**kwargs)
+
+
+class ConnectionFailed(NeutronClientException):
+ message = _("Connection to neutron failed: %(reason)s")
+
+
+class SslCertificateValidationError(NeutronClientException):
+ message = _("SSL certificate validation has failed: %(reason)s")
+
+
+class MalformedResponseBody(NeutronClientException):
+ message = _("Malformed response body: %(reason)s")
+
+
+class InvalidContentType(NeutronClientException):
+ message = _("Invalid content type %(content_type)s.")
+
+
+# Command line exceptions
+
+class NeutronCLIError(NeutronException):
+ """Exception raised when command line parsing fails."""
+ pass
+
+
+class CommandError(NeutronCLIError):
+ pass
+
+
+class UnsupportedVersion(NeutronCLIError):
+ """Indicates usage of an unsupported API version
+
+ Indicates that the user is trying to use an unsupported version of
+ the API.
+ """
+ pass
+
+
+class NeutronClientNoUniqueMatch(NeutronCLIError):
+ message = _("Multiple %(resource)s matches found for name '%(name)s',"
+ " use an ID to be more specific.")
diff --git a/neutronclient/common/extension.py b/neutronclient/common/extension.py
new file mode 100644
index 000000000..00a035d6c
--- /dev/null
+++ b/neutronclient/common/extension.py
@@ -0,0 +1,85 @@
+# Copyright 2015 Rackspace Hosting Inc.
+# All Rights Reserved
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+from stevedore import extension
+
+from neutronclient.neutron import v2_0 as neutronV20
+
+
+def _discover_via_entry_points():
+ emgr = extension.ExtensionManager('neutronclient.extension',
+ invoke_on_load=False)
+ return ((ext.name, ext.plugin) for ext in emgr)
+
+
+class NeutronClientExtension(neutronV20.NeutronCommand):
+ pagination_support = False
+ _formatters = {}
+ sorting_support = False
+
+
+class ClientExtensionShow(NeutronClientExtension, neutronV20.ShowCommand):
+ def take_action(self, parsed_args):
+ # NOTE(mdietz): Calls 'execute' to provide a consistent pattern
+ # for any implementers adding extensions with
+ # regard to any other extension verb.
+ return self.execute(parsed_args)
+
+ def execute(self, parsed_args):
+ return super(ClientExtensionShow, self).take_action(parsed_args)
+
+
+class ClientExtensionList(NeutronClientExtension, neutronV20.ListCommand):
+ def take_action(self, parsed_args):
+ # NOTE(mdietz): Calls 'execute' to provide a consistent pattern
+ # for any implementers adding extensions with
+ # regard to any other extension verb.
+ return self.execute(parsed_args)
+
+ def execute(self, parsed_args):
+ return super(ClientExtensionList, self).take_action(parsed_args)
+
+
+class ClientExtensionDelete(NeutronClientExtension, neutronV20.DeleteCommand):
+ def take_action(self, parsed_args):
+ # NOTE(mdietz): Calls 'execute' to provide a consistent pattern
+ # for any implementers adding extensions with
+ # regard to any other extension verb.
+ return self.execute(parsed_args)
+
+ def execute(self, parsed_args):
+ return super(ClientExtensionDelete, self).take_action(parsed_args)
+
+
+class ClientExtensionCreate(NeutronClientExtension, neutronV20.CreateCommand):
+ def take_action(self, parsed_args):
+ # NOTE(mdietz): Calls 'execute' to provide a consistent pattern
+ # for any implementers adding extensions with
+ # regard to any other extension verb.
+ return self.execute(parsed_args)
+
+ def execute(self, parsed_args):
+ return super(ClientExtensionCreate, self).take_action(parsed_args)
+
+
+class ClientExtensionUpdate(NeutronClientExtension, neutronV20.UpdateCommand):
+ def take_action(self, parsed_args):
+ # NOTE(mdietz): Calls 'execute' to provide a consistent pattern
+ # for any implementers adding extensions with
+ # regard to any other extension verb.
+ return self.execute(parsed_args)
+
+ def execute(self, parsed_args):
+ return super(ClientExtensionUpdate, self).take_action(parsed_args)
diff --git a/neutronclient/common/serializer.py b/neutronclient/common/serializer.py
new file mode 100644
index 000000000..d4c8bbbb6
--- /dev/null
+++ b/neutronclient/common/serializer.py
@@ -0,0 +1,120 @@
+# Copyright 2013 OpenStack Foundation.
+# All Rights Reserved
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils
+
+from neutronclient._i18n import _
+from neutronclient.common import exceptions as exception
+
+
+class ActionDispatcher(object):
+ """Maps method name to local methods through action name."""
+
+ def dispatch(self, *args, **kwargs):
+ """Find and call local method."""
+ action = kwargs.pop('action', 'default')
+ action_method = getattr(self, str(action), self.default)
+ return action_method(*args, **kwargs)
+
+ def default(self, data):
+ raise NotImplementedError()
+
+
+class DictSerializer(ActionDispatcher):
+ """Default request body serialization."""
+
+ def serialize(self, data, action='default'):
+ return self.dispatch(data, action=action)
+
+ def default(self, data):
+ return ""
+
+
+class JSONDictSerializer(DictSerializer):
+ """Default JSON request body serialization."""
+
+ def default(self, data):
+ def sanitizer(obj):
+ return str(obj)
+ return jsonutils.dumps(data, default=sanitizer)
+
+
+class TextDeserializer(ActionDispatcher):
+ """Default request body deserialization."""
+
+ def deserialize(self, datastring, action='default'):
+ return self.dispatch(datastring, action=action)
+
+ def default(self, datastring):
+ return {}
+
+
+class JSONDeserializer(TextDeserializer):
+
+ def _from_json(self, datastring):
+ try:
+ return jsonutils.loads(datastring)
+ except ValueError:
+ msg = _("Cannot understand JSON")
+ raise exception.MalformedResponseBody(reason=msg)
+
+ def default(self, datastring):
+ return {'body': self._from_json(datastring)}
+
+
+# NOTE(maru): this class is duplicated from neutron.wsgi
+class Serializer(object):
+ """Serializes and deserializes dictionaries to certain MIME types."""
+
+ def __init__(self, metadata=None):
+ """Create a serializer based on the given WSGI environment.
+
+ 'metadata' is an optional dict mapping MIME types to information
+ needed to serialize a dictionary to that type.
+
+ """
+ self.metadata = metadata or {}
+
+ def _get_serialize_handler(self, content_type):
+ handlers = {
+ 'application/json': JSONDictSerializer(),
+ }
+
+ try:
+ return handlers[content_type]
+ except Exception:
+ raise exception.InvalidContentType(content_type=content_type)
+
+ def serialize(self, data):
+ """Serialize a dictionary into the specified content type."""
+ return self._get_serialize_handler("application/json").serialize(data)
+
+ def deserialize(self, datastring):
+ """Deserialize a string to a dictionary.
+
+ The string must be in the format of a supported MIME type.
+ """
+ return self.get_deserialize_handler("application/json").deserialize(
+ datastring)
+
+ def get_deserialize_handler(self, content_type):
+ handlers = {
+ 'application/json': JSONDeserializer(),
+ }
+
+ try:
+ return handlers[content_type]
+ except Exception:
+ raise exception.InvalidContentType(content_type=content_type)
diff --git a/neutronclient/common/utils.py b/neutronclient/common/utils.py
new file mode 100644
index 000000000..93f92fd19
--- /dev/null
+++ b/neutronclient/common/utils.py
@@ -0,0 +1,237 @@
+# Copyright 2011, VMware, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+# Borrowed from nova code base, more utilities will be added/borrowed as and
+# when needed.
+
+"""Utilities and helper functions."""
+
+import argparse
+import functools
+import hashlib
+import logging
+import os
+
+from oslo_utils import encodeutils
+from oslo_utils import importutils
+
+from neutronclient._i18n import _
+from neutronclient.common import exceptions
+
+SENSITIVE_HEADERS = ('X-Auth-Token',)
+
+
+def env(*vars, **kwargs):
+ """Returns the first environment variable set.
+
+ If none are non-empty, defaults to '' or keyword arg default.
+ """
+ for v in vars:
+ value = os.environ.get(v)
+ if value:
+ return value
+ return kwargs.get('default', '')
+
+
+def convert_to_uppercase(string):
+ return string.upper()
+
+
+def convert_to_lowercase(string):
+ return string.lower()
+
+
+def get_client_class(api_name, version, version_map):
+ """Returns the client class for the requested API version.
+
+ :param api_name: the name of the API, e.g. 'compute', 'image', etc
+ :param version: the requested API version
+ :param version_map: a dict of client classes keyed by version
+ :rtype: a client class for the requested API version
+ """
+ try:
+ client_path = version_map[str(version)]
+ except (KeyError, ValueError):
+ msg = _("Invalid %(api_name)s client version '%(version)s'. must be "
+ "one of: %(map_keys)s")
+ msg = msg % {'api_name': api_name, 'version': version,
+ 'map_keys': ', '.join(version_map.keys())}
+ raise exceptions.UnsupportedVersion(msg)
+
+ return importutils.import_class(client_path)
+
+
+def get_item_properties(item, fields, mixed_case_fields=(), formatters=None):
+ """Return a tuple containing the item properties.
+
+ :param item: a single item resource (e.g. Server, Tenant, etc)
+ :param fields: tuple of strings with the desired field names
+ :param mixed_case_fields: tuple of field names to preserve case
+ :param formatters: dictionary mapping field names to callables
+ to format the values
+ """
+ if formatters is None:
+ formatters = {}
+
+ row = []
+
+ for field in fields:
+ if field in formatters:
+ row.append(formatters[field](item))
+ else:
+ if field in mixed_case_fields:
+ field_name = field.replace(' ', '_')
+ else:
+ field_name = field.lower().replace(' ', '_')
+ if not hasattr(item, field_name) and isinstance(item, dict):
+ data = item[field_name]
+ else:
+ data = getattr(item, field_name, '')
+ if data is None:
+ data = ''
+ row.append(data)
+ return tuple(row)
+
+
+def str2bool(strbool):
+ if strbool is None:
+ return None
+ return strbool.lower() == 'true'
+
+
+def str2dict(strdict, required_keys=None, optional_keys=None):
+ """Convert key1=value1,key2=value2,... string into dictionary.
+
+ :param strdict: string in the form of key1=value1,key2=value2
+ :param required_keys: list of required keys. All keys in this list must be
+ specified. Otherwise ArgumentTypeError will be raised.
+ If this parameter is unspecified, no required key check
+ will be done.
+ :param optional_keys: list of optional keys.
+ This parameter is used for valid key check.
+ When at least one of required_keys and optional_keys,
+ a key must be a member of either of required_keys or
+ optional_keys. Otherwise, ArgumentTypeError will be
+ raised. When both required_keys and optional_keys are
+ unspecified, no valid key check will be done.
+ """
+ result = {}
+ if strdict:
+ i = 0
+ kvlist = []
+ for kv in strdict.split(','):
+ if '=' in kv:
+ kvlist.append(kv)
+ i += 1
+ elif i == 0:
+ msg = _("missing value for key '%s'")
+ raise argparse.ArgumentTypeError(msg % kv)
+ else:
+ kvlist[i - 1] = "%s,%s" % (kvlist[i - 1], kv)
+ for kv in kvlist:
+ key, sep, value = kv.partition('=')
+ if not sep:
+ msg = _("invalid key-value '%s', expected format: key=value")
+ raise argparse.ArgumentTypeError(msg % kv)
+ result[key] = value
+ valid_keys = set(required_keys or []) | set(optional_keys or [])
+ if valid_keys:
+ invalid_keys = [k for k in result if k not in valid_keys]
+ if invalid_keys:
+ msg = _("Invalid key(s) '%(invalid_keys)s' specified. "
+ "Valid key(s): '%(valid_keys)s'.")
+ raise argparse.ArgumentTypeError(
+ msg % {'invalid_keys': ', '.join(sorted(invalid_keys)),
+ 'valid_keys': ', '.join(sorted(valid_keys))})
+ if required_keys:
+ not_found_keys = [k for k in required_keys if k not in result]
+ if not_found_keys:
+ msg = _("Required key(s) '%s' not specified.")
+ raise argparse.ArgumentTypeError(msg % ', '.join(not_found_keys))
+ return result
+
+
+def str2dict_type(optional_keys=None, required_keys=None):
+ return functools.partial(str2dict,
+ optional_keys=optional_keys,
+ required_keys=required_keys)
+
+
+def http_log_req(_logger, args, kwargs):
+ if not _logger.isEnabledFor(logging.DEBUG):
+ return
+
+ string_parts = ['curl -i']
+ for element in args:
+ if element in ('GET', 'POST', 'DELETE', 'PUT'):
+ string_parts.append(' -X %s' % element)
+ else:
+ string_parts.append(' %s' % element)
+
+ for (key, value) in kwargs['headers'].items():
+ if key in SENSITIVE_HEADERS:
+ v = value.encode('utf-8')
+ h = hashlib.sha256(v)
+ d = h.hexdigest()
+ value = "{SHA256}%s" % d
+ header = ' -H "%s: %s"' % (key, value)
+ string_parts.append(header)
+
+ if 'body' in kwargs and kwargs['body']:
+ string_parts.append(" -d '%s'" % (kwargs['body']))
+ req = encodeutils.safe_encode("".join(string_parts))
+ _logger.debug("REQ: %s", req)
+
+
+def http_log_resp(_logger, resp, body):
+ if not _logger.isEnabledFor(logging.DEBUG):
+ return
+ _logger.debug("RESP: %(code)s %(headers)s %(body)s",
+ {'code': resp.status_code,
+ 'headers': resp.headers,
+ 'body': body})
+
+
+def _safe_encode_without_obj(data):
+ if isinstance(data, str):
+ return encodeutils.safe_encode(data)
+ return data
+
+
+def safe_encode_list(data):
+ return list(map(_safe_encode_without_obj, data))
+
+
+def safe_encode_dict(data):
+ def _encode_item(item):
+ k, v = item
+ if isinstance(v, list):
+ return (k, safe_encode_list(v))
+ elif isinstance(v, dict):
+ return (k, safe_encode_dict(v))
+ return (k, _safe_encode_without_obj(v))
+
+ return dict(list(map(_encode_item, data.items())))
+
+
+def add_boolean_argument(parser, name, **kwargs):
+ for keyword in ('metavar', 'choices'):
+ kwargs.pop(keyword, None)
+ default = kwargs.pop('default', argparse.SUPPRESS)
+ parser.add_argument(
+ name,
+ metavar='{True,False}',
+ choices=['True', 'true', 'False', 'false'],
+ default=default,
+ **kwargs)
diff --git a/neutronclient/common/validators.py b/neutronclient/common/validators.py
new file mode 100644
index 000000000..831d68e8f
--- /dev/null
+++ b/neutronclient/common/validators.py
@@ -0,0 +1,69 @@
+# Copyright 2014 NEC Corporation
+# All Rights Reserved
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import netaddr
+
+from neutronclient._i18n import _
+from neutronclient.common import exceptions
+
+
+def validate_int_range(parsed_args, attr_name, min_value=None, max_value=None):
+ val = getattr(parsed_args, attr_name, None)
+ if val is None:
+ return
+ try:
+ if not isinstance(val, int):
+ int_val = int(val, 0)
+ else:
+ int_val = val
+ if ((min_value is None or min_value <= int_val) and
+ (max_value is None or int_val <= max_value)):
+ return
+ except (ValueError, TypeError):
+ pass
+
+ if min_value is not None and max_value is not None:
+ msg = (_('%(attr_name)s "%(val)s" should be an integer '
+ '[%(min)i:%(max)i].') %
+ {'attr_name': attr_name.replace('_', '-'),
+ 'val': val, 'min': min_value, 'max': max_value})
+ elif min_value is not None:
+ msg = (_('%(attr_name)s "%(val)s" should be an integer '
+ 'greater than or equal to %(min)i.') %
+ {'attr_name': attr_name.replace('_', '-'),
+ 'val': val, 'min': min_value})
+ elif max_value is not None:
+ msg = (_('%(attr_name)s "%(val)s" should be an integer '
+ 'smaller than or equal to %(max)i.') %
+ {'attr_name': attr_name.replace('_', '-'),
+ 'val': val, 'max': max_value})
+ else:
+ msg = (_('%(attr_name)s "%(val)s" should be an integer.') %
+ {'attr_name': attr_name.replace('_', '-'),
+ 'val': val})
+
+ raise exceptions.CommandError(msg)
+
+
+def validate_ip_subnet(parsed_args, attr_name):
+ val = getattr(parsed_args, attr_name)
+ if not val:
+ return
+ try:
+ netaddr.IPNetwork(val)
+ except (netaddr.AddrFormatError, ValueError):
+ raise exceptions.CommandError(
+ (_('%(attr_name)s "%(val)s" is not a valid CIDR.') %
+ {'attr_name': attr_name.replace('_', '-'), 'val': val}))
diff --git a/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/quantumclient/tests/unit/__init__.py b/neutronclient/neutron/v2_0/bgp/__init__.py
similarity index 100%
rename from quantumclient/tests/unit/__init__.py
rename to neutronclient/neutron/v2_0/bgp/__init__.py
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/v2_0/__init__.py b/neutronclient/neutron/v2_0/servicetype.py
similarity index 60%
rename from quantumclient/v2_0/__init__.py
rename to neutronclient/neutron/v2_0/servicetype.py
index 63c3905e2..ec8454f94 100644
--- a/quantumclient/v2_0/__init__.py
+++ b/neutronclient/neutron/v2_0/servicetype.py
@@ -1,5 +1,5 @@
-# Copyright 2012 OpenStack LLC.
-# All Rights Reserved.
+# 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
@@ -12,3 +12,16 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
+#
+
+from neutronclient.neutron import v2_0 as neutronV20
+
+
+class ListServiceProvider(neutronV20.ListCommand):
+ """List service providers."""
+
+ resource = 'service_provider'
+ list_columns = ['service_type', 'name', 'default']
+ _formatters = {}
+ pagination_support = True
+ sorting_support = True
diff --git a/neutronclient/neutron/v2_0/subnet.py b/neutronclient/neutron/v2_0/subnet.py
new file mode 100644
index 000000000..633826422
--- /dev/null
+++ b/neutronclient/neutron/v2_0/subnet.py
@@ -0,0 +1,276 @@
+# Copyright 2012 OpenStack Foundation.
+# All Rights Reserved
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import argparse
+
+from oslo_serialization import jsonutils
+
+from neutronclient._i18n import _
+from neutronclient.common import exceptions
+from neutronclient.common import utils
+from neutronclient.neutron import v2_0 as neutronV20
+
+
+def _format_allocation_pools(subnet):
+ try:
+ return '\n'.join([jsonutils.dumps(pool) for pool in
+ subnet['allocation_pools']])
+ except (TypeError, KeyError):
+ return ''
+
+
+def _format_dns_nameservers(subnet):
+ try:
+ return '\n'.join([jsonutils.dumps(server) for server in
+ subnet['dns_nameservers']])
+ except (TypeError, KeyError):
+ return ''
+
+
+def _format_host_routes(subnet):
+ try:
+ return '\n'.join([jsonutils.dumps(route) for route in
+ subnet['host_routes']])
+ except (TypeError, KeyError):
+ return ''
+
+
+def add_updatable_arguments(parser):
+ parser.add_argument(
+ '--name',
+ help=_('Name of this subnet.'))
+ parser.add_argument(
+ '--description',
+ help=_('Description of this subnet.'))
+ gateway_sg = parser.add_mutually_exclusive_group()
+ gateway_sg.add_argument(
+ '--gateway', metavar='GATEWAY_IP',
+ help=_('Gateway IP of this subnet.'))
+ gateway_sg.add_argument(
+ '--no-gateway',
+ action='store_true',
+ help=_('Do not configure a gateway for this subnet.'))
+ parser.add_argument(
+ '--allocation-pool', metavar='start=IP_ADDR,end=IP_ADDR',
+ action='append', dest='allocation_pools',
+ type=utils.str2dict_type(required_keys=['start', 'end']),
+ help=_('Allocation pool IP addresses for this subnet '
+ '(This option can be repeated).'))
+ parser.add_argument(
+ '--allocation_pool',
+ action='append', dest='allocation_pools',
+ type=utils.str2dict_type(required_keys=['start', 'end']),
+ help=argparse.SUPPRESS)
+ parser.add_argument(
+ '--host-route', metavar='destination=CIDR,nexthop=IP_ADDR',
+ action='append', dest='host_routes',
+ type=utils.str2dict_type(required_keys=['destination', 'nexthop']),
+ help=_('Additional route (This option can be repeated).'))
+ parser.add_argument(
+ '--dns-nameserver', metavar='DNS_NAMESERVER',
+ action='append', dest='dns_nameservers',
+ help=_('DNS name server for this subnet '
+ '(This option can be repeated).'))
+ parser.add_argument(
+ '--disable-dhcp',
+ action='store_true',
+ help=_('Disable DHCP for this subnet.'))
+ parser.add_argument(
+ '--enable-dhcp',
+ action='store_true',
+ help=_('Enable DHCP for this subnet.'))
+ # NOTE(ihrachys): yes, that's awful, but should be left as-is for
+ # backwards compatibility for versions <=2.3.4 that passed the
+ # boolean values through to the server without any argument
+ # validation.
+ parser.add_argument(
+ '--enable-dhcp=True',
+ action='store_true',
+ dest='enable_dhcp',
+ help=argparse.SUPPRESS)
+ parser.add_argument(
+ '--enable-dhcp=False',
+ action='store_true',
+ dest='disable_dhcp',
+ help=argparse.SUPPRESS)
+
+
+def updatable_args2body(parsed_args, body, for_create=True, ip_version=None):
+ if parsed_args.disable_dhcp and parsed_args.enable_dhcp:
+ raise exceptions.CommandError(_(
+ "You cannot enable and disable DHCP at the same time."))
+
+ neutronV20.update_dict(parsed_args, body,
+ ['name', 'allocation_pools',
+ 'host_routes', 'dns_nameservers',
+ 'description'])
+ if parsed_args.no_gateway:
+ body['gateway_ip'] = None
+ elif parsed_args.gateway:
+ body['gateway_ip'] = parsed_args.gateway
+ if parsed_args.disable_dhcp:
+ body['enable_dhcp'] = False
+ if parsed_args.enable_dhcp:
+ body['enable_dhcp'] = True
+ if for_create and parsed_args.ipv6_ra_mode:
+ if ip_version == 4:
+ raise exceptions.CommandError(_("--ipv6-ra-mode is invalid "
+ "when --ip-version is 4"))
+ body['ipv6_ra_mode'] = parsed_args.ipv6_ra_mode
+ if for_create and parsed_args.ipv6_address_mode:
+ if ip_version == 4:
+ raise exceptions.CommandError(_("--ipv6-address-mode is "
+ "invalid when --ip-version "
+ "is 4"))
+ body['ipv6_address_mode'] = parsed_args.ipv6_address_mode
+
+
+class ListSubnet(neutronV20.ListCommand):
+ """List subnets that belong to a given tenant."""
+
+ resource = 'subnet'
+ _formatters = {'allocation_pools': _format_allocation_pools,
+ 'dns_nameservers': _format_dns_nameservers,
+ 'host_routes': _format_host_routes, }
+ list_columns = ['id', 'name', 'cidr', 'allocation_pools']
+ pagination_support = True
+ sorting_support = True
+
+
+class ShowSubnet(neutronV20.ShowCommand):
+ """Show information of a given subnet."""
+
+ resource = 'subnet'
+
+
+class CreateSubnet(neutronV20.CreateCommand):
+ """Create a subnet for a given tenant."""
+
+ resource = 'subnet'
+
+ def add_known_arguments(self, parser):
+ add_updatable_arguments(parser)
+ parser.add_argument(
+ '--ip-version',
+ type=int,
+ default=4, choices=[4, 6],
+ help=_('IP version to use, default is 4. '
+ 'Note that when subnetpool is specified, '
+ 'IP version is determined from the subnetpool '
+ 'and this option is ignored.'))
+ parser.add_argument(
+ '--ip_version',
+ type=int,
+ choices=[4, 6],
+ help=argparse.SUPPRESS)
+ parser.add_argument(
+ 'network_id', metavar='NETWORK',
+ help=_('Network ID or name this subnet belongs to.'))
+ parser.add_argument(
+ 'cidr', nargs='?', metavar='CIDR',
+ help=_('CIDR of subnet to create.'))
+ parser.add_argument(
+ '--ipv6-ra-mode',
+ type=utils.convert_to_lowercase,
+ choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'],
+ help=_('IPv6 RA (Router Advertisement) mode.'))
+ parser.add_argument(
+ '--ipv6-address-mode',
+ type=utils.convert_to_lowercase,
+ choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'],
+ help=_('IPv6 address mode.'))
+ parser.add_argument(
+ '--subnetpool', metavar='SUBNETPOOL',
+ help=_('ID or name of subnetpool from which this subnet '
+ 'will obtain a CIDR.'))
+ parser.add_argument(
+ '--use-default-subnetpool',
+ action='store_true',
+ help=_('Use default subnetpool for ip_version, if it exists.'))
+ parser.add_argument(
+ '--prefixlen', metavar='PREFIX_LENGTH',
+ help=_('Prefix length for subnet allocation from subnetpool.'))
+ parser.add_argument(
+ '--segment', metavar='SEGMENT',
+ help=_('ID of segment with which this subnet will be associated.'))
+
+ def args2body(self, parsed_args):
+ _network_id = neutronV20.find_resourceid_by_name_or_id(
+ self.get_client(), 'network', parsed_args.network_id)
+ body = {'network_id': _network_id}
+
+ if parsed_args.prefixlen:
+ body['prefixlen'] = parsed_args.prefixlen
+ ip_version = parsed_args.ip_version
+ if parsed_args.use_default_subnetpool:
+ body['use_default_subnetpool'] = True
+ if parsed_args.segment:
+ body['segment_id'] = neutronV20.find_resourceid_by_name_or_id(
+ self.get_client(), 'segment', parsed_args.segment)
+ if parsed_args.subnetpool:
+ if parsed_args.subnetpool == 'None':
+ _subnetpool_id = None
+ else:
+ _subnetpool = neutronV20.find_resource_by_name_or_id(
+ self.get_client(), 'subnetpool', parsed_args.subnetpool)
+ _subnetpool_id = _subnetpool['id']
+ # Now that we have the pool_id - let's just have a check on the
+ # ip version used in the pool
+ ip_version = _subnetpool['ip_version']
+ body['subnetpool_id'] = _subnetpool_id
+
+ # IP version needs to be set as IP version can be
+ # determined by subnetpool.
+ body['ip_version'] = ip_version
+
+ if parsed_args.cidr:
+ # With subnetpool, cidr is now optional for creating subnet.
+ cidr = parsed_args.cidr
+ body['cidr'] = cidr
+ unusable_cidr = '/32' if ip_version == 4 else '/128'
+ if cidr.endswith(unusable_cidr):
+ self.log.warning(_("An IPv%(ip)d subnet with a %(cidr)s CIDR "
+ "will have only one usable IP address so "
+ "the device attached to it will not have "
+ "any IP connectivity."),
+ {"ip": ip_version,
+ "cidr": unusable_cidr})
+
+ updatable_args2body(parsed_args, body, ip_version=ip_version)
+ if parsed_args.tenant_id:
+ body['tenant_id'] = parsed_args.tenant_id
+
+ return {'subnet': body}
+
+
+class DeleteSubnet(neutronV20.DeleteCommand):
+ """Delete a given subnet."""
+
+ resource = 'subnet'
+
+
+class UpdateSubnet(neutronV20.UpdateCommand):
+ """Update subnet's information."""
+
+ resource = 'subnet'
+
+ def add_known_arguments(self, parser):
+ add_updatable_arguments(parser)
+
+ def args2body(self, parsed_args):
+ body = {}
+ updatable_args2body(parsed_args, body, for_create=False)
+ return {'subnet': body}
diff --git a/neutronclient/neutron/v2_0/subnetpool.py b/neutronclient/neutron/v2_0/subnetpool.py
new file mode 100644
index 000000000..fdefc8a47
--- /dev/null
+++ b/neutronclient/neutron/v2_0/subnetpool.py
@@ -0,0 +1,154 @@
+# Copyright 2015 OpenStack Foundation.
+# All Rights Reserved
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from neutronclient._i18n import _
+from neutronclient.common import utils
+from neutronclient.neutron import v2_0 as neutronV20
+
+
+def _format_prefixes(subnetpool):
+ try:
+ return '\n'.join(pool for pool in subnetpool['prefixes'])
+ except (TypeError, KeyError):
+ return subnetpool['prefixes']
+
+
+def add_updatable_arguments(parser, for_create=False):
+ parser.add_argument(
+ '--description',
+ help=_('Description of subnetpool.'))
+ parser.add_argument(
+ '--min-prefixlen', type=int,
+ help=_('Subnetpool minimum prefix length.'))
+ parser.add_argument(
+ '--max-prefixlen', type=int,
+ help=_('Subnetpool maximum prefix length.'))
+ parser.add_argument(
+ '--default-prefixlen', type=int,
+ help=_('Subnetpool default prefix length.'))
+ parser.add_argument(
+ '--pool-prefix',
+ action='append', dest='prefixes',
+ required=for_create,
+ help=_('Subnetpool prefixes (This option can be repeated).'))
+ utils.add_boolean_argument(
+ parser, '--is-default',
+ help=_('Specify whether this should be the default subnetpool '
+ '(True meaning default).'))
+
+
+def updatable_args2body(parsed_args, body):
+ neutronV20.update_dict(parsed_args, body,
+ ['name', 'prefixes', 'default_prefixlen',
+ 'min_prefixlen', 'max_prefixlen', 'is_default',
+ 'description'])
+
+
+class ListSubnetPool(neutronV20.ListCommand):
+ """List subnetpools that belong to a given tenant."""
+
+ _formatters = {'prefixes': _format_prefixes, }
+ resource = 'subnetpool'
+ list_columns = ['id', 'name', 'prefixes',
+ 'default_prefixlen', 'address_scope_id', 'is_default']
+ pagination_support = True
+ sorting_support = True
+
+
+class ShowSubnetPool(neutronV20.ShowCommand):
+ """Show information of a given subnetpool."""
+
+ resource = 'subnetpool'
+
+
+class CreateSubnetPool(neutronV20.CreateCommand):
+ """Create a subnetpool for a given tenant."""
+
+ resource = 'subnetpool'
+
+ def add_known_arguments(self, parser):
+ add_updatable_arguments(parser, for_create=True)
+ parser.add_argument(
+ '--shared',
+ action='store_true',
+ help=_('Set the subnetpool as shared.'))
+ parser.add_argument(
+ 'name',
+ metavar='NAME',
+ help=_('Name of the subnetpool to be created.'))
+ parser.add_argument(
+ '--address-scope',
+ metavar='ADDRSCOPE',
+ help=_('ID or name of the address scope with which the subnetpool '
+ 'is associated. Prefixes must be unique across address '
+ 'scopes.'))
+
+ def args2body(self, parsed_args):
+ body = {'prefixes': parsed_args.prefixes}
+ updatable_args2body(parsed_args, body)
+ neutronV20.update_dict(parsed_args, body, ['tenant_id'])
+ if parsed_args.shared:
+ body['shared'] = True
+
+ # Parse and update for "address-scope" option
+ if parsed_args.address_scope:
+ _addrscope_id = neutronV20.find_resourceid_by_name_or_id(
+ self.get_client(), 'address_scope',
+ parsed_args.address_scope)
+ body['address_scope_id'] = _addrscope_id
+ return {'subnetpool': body}
+
+
+class DeleteSubnetPool(neutronV20.DeleteCommand):
+ """Delete a given subnetpool."""
+
+ resource = 'subnetpool'
+
+
+class UpdateSubnetPool(neutronV20.UpdateCommand):
+ """Update subnetpool's information."""
+
+ resource = 'subnetpool'
+
+ def add_known_arguments(self, parser):
+ add_updatable_arguments(parser)
+ parser.add_argument('--name',
+ help=_('Updated name of the subnetpool.'))
+ addrscope_args = parser.add_mutually_exclusive_group()
+ addrscope_args.add_argument('--address-scope',
+ metavar='ADDRSCOPE',
+ help=_('ID or name of the address scope '
+ 'with which the subnetpool is '
+ 'associated. Prefixes must be '
+ 'unique across address scopes.'))
+ addrscope_args.add_argument('--no-address-scope',
+ action='store_true',
+ help=_('Detach subnetpool from the '
+ 'address scope.'))
+
+ def args2body(self, parsed_args):
+ body = {}
+ updatable_args2body(parsed_args, body)
+
+ # Parse and update for "address-scope" option/s
+ if parsed_args.no_address_scope:
+ body['address_scope_id'] = None
+ elif parsed_args.address_scope:
+ _addrscope_id = neutronV20.find_resourceid_by_name_or_id(
+ self.get_client(), 'address_scope',
+ parsed_args.address_scope)
+ body['address_scope_id'] = _addrscope_id
+ return {'subnetpool': body}
diff --git a/neutronclient/neutron/v2_0/tag.py b/neutronclient/neutron/v2_0/tag.py
new file mode 100644
index 000000000..ff59b74ec
--- /dev/null
+++ b/neutronclient/neutron/v2_0/tag.py
@@ -0,0 +1,107 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from neutronclient._i18n import _
+from neutronclient.common import exceptions
+from neutronclient.neutron import v2_0 as neutronv20
+
+
+# List of resources can be set tag
+TAG_RESOURCES = ['network', 'subnet', 'port', 'router', 'subnetpool']
+
+
+def _convert_resource_args(client, parsed_args):
+ resource_type = client.get_resource_plural(parsed_args.resource_type)
+ resource_id = neutronv20.find_resourceid_by_name_or_id(
+ client, parsed_args.resource_type, parsed_args.resource)
+ return resource_type, resource_id
+
+
+def _add_common_arguments(parser):
+ parser.add_argument('--resource-type',
+ choices=TAG_RESOURCES,
+ dest='resource_type',
+ required=True,
+ help=_('Resource Type.'))
+ parser.add_argument('--resource',
+ required=True,
+ help=_('Resource name or ID.'))
+
+
+class AddTag(neutronv20.NeutronCommand):
+ """Add a tag into the resource."""
+
+ def get_parser(self, prog_name):
+ parser = super(AddTag, self).get_parser(prog_name)
+ _add_common_arguments(parser)
+ parser.add_argument('--tag',
+ required=True,
+ help=_('Tag to be added.'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.get_client()
+ if not parsed_args.tag:
+ raise exceptions.CommandError(
+ _('Cannot add an empty value as tag'))
+ resource_type, resource_id = _convert_resource_args(client,
+ parsed_args)
+ client.add_tag(resource_type, resource_id, parsed_args.tag)
+
+
+class ReplaceTag(neutronv20.NeutronCommand):
+ """Replace all tags on the resource."""
+
+ def get_parser(self, prog_name):
+ parser = super(ReplaceTag, self).get_parser(prog_name)
+ _add_common_arguments(parser)
+ parser.add_argument('--tag',
+ metavar='TAG',
+ action='append',
+ dest='tags',
+ required=True,
+ help=_('Tag (This option can be repeated).'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.get_client()
+ resource_type, resource_id = _convert_resource_args(client,
+ parsed_args)
+ body = {'tags': parsed_args.tags}
+ client.replace_tag(resource_type, resource_id, body)
+
+
+class RemoveTag(neutronv20.NeutronCommand):
+ """Remove a tag on the resource."""
+
+ def get_parser(self, prog_name):
+ parser = super(RemoveTag, self).get_parser(prog_name)
+ _add_common_arguments(parser)
+ tag_opt = parser.add_mutually_exclusive_group()
+ tag_opt.add_argument('--all',
+ action='store_true',
+ help=_('Remove all tags on the resource.'))
+ tag_opt.add_argument('--tag',
+ help=_('Tag to be removed.'))
+ return parser
+
+ def take_action(self, parsed_args):
+ if not parsed_args.all and not parsed_args.tag:
+ raise exceptions.CommandError(
+ _("--all or --tag must be specified"))
+ client = self.get_client()
+ resource_type, resource_id = _convert_resource_args(client,
+ parsed_args)
+ if parsed_args.all:
+ client.remove_tag_all(resource_type, resource_id)
+ else:
+ client.remove_tag(resource_type, resource_id, parsed_args.tag)
diff --git a/neutronclient/neutron/v2_0/vpn/__init__.py b/neutronclient/neutron/v2_0/vpn/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/neutronclient/neutron/v2_0/vpn/endpoint_group.py b/neutronclient/neutron/v2_0/vpn/endpoint_group.py
new file mode 100644
index 000000000..a112b315e
--- /dev/null
+++ b/neutronclient/neutron/v2_0/vpn/endpoint_group.py
@@ -0,0 +1,100 @@
+# (c) Copyright 2015 Cisco Systems, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from neutronclient._i18n import _
+from neutronclient.neutron import v2_0 as neutronv20
+
+
+def add_known_endpoint_group_arguments(parser, is_create=True):
+ parser.add_argument(
+ '--name',
+ help=_('Set a name for the endpoint group.'))
+ parser.add_argument(
+ '--description',
+ help=_('Set a description for the endpoint group.'))
+ if is_create:
+ parser.add_argument(
+ '--type',
+ required=is_create,
+ help=_('Type of endpoints in group (e.g. subnet, cidr, vlan).'))
+ parser.add_argument(
+ '--value',
+ action='append', dest='endpoints',
+ required=is_create,
+ help=_('Endpoint(s) for the group. Must all be of the same type.'))
+
+
+class ListEndpointGroup(neutronv20.ListCommand):
+ """List VPN endpoint groups that belong to a given tenant."""
+
+ resource = 'endpoint_group'
+ list_columns = ['id', 'name', 'type', 'endpoints']
+ _formatters = {}
+ pagination_support = True
+ sorting_support = True
+
+
+class ShowEndpointGroup(neutronv20.ShowCommand):
+ """Show a specific VPN endpoint group."""
+
+ resource = 'endpoint_group'
+
+
+class CreateEndpointGroup(neutronv20.CreateCommand):
+ """Create a VPN endpoint group."""
+ resource = 'endpoint_group'
+
+ def add_known_arguments(self, parser):
+ add_known_endpoint_group_arguments(parser)
+
+ def subnet_name2id(self, endpoint):
+ return neutronv20.find_resourceid_by_name_or_id(self.get_client(),
+ 'subnet', endpoint)
+
+ def args2body(self, parsed_args):
+ if parsed_args.type == 'subnet':
+ endpoints = [self.subnet_name2id(ep)
+ for ep in parsed_args.endpoints]
+ else:
+ endpoints = parsed_args.endpoints
+
+ body = {'endpoints': endpoints}
+
+ neutronv20.update_dict(parsed_args, body,
+ ['name', 'description',
+ 'tenant_id', 'type'])
+ return {self.resource: body}
+
+
+class UpdateEndpointGroup(neutronv20.UpdateCommand):
+ """Update a given VPN endpoint group."""
+
+ resource = 'endpoint_group'
+
+ def add_known_arguments(self, parser):
+ add_known_endpoint_group_arguments(parser, is_create=False)
+
+ def args2body(self, parsed_args):
+ body = {}
+ neutronv20.update_dict(parsed_args, body,
+ ['name', 'description'])
+ return {self.resource: body}
+
+
+class DeleteEndpointGroup(neutronv20.DeleteCommand):
+ """Delete a given VPN endpoint group."""
+
+ resource = 'endpoint_group'
diff --git a/neutronclient/neutron/v2_0/vpn/ikepolicy.py b/neutronclient/neutron/v2_0/vpn/ikepolicy.py
new file mode 100644
index 000000000..20a96aaea
--- /dev/null
+++ b/neutronclient/neutron/v2_0/vpn/ikepolicy.py
@@ -0,0 +1,128 @@
+# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import argparse
+
+from neutronclient._i18n import _
+from neutronclient.common import utils
+from neutronclient.neutron import v2_0 as neutronv20
+from neutronclient.neutron.v2_0.vpn import utils as vpn_utils
+
+
+def add_common_args(parser, is_create=True):
+ parser.add_argument(
+ '--description',
+ help=_('Description of the IKE policy.'))
+ parser.add_argument(
+ '--auth-algorithm',
+ type=utils.convert_to_lowercase,
+ default='sha1' if is_create else argparse.SUPPRESS,
+ choices=['sha1', 'sha256', 'sha384', 'sha512'],
+ help=_('Authentication algorithm, default:sha1.'))
+ parser.add_argument(
+ '--encryption-algorithm',
+ default='aes-128' if is_create else argparse.SUPPRESS,
+ type=utils.convert_to_lowercase,
+ help=_('Encryption algorithm, default:aes-128.'))
+ parser.add_argument(
+ '--phase1-negotiation-mode',
+ default='main' if is_create else argparse.SUPPRESS,
+ choices=['main'],
+ type=utils.convert_to_lowercase,
+ help=_('IKE Phase1 negotiation mode, default:main.'))
+ parser.add_argument(
+ '--ike-version',
+ default='v1' if is_create else argparse.SUPPRESS,
+ choices=['v1', 'v2'],
+ type=utils.convert_to_lowercase,
+ help=_('IKE version for the policy, default:v1.'))
+ parser.add_argument(
+ '--pfs',
+ default='group5' if is_create else argparse.SUPPRESS,
+ type=utils.convert_to_lowercase,
+ help=_('Perfect Forward Secrecy, default:group5.'))
+ parser.add_argument(
+ '--lifetime',
+ metavar="units=UNITS,value=VALUE",
+ type=utils.str2dict_type(optional_keys=['units', 'value']),
+ help=vpn_utils.lifetime_help("IKE"))
+
+
+def parse_common_args2body(parsed_args, body):
+ neutronv20.update_dict(parsed_args, body,
+ ['auth_algorithm', 'encryption_algorithm',
+ 'phase1_negotiation_mode', 'ike_version',
+ 'pfs', 'name', 'description', 'tenant_id'])
+ if parsed_args.lifetime:
+ vpn_utils.validate_lifetime_dict(parsed_args.lifetime)
+ body['lifetime'] = parsed_args.lifetime
+ return body
+
+
+class ListIKEPolicy(neutronv20.ListCommand):
+ """List IKE policies that belong to a tenant."""
+
+ resource = 'ikepolicy'
+ list_columns = ['id', 'name', 'auth_algorithm',
+ 'encryption_algorithm', 'ike_version', 'pfs']
+ _formatters = {}
+ pagination_support = True
+ sorting_support = True
+
+
+class ShowIKEPolicy(neutronv20.ShowCommand):
+ """Show information of a given IKE policy."""
+
+ resource = 'ikepolicy'
+ help_resource = 'IKE policy'
+
+
+class CreateIKEPolicy(neutronv20.CreateCommand):
+ """Create an IKE policy."""
+
+ resource = 'ikepolicy'
+
+ def add_known_arguments(self, parser):
+ parser.add_argument(
+ 'name', metavar='NAME',
+ help=_('Name of the IKE policy.'))
+ add_common_args(parser)
+
+ def args2body(self, parsed_args):
+ return {'ikepolicy': parse_common_args2body(parsed_args, body={})}
+
+
+class UpdateIKEPolicy(neutronv20.UpdateCommand):
+ """Update a given IKE policy."""
+
+ resource = 'ikepolicy'
+ help_resource = 'IKE policy'
+
+ def add_known_arguments(self, parser):
+ parser.add_argument(
+ '--name',
+ help=_('Updated name of the IKE policy.'))
+ add_common_args(parser, is_create=False)
+
+ def args2body(self, parsed_args):
+ return {'ikepolicy': parse_common_args2body(parsed_args, body={})}
+
+
+class DeleteIKEPolicy(neutronv20.DeleteCommand):
+ """Delete a given IKE policy."""
+
+ resource = 'ikepolicy'
+ help_resource = 'IKE policy'
diff --git a/neutronclient/neutron/v2_0/vpn/ipsec_site_connection.py b/neutronclient/neutron/v2_0/vpn/ipsec_site_connection.py
new file mode 100644
index 000000000..03a635f6e
--- /dev/null
+++ b/neutronclient/neutron/v2_0/vpn/ipsec_site_connection.py
@@ -0,0 +1,220 @@
+# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import argparse
+
+from oslo_serialization import jsonutils
+
+from neutronclient._i18n import _
+from neutronclient.common import exceptions
+from neutronclient.common import utils
+from neutronclient.neutron import v2_0 as neutronv20
+from neutronclient.neutron.v2_0.vpn import utils as vpn_utils
+
+
+def _format_peer_cidrs(ipsec_site_connection):
+ try:
+ return '\n'.join([jsonutils.dumps(cidrs) for cidrs in
+ ipsec_site_connection['peer_cidrs']])
+ except (TypeError, KeyError):
+ return ''
+
+
+class ListIPsecSiteConnection(neutronv20.ListCommand):
+ """List IPsec site connections that belong to a given tenant."""
+
+ resource = 'ipsec_site_connection'
+ _formatters = {'peer_cidrs': _format_peer_cidrs}
+ list_columns = [
+ 'id', 'name', 'peer_address', 'auth_mode', 'status']
+ pagination_support = True
+ sorting_support = True
+
+
+class ShowIPsecSiteConnection(neutronv20.ShowCommand):
+ """Show information of a given IPsec site connection."""
+
+ resource = 'ipsec_site_connection'
+ help_resource = 'IPsec site connection'
+
+
+class IPsecSiteConnectionMixin(object):
+
+ def add_known_arguments(self, parser, is_create=True):
+ parser.add_argument(
+ '--name',
+ help=_('Set friendly name for the connection.'))
+ parser.add_argument(
+ '--description',
+ help=_('Set a description for the connection.'))
+ parser.add_argument(
+ '--dpd',
+ metavar="action=ACTION,interval=INTERVAL,timeout=TIMEOUT",
+ type=utils.str2dict_type(
+ optional_keys=['action', 'interval', 'timeout']),
+ help=vpn_utils.dpd_help("IPsec connection."))
+ parser.add_argument(
+ '--local-ep-group',
+ help=_('Local endpoint group ID/name with subnet(s) for '
+ 'IPSec connection.'))
+ parser.add_argument(
+ '--peer-ep-group',
+ help=_('Peer endpoint group ID/name with CIDR(s) for '
+ 'IPSec connection.'))
+ parser.add_argument(
+ '--peer-cidr',
+ action='append', dest='peer_cidrs',
+ help=_('[DEPRECATED in Mitaka] Remote subnet(s) in CIDR format. '
+ 'Cannot be specified when using endpoint groups. Only '
+ 'applicable, if subnet provided for VPN service.'))
+ parser.add_argument(
+ '--peer-id',
+ required=is_create,
+ help=_('Peer router identity for authentication. Can be '
+ 'IPv4/IPv6 address, e-mail address, key id, or FQDN.'))
+ parser.add_argument(
+ '--peer-address',
+ required=is_create,
+ help=_('Peer gateway public IPv4/IPv6 address or FQDN.'))
+ parser.add_argument(
+ '--psk',
+ required=is_create,
+ help=_('Pre-shared key string.'))
+ parser.add_argument(
+ '--mtu',
+ default='1500' if is_create else argparse.SUPPRESS,
+ help=_('MTU size for the connection, default:1500.'))
+ parser.add_argument(
+ '--initiator',
+ default='bi-directional' if is_create else argparse.SUPPRESS,
+ choices=['bi-directional', 'response-only'],
+ help=_('Initiator state in lowercase, default:bi-directional'))
+
+ def args2body(self, parsed_args, body=None):
+ """Add in conditional args and then return all conn info."""
+
+ if body is None:
+ body = {}
+ if parsed_args.dpd:
+ vpn_utils.validate_dpd_dict(parsed_args.dpd)
+ body['dpd'] = parsed_args.dpd
+ if parsed_args.local_ep_group:
+ _local_epg = neutronv20.find_resourceid_by_name_or_id(
+ self.get_client(), 'endpoint_group',
+ parsed_args.local_ep_group)
+ body['local_ep_group_id'] = _local_epg
+ if parsed_args.peer_ep_group:
+ _peer_epg = neutronv20.find_resourceid_by_name_or_id(
+ self.get_client(), 'endpoint_group',
+ parsed_args.peer_ep_group)
+ body['peer_ep_group_id'] = _peer_epg
+ if hasattr(parsed_args, 'mtu') and int(parsed_args.mtu) < 68:
+ message = _("Invalid MTU value: MTU must be "
+ "greater than or equal to 68.")
+ raise exceptions.CommandError(message)
+ # ToDo (Reedip) : Remove below check when peer-cidr is removed
+ if parsed_args.peer_cidrs and parsed_args.local_ep_group:
+ message = _("You cannot specify both endpoint groups and peer "
+ "CIDR(s).")
+ raise exceptions.CommandError(message)
+ neutronv20.update_dict(parsed_args, body,
+ ['peer_id', 'mtu', 'initiator', 'psk',
+ 'peer_address', 'name', 'description',
+ 'peer_cidrs'])
+ return {self.resource: body}
+
+
+class CreateIPsecSiteConnection(IPsecSiteConnectionMixin,
+ neutronv20.CreateCommand):
+ """Create an IPsec site connection."""
+ resource = 'ipsec_site_connection'
+
+ def add_known_arguments(self, parser):
+ parser.add_argument(
+ '--admin-state-down',
+ default=True, action='store_false',
+ help=_('Set admin state up to false.'))
+ parser.add_argument(
+ '--vpnservice-id', metavar='VPNSERVICE',
+ required=True,
+ help=_('VPN service instance ID associated with this connection.'))
+ parser.add_argument(
+ '--ikepolicy-id', metavar='IKEPOLICY',
+ required=True,
+ help=_('IKE policy ID associated with this connection.'))
+ parser.add_argument(
+ '--ipsecpolicy-id', metavar='IPSECPOLICY',
+ required=True,
+ help=_('IPsec policy ID associated with this connection.'))
+ super(CreateIPsecSiteConnection, self).add_known_arguments(parser)
+
+ def args2body(self, parsed_args):
+ _vpnservice_id = neutronv20.find_resourceid_by_name_or_id(
+ self.get_client(), 'vpnservice',
+ parsed_args.vpnservice_id)
+ _ikepolicy_id = neutronv20.find_resourceid_by_name_or_id(
+ self.get_client(), 'ikepolicy',
+ parsed_args.ikepolicy_id)
+ _ipsecpolicy_id = neutronv20.find_resourceid_by_name_or_id(
+ self.get_client(), 'ipsecpolicy',
+ parsed_args.ipsecpolicy_id)
+ body = {
+ 'vpnservice_id': _vpnservice_id,
+ 'ikepolicy_id': _ikepolicy_id,
+ 'ipsecpolicy_id': _ipsecpolicy_id,
+ 'admin_state_up': parsed_args.admin_state_down,
+ }
+ if parsed_args.tenant_id:
+ body['tenant_id'] = parsed_args.tenant_id
+
+ if (bool(parsed_args.local_ep_group) !=
+ bool(parsed_args.peer_ep_group)):
+ message = _("You must specify both local and peer endpoint "
+ "groups.")
+ raise exceptions.CommandError(message)
+ if not parsed_args.peer_cidrs and not parsed_args.local_ep_group:
+ message = _("You must specify endpoint groups or peer CIDR(s).")
+ raise exceptions.CommandError(message)
+ return super(CreateIPsecSiteConnection, self).args2body(parsed_args,
+ body)
+
+
+class UpdateIPsecSiteConnection(IPsecSiteConnectionMixin,
+ neutronv20.UpdateCommand):
+ """Update a given IPsec site connection."""
+
+ resource = 'ipsec_site_connection'
+ help_resource = 'IPsec site connection'
+
+ def add_known_arguments(self, parser):
+ utils.add_boolean_argument(
+ parser, '--admin-state-up',
+ help=_('Update the administrative state. (True meaning "Up")'))
+ super(UpdateIPsecSiteConnection, self).add_known_arguments(parser,
+ False)
+
+ def args2body(self, parsed_args):
+ body = {}
+ neutronv20.update_dict(parsed_args, body, ['admin_state_up'])
+ return super(UpdateIPsecSiteConnection, self).args2body(parsed_args,
+ body)
+
+
+class DeleteIPsecSiteConnection(neutronv20.DeleteCommand):
+ """Delete a given IPsec site connection."""
+
+ resource = 'ipsec_site_connection'
+ help_resource = 'IPsec site connection'
diff --git a/neutronclient/neutron/v2_0/vpn/ipsecpolicy.py b/neutronclient/neutron/v2_0/vpn/ipsecpolicy.py
new file mode 100644
index 000000000..ea70155b7
--- /dev/null
+++ b/neutronclient/neutron/v2_0/vpn/ipsecpolicy.py
@@ -0,0 +1,129 @@
+# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import argparse
+
+from neutronclient._i18n import _
+from neutronclient.common import utils
+from neutronclient.neutron import v2_0 as neutronv20
+from neutronclient.neutron.v2_0.vpn import utils as vpn_utils
+
+
+def add_common_args(parser, is_create=True):
+ parser.add_argument(
+ '--auth-algorithm',
+ default='sha1' if is_create else argparse.SUPPRESS,
+ type=utils.convert_to_lowercase,
+ choices=['sha1', 'sha256', 'sha384', 'sha512'],
+ help=_('Authentication algorithm for IPsec policy, default:sha1.'))
+ parser.add_argument(
+ '--description',
+ help=_('Description of the IPsec policy.'))
+ parser.add_argument(
+ '--encapsulation-mode',
+ default='tunnel' if is_create else argparse.SUPPRESS,
+ choices=['tunnel', 'transport'],
+ type=utils.convert_to_lowercase,
+ help=_('Encapsulation mode for IPsec policy, default:tunnel.'))
+ parser.add_argument(
+ '--encryption-algorithm',
+ default='aes-128' if is_create else argparse.SUPPRESS,
+ type=utils.convert_to_lowercase,
+ help=_('Encryption algorithm for IPsec policy, default:aes-128.'))
+ parser.add_argument(
+ '--lifetime',
+ metavar="units=UNITS,value=VALUE",
+ type=utils.str2dict_type(optional_keys=['units', 'value']),
+ help=vpn_utils.lifetime_help("IPsec"))
+ parser.add_argument(
+ '--pfs',
+ default='group5' if is_create else argparse.SUPPRESS,
+ type=utils.convert_to_lowercase,
+ help=_('Perfect Forward Secrecy for IPsec policy, default:group5.'))
+ parser.add_argument(
+ '--transform-protocol',
+ default='esp' if is_create else argparse.SUPPRESS,
+ type=utils.convert_to_lowercase,
+ choices=['esp', 'ah', 'ah-esp'],
+ help=_('Transform protocol for IPsec policy, default:esp.'))
+
+
+def parse_common_args2body(parsed_args, body):
+ neutronv20.update_dict(parsed_args, body,
+ ['auth_algorithm', 'encryption_algorithm',
+ 'encapsulation_mode', 'transform_protocol',
+ 'pfs', 'name', 'description', 'tenant_id'])
+ if parsed_args.lifetime:
+ vpn_utils.validate_lifetime_dict(parsed_args.lifetime)
+ body['lifetime'] = parsed_args.lifetime
+ return body
+
+
+class ListIPsecPolicy(neutronv20.ListCommand):
+ """List IPsec policies that belong to a given tenant connection."""
+
+ resource = 'ipsecpolicy'
+ list_columns = ['id', 'name', 'auth_algorithm',
+ 'encryption_algorithm', 'pfs']
+ _formatters = {}
+ pagination_support = True
+ sorting_support = True
+
+
+class ShowIPsecPolicy(neutronv20.ShowCommand):
+ """Show information of a given IPsec policy."""
+
+ resource = 'ipsecpolicy'
+ help_resource = 'IPsec policy'
+
+
+class CreateIPsecPolicy(neutronv20.CreateCommand):
+ """Create an IPsec policy."""
+
+ resource = 'ipsecpolicy'
+ help_resource = 'IPsec policy'
+
+ def add_known_arguments(self, parser):
+ parser.add_argument(
+ 'name', metavar='NAME',
+ help=_('Name of the IPsec policy.'))
+ add_common_args(parser)
+
+ def args2body(self, parsed_args):
+ return {'ipsecpolicy': parse_common_args2body(parsed_args, body={})}
+
+
+class UpdateIPsecPolicy(neutronv20.UpdateCommand):
+ """Update a given IPsec policy."""
+
+ resource = 'ipsecpolicy'
+ help_resource = 'IPsec policy'
+
+ def add_known_arguments(self, parser):
+ parser.add_argument(
+ '--name',
+ help=_('Updated name of the IPsec policy.'))
+ add_common_args(parser, is_create=False)
+
+ def args2body(self, parsed_args):
+ return {'ipsecpolicy': parse_common_args2body(parsed_args, body={})}
+
+
+class DeleteIPsecPolicy(neutronv20.DeleteCommand):
+ """Delete a given IPsec policy."""
+
+ resource = 'ipsecpolicy'
+ help_resource = 'IPsec policy'
diff --git a/neutronclient/neutron/v2_0/vpn/utils.py b/neutronclient/neutron/v2_0/vpn/utils.py
new file mode 100644
index 000000000..65d9c7c4b
--- /dev/null
+++ b/neutronclient/neutron/v2_0/vpn/utils.py
@@ -0,0 +1,112 @@
+# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+
+"""VPN Utilities and helper functions."""
+
+
+from neutronclient._i18n import _
+from neutronclient.common import exceptions
+
+dpd_supported_actions = ['hold', 'clear', 'restart',
+ 'restart-by-peer', 'disabled']
+dpd_supported_keys = ['action', 'interval', 'timeout']
+
+lifetime_keys = ['units', 'value']
+lifetime_units = ['seconds']
+
+
+def validate_dpd_dict(dpd_dict):
+ for key, value in dpd_dict.items():
+ if key not in dpd_supported_keys:
+ message = _(
+ "DPD Dictionary KeyError: "
+ "Reason-Invalid DPD key : "
+ "'%(key)s' not in %(supported_key)s") % {
+ 'key': key, 'supported_key': dpd_supported_keys}
+ raise exceptions.CommandError(message)
+ if key == 'action' and value not in dpd_supported_actions:
+ message = _(
+ "DPD Dictionary ValueError: "
+ "Reason-Invalid DPD action : "
+ "'%(key_value)s' not in %(supported_action)s") % {
+ 'key_value': value,
+ 'supported_action': dpd_supported_actions}
+ raise exceptions.CommandError(message)
+ if key in ('interval', 'timeout'):
+ try:
+ if int(value) <= 0:
+ raise ValueError()
+ except ValueError:
+ message = _(
+ "DPD Dictionary ValueError: "
+ "Reason-Invalid positive integer value: "
+ "'%(key)s' = %(value)s") % {
+ 'key': key, 'value': value}
+ raise exceptions.CommandError(message)
+ else:
+ dpd_dict[key] = int(value)
+ return
+
+
+def validate_lifetime_dict(lifetime_dict):
+
+ for key, value in lifetime_dict.items():
+ if key not in lifetime_keys:
+ message = _(
+ "Lifetime Dictionary KeyError: "
+ "Reason-Invalid unit key : "
+ "'%(key)s' not in %(supported_key)s") % {
+ 'key': key, 'supported_key': lifetime_keys}
+ raise exceptions.CommandError(message)
+ if key == 'units' and value not in lifetime_units:
+ message = _(
+ "Lifetime Dictionary ValueError: "
+ "Reason-Invalid units : "
+ "'%(key_value)s' not in %(supported_units)s") % {
+ 'key_value': key, 'supported_units': lifetime_units}
+ raise exceptions.CommandError(message)
+ if key == 'value':
+ try:
+ if int(value) < 60:
+ raise ValueError()
+ except ValueError:
+ message = _(
+ "Lifetime Dictionary ValueError: "
+ "Reason-Invalid value should be at least 60:"
+ "'%(key_value)s' = %(value)s") % {
+ 'key_value': key, 'value': value}
+ raise exceptions.CommandError(message)
+ else:
+ lifetime_dict['value'] = int(value)
+ return
+
+
+def lifetime_help(policy):
+ lifetime = _("%s lifetime attributes. "
+ "'units'-seconds, default:seconds. "
+ "'value'-non negative integer, default:3600.") % policy
+ return lifetime
+
+
+def dpd_help(policy):
+ dpd = _(" %s Dead Peer Detection attributes."
+ " 'action'-hold,clear,disabled,restart,restart-by-peer."
+ " 'interval' and 'timeout' are non negative integers. "
+ " 'interval' should be less than 'timeout' value. "
+ " 'action', default:hold 'interval', default:30, "
+ " 'timeout', default:120.") % policy.capitalize()
+ return dpd
diff --git a/neutronclient/neutron/v2_0/vpn/vpnservice.py b/neutronclient/neutron/v2_0/vpn/vpnservice.py
new file mode 100644
index 000000000..dae531408
--- /dev/null
+++ b/neutronclient/neutron/v2_0/vpn/vpnservice.py
@@ -0,0 +1,117 @@
+# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from neutronclient._i18n import _
+from neutronclient.common import utils
+from neutronclient.neutron import v2_0 as neutronv20
+
+
+def add_common_args(parser):
+ parser.add_argument(
+ '--name',
+ help=_('Name for the VPN service.'))
+ parser.add_argument(
+ '--description',
+ help=_('Description for the VPN service.'))
+
+
+def common_args2body(parsed_args, body):
+ neutronv20.update_dict(parsed_args, body,
+ ['name', 'description'])
+
+
+class ListVPNService(neutronv20.ListCommand):
+ """List VPN service configurations that belong to a given tenant."""
+
+ resource = 'vpnservice'
+ list_columns = [
+ 'id', 'name', 'router_id', 'status'
+ ]
+ _formatters = {}
+ pagination_support = True
+ sorting_support = True
+
+
+class ShowVPNService(neutronv20.ShowCommand):
+ """Show information of a given VPN service."""
+
+ resource = 'vpnservice'
+ help_resource = 'VPN service'
+
+
+class CreateVPNService(neutronv20.CreateCommand):
+ """Create a VPN service."""
+ resource = 'vpnservice'
+
+ def add_known_arguments(self, parser):
+ parser.add_argument(
+ '--admin-state-down',
+ dest='admin_state', action='store_false',
+ help=_('Set admin state up to false.'))
+ parser.add_argument(
+ 'router', metavar='ROUTER',
+ help=_('Router unique identifier for the VPN service.'))
+ parser.add_argument(
+ 'subnet', nargs='?', metavar='SUBNET',
+ help=_('[DEPRECATED in Mitaka] Unique identifier for the local '
+ 'private subnet.'))
+ add_common_args(parser)
+
+ def args2body(self, parsed_args):
+ if parsed_args.subnet:
+ _subnet_id = neutronv20.find_resourceid_by_name_or_id(
+ self.get_client(), 'subnet', parsed_args.subnet)
+ else:
+ _subnet_id = None
+ _router_id = neutronv20.find_resourceid_by_name_or_id(
+ self.get_client(), 'router',
+ parsed_args.router)
+
+ body = {'subnet_id': _subnet_id,
+ 'router_id': _router_id,
+ 'admin_state_up': parsed_args.admin_state}
+ neutronv20.update_dict(parsed_args, body,
+ ['tenant_id'])
+ common_args2body(parsed_args, body)
+ return {self.resource: body}
+
+
+class UpdateVPNService(neutronv20.UpdateCommand):
+ """Update a given VPN service."""
+
+ resource = 'vpnservice'
+ help_resource = 'VPN service'
+
+ def add_known_arguments(self, parser):
+ add_common_args(parser)
+ utils.add_boolean_argument(
+ parser, '--admin-state-up',
+ help=_('Update the admin state for the VPN Service.'
+ '(True means UP)'))
+
+ def args2body(self, parsed_args):
+ body = {}
+ common_args2body(parsed_args, body)
+ neutronv20.update_dict(parsed_args, body,
+ ['admin_state_up'])
+ return {self.resource: body}
+
+
+class DeleteVPNService(neutronv20.DeleteCommand):
+ """Delete a given VPN service."""
+
+ resource = 'vpnservice'
+ help_resource = 'VPN service'
diff --git a/neutronclient/osc/__init__.py b/neutronclient/osc/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/neutronclient/osc/plugin.py b/neutronclient/osc/plugin.py
new file mode 100644
index 000000000..c98099f2a
--- /dev/null
+++ b/neutronclient/osc/plugin.py
@@ -0,0 +1,63 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+"""OpenStackClient plugin for advanced Networking service."""
+
+import logging
+
+# TODO(rtheis/amotoki): Add functional test infrastructure for OSC
+# plugin commands.
+# TODO(amotoki): Add and update document on OSC plugin.
+
+from osc_lib import utils
+
+LOG = logging.getLogger(__name__)
+
+DEFAULT_API_VERSION = '2.0'
+API_VERSION_OPTION = 'os_network_api_version'
+# NOTE(rtheis): API_NAME must NOT be set to 'network' since
+# 'network' is owned by OSC! The OSC 'network' client uses
+# the OpenStack SDK.
+API_NAME = 'neutronclient'
+API_VERSIONS = {
+ '2.0': 'neutronclient.v2_0.client.Client',
+ '2': 'neutronclient.v2_0.client.Client',
+}
+
+
+def make_client(instance):
+ """Returns an neutron client."""
+ neutron_client = utils.get_client_class(
+ API_NAME,
+ instance._api_version[API_NAME],
+ API_VERSIONS)
+ LOG.debug('Instantiating neutron client: %s', neutron_client)
+
+ # TODO(amotoki): Check the following arguments need to be passed
+ # to neutronclient class. Check keystoneauth code.
+ # - endpoint_type (do we need to specify it explicitly?)
+ # - auth (session object contains auth. Is it required?)
+ client = neutron_client(session=instance.session,
+ region_name=instance.region_name,
+ endpoint_type=instance.interface,
+ insecure=not instance.verify,
+ ca_cert=instance.cacert)
+ return client
+
+
+def build_option_parser(parser):
+ """Hook to add global options"""
+
+ # NOTE(amotoki): At now we register no option.
+ # OSC itself has an option for Network API version # and we refer to it.
+ return parser
diff --git a/neutronclient/osc/utils.py b/neutronclient/osc/utils.py
new file mode 100644
index 000000000..39b17b640
--- /dev/null
+++ b/neutronclient/osc/utils.py
@@ -0,0 +1,130 @@
+# Copyright 2016 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""This module should contain OSC plugin generic methods.
+
+Methods in this module are candidates adopted to osc-lib.
+
+Stuffs specific to neutronclient OSC plugin should not be added
+to this module. They should go to neutronclient.osc.v2.utils.
+"""
+
+from keystoneclient import exceptions as identity_exc
+from keystoneclient.v3 import domains
+from keystoneclient.v3 import projects
+from osc_lib import utils
+
+from neutronclient._i18n import _
+
+
+# TODO(amotoki): Use osc-lib version once osc-lib provides this.
+def add_project_owner_option_to_parser(parser):
+ """Register project and project domain options.
+
+ :param parser: argparse.Argument parser object.
+
+ """
+ parser.add_argument(
+ '--project',
+ metavar='',
+ help=_("Owner's project (name or ID)")
+ )
+ # Borrowed from openstackclient.identity.common
+ # as it is not exposed officially.
+ parser.add_argument(
+ '--project-domain',
+ metavar='',
+ help=_('Domain the project belongs to (name or ID). '
+ 'This can be used in case collisions between project names '
+ 'exist.'),
+ )
+
+
+# The following methods are borrowed from openstackclient.identity.common
+# as it is not exposed officially.
+# TODO(amotoki): Use osc-lib version once osc-lib provides this.
+
+
+def find_domain(identity_client, name_or_id):
+ return _find_identity_resource(identity_client.domains, name_or_id,
+ domains.Domain)
+
+
+def find_project(identity_client, name_or_id, domain_name_or_id=None):
+ domain_id = _get_domain_id_if_requested(identity_client, domain_name_or_id)
+ if not domain_id:
+ return _find_identity_resource(identity_client.projects, name_or_id,
+ projects.Project)
+ else:
+ return _find_identity_resource(identity_client.projects, name_or_id,
+ projects.Project, domain_id=domain_id)
+
+
+def _get_domain_id_if_requested(identity_client, domain_name_or_id):
+ if not domain_name_or_id:
+ return None
+ domain = find_domain(identity_client, domain_name_or_id)
+ return domain.id
+
+
+def _find_identity_resource(identity_client_manager, name_or_id,
+ resource_type, **kwargs):
+ """Find a specific identity resource.
+
+ Using keystoneclient's manager, attempt to find a specific resource by its
+ name or ID. If Forbidden to find the resource (a common case if the user
+ does not have permission), then return the resource by creating a local
+ instance of keystoneclient's Resource.
+
+ The parameter identity_client_manager is a keystoneclient manager,
+ for example: keystoneclient.v3.users or keystoneclient.v3.projects.
+
+ The parameter resource_type is a keystoneclient resource, for example:
+ keystoneclient.v3.users.User or keystoneclient.v3.projects.Project.
+
+ :param identity_client_manager: the manager that contains the resource
+ :type identity_client_manager: `keystoneclient.base.CrudManager`
+ :param name_or_id: the resources's name or ID
+ :type name_or_id: string
+ :param resource_type: class that represents the resource type
+ :type resource_type: `keystoneclient.base.Resource`
+
+ :returns: the resource in question
+ :rtype: `keystoneclient.base.Resource`
+
+ """
+
+ try:
+ identity_resource = utils.find_resource(identity_client_manager,
+ name_or_id, **kwargs)
+ if identity_resource is not None:
+ return identity_resource
+ except identity_exc.Forbidden:
+ pass
+
+ return resource_type(None, {'id': name_or_id, 'name': name_or_id})
+
+
+# The above are borrowed from openstackclient.identity.common.
+# DO NOT ADD original methods in neutronclient repo to the above area.
+
+
+def _get_columns(item):
+ column_map = {}
+ hidden_columns = ['location', 'tenant_id']
+ return utils.get_osc_show_columns_for_sdk_resource(
+ item,
+ column_map,
+ hidden_columns
+ )
diff --git a/neutronclient/osc/v2/__init__.py b/neutronclient/osc/v2/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/neutronclient/osc/v2/fwaas/__init__.py b/neutronclient/osc/v2/fwaas/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/quantumclient/quantum/__init__.py b/neutronclient/osc/v2/fwaas/constants.py
similarity index 59%
rename from quantumclient/quantum/__init__.py
rename to neutronclient/osc/v2/fwaas/constants.py
index 63c3905e2..a5a74af73 100644
--- a/quantumclient/quantum/__init__.py
+++ b/neutronclient/osc/v2/fwaas/constants.py
@@ -1,5 +1,5 @@
-# Copyright 2012 OpenStack LLC.
-# All Rights Reserved.
+# Copyright 2016-2017 FUJITSU LIMITED
+# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@@ -12,3 +12,17 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
+#
+
+# TODO(slaweq): remove it once "logging" OSC plugin will be moved to the
+# python-openstackclient repo
+
+FWG = 'firewall_group'
+FWGS = 'firewall_groups'
+FWP = 'firewall_policy'
+FWPS = 'firewall_policies'
+FWR = 'firewall_rule'
+FWRS = 'firewall_rules'
+CMD_FWG = 'fwaas_' + FWG
+CMD_FWP = 'fwaas_' + FWP
+CMD_FWR = 'fwaas_' + FWR
diff --git a/neutronclient/osc/v2/lbaas/__init__.py b/neutronclient/osc/v2/lbaas/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/neutronclient/osc/v2/logging/__init__.py b/neutronclient/osc/v2/logging/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/neutronclient/osc/v2/logging/network_log.py b/neutronclient/osc/v2/logging/network_log.py
new file mode 100644
index 000000000..b3b8204c3
--- /dev/null
+++ b/neutronclient/osc/v2/logging/network_log.py
@@ -0,0 +1,304 @@
+# Copyright 2017-2018 FUJTISU LIMITED.
+# All Rights Reserved
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import copy
+
+from osc_lib.command import command
+from osc_lib import exceptions
+from osc_lib import utils
+from osc_lib.utils import columns as column_util
+from oslo_log import log as logging
+
+from neutronclient._i18n import _
+from neutronclient.common import utils as nc_utils
+from neutronclient.osc import utils as osc_utils
+from neutronclient.osc.v2.fwaas import constants as fwaas_const
+
+LOG = logging.getLogger(__name__)
+
+_attr_map = (
+ ('id', 'ID', column_util.LIST_BOTH),
+ ('description', 'Description', column_util.LIST_LONG_ONLY),
+ ('enabled', 'Enabled', column_util.LIST_BOTH),
+ ('name', 'Name', column_util.LIST_BOTH),
+ ('target_id', 'Target', column_util.LIST_LONG_ONLY),
+ ('project_id', 'Project', column_util.LIST_LONG_ONLY),
+ ('resource_id', 'Resource', column_util.LIST_LONG_ONLY),
+ ('resource_type', 'Type', column_util.LIST_BOTH),
+ ('event', 'Event', column_util.LIST_LONG_ONLY),
+ ('summary', 'Summary', column_util.LIST_SHORT_ONLY),
+)
+
+_attr_map_for_loggable = (
+ ('type', 'Supported types', column_util.LIST_BOTH),
+)
+
+NET_LOG = 'network_log'
+
+
+def _get_common_parser(parser):
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Description of the network log'))
+ enable_group = parser.add_mutually_exclusive_group()
+ enable_group.add_argument(
+ '--enable',
+ action='store_true',
+ help=_('Enable this log'))
+ enable_group.add_argument(
+ '--disable',
+ action='store_true',
+ help=_('Disable this log (default is enabled)'))
+ return parser
+
+
+def _get_common_attrs(client_manager, parsed_args, is_create=True):
+ attrs = {}
+ client = client_manager.neutronclient
+
+ if is_create:
+ if 'project' in parsed_args and parsed_args.project is not None:
+ attrs['project_id'] = osc_utils.find_project(
+ client_manager.identity,
+ parsed_args.project,
+ parsed_args.project_domain,
+ ).id
+ resource_type = parsed_args.resource_type
+ attrs['resource_type'] = resource_type
+ if parsed_args.resource:
+ cmd_resource = None
+ if resource_type == fwaas_const.FWG:
+ cmd_resource = fwaas_const.CMD_FWG
+ attrs['resource_id'] = client.find_resource(
+ resource_type,
+ parsed_args.resource,
+ cmd_resource=cmd_resource)['id']
+
+ if parsed_args.target:
+ # NOTE(yushiro) Currently, we're supporting only port
+ attrs['target_id'] = client.find_resource(
+ 'port', parsed_args.target)['id']
+ if parsed_args.event:
+ attrs['event'] = parsed_args.event
+ if parsed_args.enable:
+ attrs['enabled'] = True
+ if parsed_args.disable:
+ attrs['enabled'] = False
+ if parsed_args.name:
+ attrs['name'] = parsed_args.name
+ if parsed_args.description:
+ attrs['description'] = parsed_args.description
+ return attrs
+
+
+class CreateNetworkLog(command.ShowOne):
+ _description = _("Create a new network log")
+
+ def get_parser(self, prog_name):
+ parser = super(CreateNetworkLog, self).get_parser(prog_name)
+ _get_common_parser(parser)
+ osc_utils.add_project_owner_option_to_parser(parser)
+ parser.add_argument(
+ 'name',
+ metavar='',
+ help=_('Name for the network log'))
+ parser.add_argument(
+ '--event',
+ metavar='{ALL,ACCEPT,DROP}',
+ choices=['ALL', 'ACCEPT', 'DROP'],
+ type=nc_utils.convert_to_uppercase,
+ help=_('An event to store with log'))
+ # NOTE(yushiro) '--resource-type' is managed by following command:
+ # "openstack network loggable resources list". Therefore, this option
+ # shouldn't have "choices" like ['security_group', 'firewall_group']
+ parser.add_argument(
+ '--resource-type',
+ metavar='',
+ required=True,
+ type=nc_utils.convert_to_lowercase,
+ help=_('Network log type(s). '
+ 'You can see supported type(s) with following command:\n'
+ '$ openstack network loggable resources list'))
+ parser.add_argument(
+ '--resource',
+ metavar='',
+ help=_('Name or ID of resource (security group or firewall group) '
+ 'that used for logging. You can control for logging target '
+ 'combination with --target option.'))
+ parser.add_argument(
+ '--target',
+ metavar='',
+ help=_('Port (name or ID) for logging. You can control '
+ 'for logging target combination with --resource option.'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.neutronclient
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args)
+ obj = client.create_network_log({'log': attrs})['log']
+ columns, display_columns = column_util.get_columns(obj, _attr_map)
+ data = utils.get_dict_properties(obj, columns)
+ return (display_columns, data)
+
+
+class DeleteNetworkLog(command.Command):
+ _description = _("Delete network log(s)")
+
+ def get_parser(self, prog_name):
+ parser = super(DeleteNetworkLog, self).get_parser(prog_name)
+ parser.add_argument(
+ 'network_log',
+ metavar='',
+ nargs='+',
+ help=_('Network log(s) to delete (name or ID)'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.neutronclient
+ result = 0
+ for log_res in parsed_args.network_log:
+ try:
+ log_id = client.find_resource(
+ 'log', log_res, cmd_resource=NET_LOG)['id']
+ client.delete_network_log(log_id)
+ except Exception as e:
+ result += 1
+ LOG.error(_("Failed to delete network log with "
+ "name or ID '%(network_log)s': %(e)s"),
+ {'network_log': log_res, 'e': e})
+
+ if result > 0:
+ total = len(parsed_args.network_log)
+ msg = (_("%(result)s of %(total)s network log(s) "
+ "failed to delete") % {'result': result, 'total': total})
+ raise exceptions.CommandError(msg)
+
+
+class ListLoggableResource(command.Lister):
+ _description = _("List supported loggable resources")
+
+ def get_parser(self, prog_name):
+ parser = super(ListLoggableResource, self).get_parser(prog_name)
+ parser.add_argument(
+ '--long',
+ action='store_true',
+ help=_("List additional fields in output")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.neutronclient
+ obj = client.list_network_loggable_resources()['loggable_resources']
+ headers, columns = column_util.get_column_definitions(
+ _attr_map_for_loggable, long_listing=parsed_args.long)
+ return (headers, (utils.get_dict_properties(s, columns) for s in obj))
+
+
+class ListNetworkLog(command.Lister):
+ _description = _("List network logs")
+
+ def get_parser(self, prog_name):
+ parser = super(ListNetworkLog, self).get_parser(prog_name)
+ parser.add_argument(
+ '--long',
+ action='store_true',
+ help=_("List additional fields in output")
+ )
+ # TODO(yushiro): We'll support filtering in the future.
+ return parser
+
+ def _extend_list(self, data, parsed_args):
+ ext_data = copy.deepcopy(data)
+ for d in ext_data:
+ e_prefix = 'Event: '
+ if d['event']:
+ event = e_prefix + d['event'].upper()
+ port = '(port) ' + d['target_id'] if d['target_id'] else ''
+ resource_type = d['resource_type']
+ if d['resource_id']:
+ res = '(%s) %s' % (resource_type, d['resource_id'])
+ else:
+ res = ''
+ t_prefix = 'Logged: '
+ if port and res:
+ t = '%s on %s' % (res, port)
+ else:
+ # Either of res and port is empty, so concatenation works fine
+ t = res + port
+ target = t_prefix + t if t else t_prefix + '(None specified)'
+ d['summary'] = ',\n'.join([event, target])
+ return ext_data
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.neutronclient
+ obj = client.list_network_logs()['logs']
+ obj_extend = self._extend_list(obj, parsed_args)
+ headers, columns = column_util.get_column_definitions(
+ _attr_map, long_listing=parsed_args.long)
+ return (headers, (
+ utils.get_dict_properties(s, columns) for s in obj_extend))
+
+
+class SetNetworkLog(command.Command):
+ _description = _("Set network log properties")
+
+ def get_parser(self, prog_name):
+ parser = super(SetNetworkLog, self).get_parser(prog_name)
+ _get_common_parser(parser)
+ parser.add_argument(
+ 'network_log',
+ metavar='',
+ help=_('Network log to set (name or ID)'))
+ parser.add_argument(
+ '--name',
+ metavar='',
+ help=_('Name of the network log'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.neutronclient
+ log_id = client.find_resource(
+ 'log', parsed_args.network_log, cmd_resource=NET_LOG)['id']
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args,
+ is_create=False)
+ try:
+ client.update_network_log(log_id, {'log': attrs})
+ except Exception as e:
+ msg = (_("Failed to set network log '%(logging)s': %(e)s")
+ % {'logging': parsed_args.network_log, 'e': e})
+ raise exceptions.CommandError(msg)
+
+
+class ShowNetworkLog(command.ShowOne):
+ _description = _("Display network log details")
+
+ def get_parser(self, prog_name):
+ parser = super(ShowNetworkLog, self).get_parser(prog_name)
+ parser.add_argument(
+ 'network_log',
+ metavar='',
+ help=_('Network log to show (name or ID)'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.neutronclient
+ log_id = client.find_resource(
+ 'log', parsed_args.network_log, cmd_resource=NET_LOG)['id']
+ obj = client.show_network_log(log_id)['log']
+ columns, display_columns = column_util.get_columns(obj, _attr_map)
+ data = utils.get_dict_properties(obj, columns)
+ return (display_columns, data)
diff --git a/neutronclient/osc/v2/sfc/__init__.py b/neutronclient/osc/v2/sfc/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/neutronclient/osc/v2/sfc/sfc_flow_classifier.py b/neutronclient/osc/v2/sfc/sfc_flow_classifier.py
new file mode 100755
index 000000000..775730fc2
--- /dev/null
+++ b/neutronclient/osc/v2/sfc/sfc_flow_classifier.py
@@ -0,0 +1,346 @@
+# Copyright (c) 2017 Huawei Technologies India Pvt.Limited.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import argparse
+import logging
+
+from osc_lib.command import command
+from osc_lib import exceptions
+from osc_lib import utils
+from osc_lib.utils import columns as column_util
+
+from neutronclient._i18n import _
+from neutronclient.common import exceptions as nc_exc
+
+LOG = logging.getLogger(__name__)
+
+resource = 'flow_classifier'
+
+_attr_map = (
+ ('id', 'ID', column_util.LIST_BOTH),
+ ('name', 'Name', column_util.LIST_BOTH),
+ ('summary', 'Summary', column_util.LIST_SHORT_ONLY),
+ ('protocol', 'Protocol', column_util.LIST_LONG_ONLY),
+ ('ethertype', 'Ethertype', column_util.LIST_LONG_ONLY),
+ ('source_ip_prefix', 'Source IP',
+ column_util.LIST_LONG_ONLY),
+ ('destination_ip_prefix', 'Destination IP',
+ column_util.LIST_LONG_ONLY),
+ ('logical_source_port', 'Logical Source Port',
+ column_util.LIST_LONG_ONLY),
+ ('logical_destination_port', 'Logical Destination Port',
+ column_util.LIST_LONG_ONLY),
+ ('source_port_range_min', 'Source Port Range Min',
+ column_util.LIST_LONG_ONLY),
+ ('source_port_range_max', 'Source Port Range Max',
+ column_util.LIST_LONG_ONLY),
+ ('destination_port_range_min', 'Destination Port Range Min',
+ column_util.LIST_LONG_ONLY),
+ ('destination_port_range_max', 'Destination Port Range Max',
+ column_util.LIST_LONG_ONLY),
+ ('l7_parameters', 'L7 Parameters', column_util.LIST_LONG_ONLY),
+ ('description', 'Description', column_util.LIST_LONG_ONLY),
+ ('project_id', 'Project', column_util.LIST_LONG_ONLY),
+)
+
+_attr_map_dict = {
+ 'id': 'ID',
+ 'name': 'Name',
+ 'description': 'Description',
+ 'summary': 'Summary',
+ 'protocol': 'Protocol',
+ 'ethertype': 'Ethertype',
+ 'source_ip_prefix': 'Source IP',
+ 'destination_ip_prefix': 'Destination IP',
+ 'logical_source_port': 'Logical Source Port',
+ 'logical_destination_port': 'Logical Destination Port',
+ 'source_port_range_min': 'Source Port Range Min',
+ 'source_port_range_max': 'Source Port Range Max',
+ 'destination_port_range_min': 'Destination Port Range Min',
+ 'destination_port_range_max': 'Destination Port Range Max',
+ 'l7_parameters': 'L7 Parameters',
+ 'tenant_id': 'Project',
+ 'project_id': 'Project',
+}
+
+
+class CreateSfcFlowClassifier(command.ShowOne):
+ _description = _("Create a flow classifier")
+
+ def get_parser(self, prog_name):
+ parser = super(CreateSfcFlowClassifier, self).get_parser(prog_name)
+ parser.add_argument(
+ 'name',
+ metavar='',
+ help=_('Name of the flow classifier'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Description for the flow classifier'))
+ parser.add_argument(
+ '--protocol',
+ metavar='',
+ help=_('IP protocol name. Protocol name should be as per '
+ 'IANA standard.'))
+ parser.add_argument(
+ '--ethertype',
+ metavar='{IPv4,IPv6}',
+ default='IPv4', choices=['IPv4', 'IPv6'],
+ help=_('L2 ethertype, default is IPv4'))
+ parser.add_argument(
+ '--source-port',
+ metavar=':',
+ help=_('Source protocol port (allowed range [1,65535]. Must be '
+ 'specified as a:b, where a=min-port and b=max-port) '
+ 'in the allowed range.'))
+ parser.add_argument(
+ '--destination-port',
+ metavar=':',
+ help=_('Destination protocol port (allowed range [1,65535]. Must '
+ 'be specified as a:b, where a=min-port and b=max-port) '
+ 'in the allowed range.'))
+ parser.add_argument(
+ '--source-ip-prefix',
+ metavar='',
+ help=_('Source IP address in CIDR notation'))
+ parser.add_argument(
+ '--destination-ip-prefix',
+ metavar='',
+ help=_('Destination IP address in CIDR notation'))
+ parser.add_argument(
+ '--logical-source-port',
+ metavar='',
+ help=_('Neutron source port (name or ID)'))
+ parser.add_argument(
+ '--logical-destination-port',
+ metavar='',
+ help=_('Neutron destination port (name or ID)'))
+ parser.add_argument(
+ '--l7-parameters',
+ help=_('Dictionary of L7 parameters. Currently, no value is '
+ 'supported for this option.'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args)
+ obj = client.create_sfc_flow_classifier(**attrs)
+ display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
+ obj, _attr_map_dict, ['location', 'tenant_id', 'summary'])
+ data = utils.get_dict_properties(obj, columns)
+ return display_columns, data
+
+
+class DeleteSfcFlowClassifier(command.Command):
+ _description = _("Delete a given flow classifier")
+
+ def get_parser(self, prog_name):
+ parser = super(DeleteSfcFlowClassifier, self).get_parser(prog_name)
+ parser.add_argument(
+ 'flow_classifier',
+ metavar='',
+ nargs='+',
+ help=_("Flow classifier(s) to delete (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ result = 0
+ for fcl in parsed_args.flow_classifier:
+ try:
+ fc_id = client.find_sfc_flow_classifier(
+ fcl, ignore_missing=False)['id']
+ client.delete_sfc_flow_classifier(fc_id)
+ except Exception as e:
+ result += 1
+ LOG.error(_("Failed to delete flow classifier with name "
+ "or ID '%(fc)s': %(e)s"), {'fc': fcl, 'e': e})
+ if result > 0:
+ total = len(parsed_args.flow_classifier)
+ msg = (_("%(result)s of %(total)s flow classifier(s) "
+ "failed to delete.") % {'result': result, 'total': total})
+ raise exceptions.CommandError(msg)
+
+
+class ListSfcFlowClassifier(command.Lister):
+ _description = _("List flow classifiers")
+
+ def get_parser(self, prog_name):
+ parser = super(ListSfcFlowClassifier, self).get_parser(prog_name)
+ parser.add_argument(
+ '--long',
+ action='store_true',
+ help=_("List additional fields in output")
+ )
+ return parser
+
+ def extend_list(self, data, parsed_args):
+ ext_data = []
+ for d in data:
+ val = []
+ protocol = d['protocol'].upper() if d['protocol'] else 'any'
+ val.append('protocol: ' + protocol)
+ val.append(self._get_protocol_port_details(d, 'source'))
+ val.append(self._get_protocol_port_details(d, 'destination'))
+ if 'logical_source_port' in d:
+ val.append('neutron_source_port: ' +
+ str(d['logical_source_port']))
+
+ if 'logical_destination_port' in d:
+ val.append('neutron_destination_port: ' +
+ str(d['logical_destination_port']))
+
+ if 'l7_parameters' in d:
+ l7_param = 'l7_parameters: {%s}' % ','.join(d['l7_parameters'])
+ val.append(l7_param)
+ d['summary'] = ',\n'.join(val)
+ ext_data.append(d)
+ return ext_data
+
+ def _get_protocol_port_details(self, data, val):
+ type_ip_prefix = val + '_ip_prefix'
+ ip_prefix = data.get(type_ip_prefix)
+ if not ip_prefix:
+ ip_prefix = 'any'
+ min_port = data.get(val + '_port_range_min')
+ if min_port is None:
+ min_port = 'any'
+ max_port = data.get(val + '_port_range_max')
+ if max_port is None:
+ max_port = 'any'
+ return '%s[port]: %s[%s:%s]' % (
+ val, ip_prefix, min_port, max_port)
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ obj = client.sfc_flow_classifiers()
+ obj_extend = self.extend_list(obj, parsed_args)
+ headers, columns = column_util.get_column_definitions(
+ _attr_map, long_listing=parsed_args.long)
+ return (headers, (utils.get_dict_properties(
+ s, columns) for s in obj_extend))
+
+
+class SetSfcFlowClassifier(command.Command):
+ _description = _("Set flow classifier properties")
+
+ def get_parser(self, prog_name):
+ parser = super(SetSfcFlowClassifier, self).get_parser(prog_name)
+ parser.add_argument(
+ '--name',
+ metavar='',
+ help=_('Name of the flow classifier'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Description for the flow classifier'))
+ parser.add_argument(
+ 'flow_classifier',
+ metavar='',
+ help=_("Flow classifier to modify (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ fc_id = client.find_sfc_flow_classifier(parsed_args.flow_classifier,
+ ignore_missing=False)['id']
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args,
+ is_create=False)
+ try:
+ client.update_sfc_flow_classifier(fc_id, **attrs)
+ except Exception as e:
+ msg = (_("Failed to update flow classifier '%(fc)s': %(e)s")
+ % {'fc': parsed_args.flow_classifier, 'e': e})
+ raise exceptions.CommandError(msg)
+
+
+class ShowSfcFlowClassifier(command.ShowOne):
+ _description = _("Display flow classifier details")
+
+ def get_parser(self, prog_name):
+ parser = super(ShowSfcFlowClassifier, self).get_parser(prog_name)
+ parser.add_argument(
+ 'flow_classifier',
+ metavar='',
+ help=_("Flow classifier to display (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ fc_id = client.find_sfc_flow_classifier(parsed_args.flow_classifier,
+ ignore_missing=False)['id']
+ obj = client.get_sfc_flow_classifier(fc_id)
+ display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
+ obj, _attr_map_dict, ['location', 'tenant_id', 'summary'])
+ data = utils.get_dict_properties(obj, columns)
+ return display_columns, data
+
+
+def _get_common_attrs(client_manager, parsed_args, is_create=True):
+ attrs = {}
+ if parsed_args.name is not None:
+ attrs['name'] = parsed_args.name
+ if parsed_args.description is not None:
+ attrs['description'] = parsed_args.description
+ if is_create:
+ _get_attrs(client_manager, attrs, parsed_args)
+ return attrs
+
+
+def _get_attrs(client_manager, attrs, parsed_args):
+ if parsed_args.protocol is not None:
+ attrs['protocol'] = parsed_args.protocol
+ if parsed_args.ethertype:
+ attrs['ethertype'] = parsed_args.ethertype
+ if parsed_args.source_ip_prefix is not None:
+ attrs['source_ip_prefix'] = parsed_args.source_ip_prefix
+ if parsed_args.destination_ip_prefix is not None:
+ attrs['destination_ip_prefix'] = parsed_args.destination_ip_prefix
+ if parsed_args.logical_source_port is not None:
+ attrs['logical_source_port'] = client_manager.network.find_port(
+ parsed_args.logical_source_port, ignore_missing=False
+ )['id']
+ if parsed_args.logical_destination_port is not None:
+ attrs['logical_destination_port'] = client_manager.network.find_port(
+ parsed_args.logical_destination_port, ignore_missing=False
+ )['id']
+ if parsed_args.source_port is not None:
+ _fill_protocol_port_info(attrs, 'source',
+ parsed_args.source_port)
+ if parsed_args.destination_port is not None:
+ _fill_protocol_port_info(attrs, 'destination',
+ parsed_args.destination_port)
+ if parsed_args.l7_parameters is not None:
+ attrs['l7_parameters'] = parsed_args.l7_parameters
+
+
+def _fill_protocol_port_info(attrs, port_type, port_val):
+ min_port, sep, max_port = port_val.partition(":")
+ if not min_port:
+ msg = ("Invalid port value '%s', expected format is "
+ "min-port:max-port or min-port.")
+ raise argparse.ArgumentTypeError(msg % port_val)
+ if not max_port:
+ max_port = min_port
+ try:
+ attrs[port_type + '_port_range_min'] = int(min_port)
+ attrs[port_type + '_port_range_max'] = int(max_port)
+ except ValueError:
+ message = (_("Protocol port value %s must be an integer "
+ "or integer:integer.") % port_val)
+ raise nc_exc.CommandError(message=message)
diff --git a/neutronclient/osc/v2/sfc/sfc_port_chain.py b/neutronclient/osc/v2/sfc/sfc_port_chain.py
new file mode 100755
index 000000000..d96007b85
--- /dev/null
+++ b/neutronclient/osc/v2/sfc/sfc_port_chain.py
@@ -0,0 +1,381 @@
+# Copyright (c) 2017 Huawei Technologies India Pvt.Limited.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from osc_lib.cli import parseractions
+from osc_lib.command import command
+from osc_lib import exceptions
+from osc_lib import utils
+from osc_lib.utils import columns as column_util
+
+from neutronclient._i18n import _
+
+LOG = logging.getLogger(__name__)
+
+resource = 'port_chain'
+
+_attr_map = (
+ ('id', 'ID', column_util.LIST_BOTH),
+ ('name', 'Name', column_util.LIST_BOTH),
+ ('port_pair_groups', 'Port Pair Groups', column_util.LIST_BOTH),
+ ('flow_classifiers', 'Flow Classifiers',
+ column_util.LIST_BOTH),
+ ('chain_parameters', 'Chain Parameters',
+ column_util.LIST_BOTH),
+ ('description', 'Description', column_util.LIST_LONG_ONLY),
+ ('project_id', 'Project', column_util.LIST_LONG_ONLY),
+)
+
+_attr_map_dict = {
+ 'id': 'ID',
+ 'name': 'Name',
+ 'port_pair_groups': 'Port Pair Groups',
+ 'flow_classifiers': 'Flow Classifiers',
+ 'chain_parameters': 'Chain Parameters',
+ 'description': 'Description',
+ 'tenant_id': 'Project',
+ 'project_id': 'Project',
+}
+
+
+class CreateSfcPortChain(command.ShowOne):
+ _description = _("Create a port chain")
+
+ def get_parser(self, prog_name):
+ parser = super(CreateSfcPortChain, self).get_parser(prog_name)
+ parser.add_argument(
+ 'name',
+ metavar='',
+ help=_('Name of the port chain'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Description for the port chain'))
+ parser.add_argument(
+ '--flow-classifier',
+ default=[],
+ metavar='',
+ dest='flow_classifiers',
+ action='append',
+ help=_('Add flow classifier (name or ID). '
+ 'This option can be repeated.'))
+ parser.add_argument(
+ '--chain-parameters',
+ metavar='correlation=,symmetric=',
+ action=parseractions.MultiKeyValueAction,
+ optional_keys=['correlation', 'symmetric'],
+ help=_('Dictionary of chain parameters. Supports '
+ 'correlation=(mpls|nsh) (default is mpls) '
+ 'and symmetric=(true|false).'))
+ parser.add_argument(
+ '--port-pair-group',
+ metavar='',
+ dest='port_pair_groups',
+ required=True,
+ action='append',
+ help=_('Add port pair group (name or ID). '
+ 'This option can be repeated.'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args)
+ obj = client.create_sfc_port_chain(**attrs)
+ display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
+ obj, _attr_map_dict, ['location', 'tenant_id'])
+ data = utils.get_dict_properties(obj, columns)
+ return display_columns, data
+
+
+class DeleteSfcPortChain(command.Command):
+ _description = _("Delete a given port chain")
+
+ def get_parser(self, prog_name):
+ parser = super(DeleteSfcPortChain, self).get_parser(prog_name)
+ parser.add_argument(
+ 'port_chain',
+ metavar="",
+ nargs='+',
+ help=_("Port chain(s) to delete (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ result = 0
+ for pc in parsed_args.port_chain:
+ try:
+ pc_id = client.find_sfc_port_chain(
+ pc, ignore_missing=False)['id']
+ client.delete_sfc_port_chain(pc_id)
+ except Exception as e:
+ result += 1
+ LOG.error(_("Failed to delete port chain with name "
+ "or ID '%(pc)s': %(e)s"), {'pc': pc, 'e': e})
+ if result > 0:
+ total = len(parsed_args.port_chain)
+ msg = (_("%(result)s of %(total)s port chain(s) "
+ "failed to delete.") % {'result': result,
+ 'total': total})
+ raise exceptions.CommandError(msg)
+
+
+class ListSfcPortChain(command.Lister):
+ _description = _("List port chains")
+
+ def get_parser(self, prog_name):
+ parser = super(ListSfcPortChain, self).get_parser(prog_name)
+ parser.add_argument(
+ '--long',
+ action='store_true',
+ default=False,
+ help=_("List additional fields in output")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ data = client.sfc_port_chains()
+ headers, columns = column_util.get_column_definitions(
+ _attr_map, long_listing=parsed_args.long)
+ return (headers,
+ (utils.get_dict_properties(s, columns)
+ for s in data))
+
+
+class SetSfcPortChain(command.Command):
+ _description = _("Set port chain properties")
+
+ def get_parser(self, prog_name):
+ parser = super(SetSfcPortChain, self).get_parser(prog_name)
+ parser.add_argument(
+ '--name',
+ metavar='',
+ help=_('Name of the port chain'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Description for the port chain'))
+ parser.add_argument(
+ '--flow-classifier',
+ metavar='',
+ dest='flow_classifiers',
+ action='append',
+ help=_('Add flow classifier (name or ID). '
+ 'This option can be repeated.'))
+ parser.add_argument(
+ '--no-flow-classifier',
+ action='store_true',
+ help=_('Remove associated flow classifiers from the port chain'))
+ parser.add_argument(
+ '--port-pair-group',
+ metavar='',
+ dest='port_pair_groups',
+ action='append',
+ help=_('Add port pair group (name or ID). '
+ 'Current port pair groups order is kept, the added port '
+ 'pair group will be placed at the end of the port chain. '
+ 'This option can be repeated.'))
+ parser.add_argument(
+ '--no-port-pair-group',
+ action='store_true',
+ help=_('Remove associated port pair groups from the port chain. '
+ 'At least one --port-pair-group must be specified '
+ 'together.'))
+ parser.add_argument(
+ 'port_chain',
+ metavar='',
+ help=_("Port chain to modify (name or ID)"))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ pc_id = client.find_sfc_port_chain(parsed_args.port_chain,
+ ignore_missing=False)['id']
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args,
+ is_create=False)
+ if parsed_args.no_flow_classifier:
+ attrs['flow_classifiers'] = []
+ if parsed_args.flow_classifiers:
+ if parsed_args.no_flow_classifier:
+ fc_list = []
+ else:
+ fc_list = client.find_sfc_port_chain(
+ parsed_args.port_chain,
+ ignore_missing=False
+ )['flow_classifiers']
+ for fc in parsed_args.flow_classifiers:
+ fc_id = client.find_sfc_flow_classifier(
+ fc,
+ ignore_missing=False)['id']
+ if fc_id not in fc_list:
+ fc_list.append(fc_id)
+ attrs['flow_classifiers'] = fc_list
+ if (parsed_args.no_port_pair_group and not
+ parsed_args.port_pair_groups):
+ message = _('At least one --port-pair-group must be specified.')
+ raise exceptions.CommandError(message)
+ if parsed_args.no_port_pair_group and parsed_args.port_pair_groups:
+ ppg_list = []
+ for ppg in parsed_args.port_pair_groups:
+ ppg_id = client.find_sfc_port_pair_group(
+ ppg, ignore_missing=False)['id']
+ if ppg_id not in ppg_list:
+ ppg_list.append(ppg_id)
+ attrs['port_pair_groups'] = ppg_list
+ if (parsed_args.port_pair_groups and
+ not parsed_args.no_port_pair_group):
+ ppg_list = client.find_sfc_port_chain(
+ parsed_args.port_chain,
+ ignore_missing=False
+ )['port_pair_groups']
+ for ppg in parsed_args.port_pair_groups:
+ ppg_id = client.find_sfc_port_pair_group(
+ ppg, ignore_missing=False)['id']
+ if ppg_id not in ppg_list:
+ ppg_list.append(ppg_id)
+ attrs['port_pair_groups'] = ppg_list
+ try:
+ client.update_sfc_port_chain(pc_id, **attrs)
+ except Exception as e:
+ msg = (_("Failed to update port chain '%(pc)s': %(e)s")
+ % {'pc': parsed_args.port_chain, 'e': e})
+ raise exceptions.CommandError(msg)
+
+
+class ShowSfcPortChain(command.ShowOne):
+ _description = _("Display port chain details")
+
+ def get_parser(self, prog_name):
+ parser = super(ShowSfcPortChain, self).get_parser(prog_name)
+ parser.add_argument(
+ 'port_chain',
+ metavar="",
+ help=_("Port chain to display (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ pc_id = client.find_sfc_port_chain(parsed_args.port_chain,
+ ignore_missing=False)['id']
+ obj = client.get_sfc_port_chain(pc_id)
+ display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
+ obj, _attr_map_dict, ['location', 'tenant_id'])
+ data = utils.get_dict_properties(obj, columns)
+ return display_columns, data
+
+
+class UnsetSfcPortChain(command.Command):
+ _description = _("Unset port chain properties")
+
+ def get_parser(self, prog_name):
+ parser = super(UnsetSfcPortChain, self).get_parser(prog_name)
+ parser.add_argument(
+ 'port_chain',
+ metavar='',
+ help=_("Port chain to unset (name or ID)"))
+ port_chain = parser.add_mutually_exclusive_group()
+ port_chain.add_argument(
+ '--flow-classifier',
+ action='append',
+ metavar='',
+ dest='flow_classifiers',
+ help=_('Remove flow classifier(s) from the port chain '
+ '(name or ID). This option can be repeated.'))
+ port_chain.add_argument(
+ '--all-flow-classifier',
+ action='store_true',
+ help=_('Remove all flow classifiers from the port chain'))
+ parser.add_argument(
+ '--port-pair-group',
+ metavar='',
+ dest='port_pair_groups',
+ action='append',
+ help=_('Remove port pair group(s) from the port chain '
+ '(name or ID). This option can be repeated.'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ pc_id = client.find_sfc_port_chain(parsed_args.port_chain,
+ ignore_missing=False)['id']
+ attrs = {}
+ if parsed_args.flow_classifiers:
+ fc_list = client.find_sfc_port_chain(
+ parsed_args.port_chain, ignore_missing=False
+ )['flow_classifiers']
+ for fc in parsed_args.flow_classifiers:
+ fc_id = client.find_sfc_flow_classifier(
+ fc,
+ ignore_missing=False)['id']
+ if fc_id in fc_list:
+ fc_list.remove(fc_id)
+ attrs['flow_classifiers'] = fc_list
+ if parsed_args.all_flow_classifier:
+ attrs['flow_classifiers'] = []
+ if parsed_args.port_pair_groups:
+ ppg_list = client.find_sfc_port_chain(
+ parsed_args.port_chain,
+ ignore_missing=False)['port_pair_groups']
+ for ppg in parsed_args.port_pair_groups:
+ ppg_id = client.find_sfc_port_pair_group(
+ ppg,
+ ignore_missing=False)['id']
+ if ppg_id in ppg_list:
+ ppg_list.remove(ppg_id)
+ if ppg_list == []:
+ message = _('At least one port pair group must be'
+ ' specified.')
+ raise exceptions.CommandError(message)
+ attrs['port_pair_groups'] = ppg_list
+ try:
+ client.update_sfc_port_chain(pc_id, **attrs)
+ except Exception as e:
+ msg = (_("Failed to unset port chain '%(pc)s': %(e)s")
+ % {'pc': parsed_args.port_chain, 'e': e})
+ raise exceptions.CommandError(msg)
+
+
+def _get_common_attrs(client_manager, parsed_args, is_create=True):
+ attrs = {}
+ client = client_manager.network
+ if parsed_args.name is not None:
+ attrs['name'] = parsed_args.name
+ if parsed_args.description is not None:
+ attrs['description'] = parsed_args.description
+ if parsed_args.port_pair_groups:
+ attrs['port_pair_groups'] = [client.find_sfc_port_pair_group(
+ ppg, ignore_missing=False)['id']
+ for ppg in parsed_args.port_pair_groups]
+ if parsed_args.flow_classifiers:
+ attrs['flow_classifiers'] = [client.find_sfc_flow_classifier(
+ fc, ignore_missing=False)['id']
+ for fc in parsed_args.flow_classifiers]
+ if is_create is True:
+ _get_attrs(attrs, parsed_args)
+ return attrs
+
+
+def _get_attrs(attrs, parsed_args):
+ if parsed_args.chain_parameters is not None:
+ chain_params = {}
+ for chain_param in parsed_args.chain_parameters:
+ if 'correlation' in chain_param:
+ chain_params['correlation'] = chain_param['correlation']
+ if 'symmetric' in chain_param:
+ chain_params['symmetric'] = chain_param['symmetric']
+ attrs['chain_parameters'] = chain_params
diff --git a/neutronclient/osc/v2/sfc/sfc_port_pair.py b/neutronclient/osc/v2/sfc/sfc_port_pair.py
new file mode 100755
index 000000000..11e8e7631
--- /dev/null
+++ b/neutronclient/osc/v2/sfc/sfc_port_pair.py
@@ -0,0 +1,251 @@
+# Copyright (c) 2017 Huawei Technologies India Pvt.Limited.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from osc_lib.cli import parseractions
+from osc_lib.command import command
+from osc_lib import exceptions
+from osc_lib import utils
+from osc_lib.utils import columns as column_util
+
+from neutronclient._i18n import _
+
+LOG = logging.getLogger(__name__)
+
+resource = 'port_pair'
+
+_attr_map = (
+ ('id', 'ID', column_util.LIST_BOTH),
+ ('name', 'Name', column_util.LIST_BOTH),
+ ('ingress', 'Ingress Logical Port', column_util.LIST_BOTH),
+ ('egress', 'Egress Logical Port', column_util.LIST_BOTH),
+ ('service_function_parameters', 'Service Function Parameters',
+ column_util.LIST_LONG_ONLY),
+ ('description', 'Description', column_util.LIST_LONG_ONLY),
+ ('project_id', 'Project', column_util.LIST_LONG_ONLY),
+)
+
+_attr_map_dict = {
+ 'id': 'ID',
+ 'name': 'Name',
+ 'description': 'Description',
+ 'ingress': 'Ingress Logical Port',
+ 'egress': 'Egress Logical Port',
+ 'service_function_parameters': 'Service Function Parameters',
+ 'tenant_id': 'Project',
+ 'project_id': 'Project',
+}
+
+
+class CreateSfcPortPair(command.ShowOne):
+ _description = _("Create a port pair")
+
+ def get_parser(self, prog_name):
+ parser = super(CreateSfcPortPair, self).get_parser(prog_name)
+ parser.add_argument(
+ 'name',
+ metavar='',
+ help=_('Name of the port pair'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Description for the port pair'))
+ parser.add_argument(
+ '--service-function-parameters',
+ metavar='correlation=,weight=',
+ action=parseractions.MultiKeyValueAction,
+ optional_keys=['correlation', 'weight'],
+ help=_('Dictionary of service function parameters. '
+ 'Currently, correlation=(None|mpls|nsh) and weight '
+ 'are supported. Weight is an integer that influences '
+ 'the selection of a port pair within a port pair group '
+ 'for a flow. The higher the weight, the more flows will '
+ 'hash to the port pair. The default weight is 1.'))
+ parser.add_argument(
+ '--ingress',
+ metavar='',
+ required=True,
+ help=_('Ingress neutron port (name or ID)'))
+ parser.add_argument(
+ '--egress',
+ metavar='',
+ required=True,
+ help=_('Egress neutron port (name or ID)'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args)
+ obj = client.create_sfc_port_pair(**attrs)
+ display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
+ obj, _attr_map_dict, ['location', 'tenant_id'])
+ data = utils.get_dict_properties(obj, columns)
+ return display_columns, data
+
+
+class DeleteSfcPortPair(command.Command):
+ _description = _("Delete a given port pair")
+
+ def get_parser(self, prog_name):
+ parser = super(DeleteSfcPortPair, self).get_parser(prog_name)
+ parser.add_argument(
+ 'port_pair',
+ metavar="",
+ nargs='+',
+ help=_("Port pair(s) to delete (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ result = 0
+ for pp in parsed_args.port_pair:
+ try:
+ port_pair_id = client.find_sfc_port_pair(
+ pp, ignore_missing=False)['id']
+ client.delete_sfc_port_pair(port_pair_id)
+ except Exception as e:
+ result += 1
+ LOG.error(_("Failed to delete port pair with name "
+ "or ID '%(port_pair)s': %(e)s"),
+ {'port_pair': pp, 'e': e})
+ if result > 0:
+ total = len(parsed_args.port_pair)
+ msg = (_("%(result)s of %(total)s port pair(s) "
+ "failed to delete.") % {'result': result,
+ 'total': total})
+ raise exceptions.CommandError(msg)
+
+
+class ListSfcPortPair(command.Lister):
+ _description = _("List port pairs")
+
+ def get_parser(self, prog_name):
+ parser = super(ListSfcPortPair, self).get_parser(prog_name)
+ parser.add_argument(
+ '--long',
+ action='store_true',
+ help=_("List additional fields in output")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ data = client.sfc_port_pairs()
+ headers, columns = column_util.get_column_definitions(
+ _attr_map, long_listing=parsed_args.long)
+ return (headers,
+ (utils.get_dict_properties(
+ s, columns,
+ ) for s in data))
+
+
+class SetSfcPortPair(command.Command):
+ _description = _("Set port pair properties")
+
+ def get_parser(self, prog_name):
+ parser = super(SetSfcPortPair, self).get_parser(prog_name)
+ parser.add_argument(
+ '--name',
+ metavar='',
+ help=_('Name of the port pair'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Description for the port pair'))
+ parser.add_argument(
+ 'port_pair',
+ metavar='',
+ help=_("Port pair to modify (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ port_pair_id = client.find_sfc_port_pair(
+ parsed_args.port_pair, ignore_missing=False
+ )['id']
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args,
+ is_create=False)
+ try:
+ client.update_sfc_port_pair(port_pair_id, **attrs)
+ except Exception as e:
+ msg = (_("Failed to update port pair '%(port_pair)s': %(e)s")
+ % {'port_pair': parsed_args.port_pair, 'e': e})
+ raise exceptions.CommandError(msg)
+
+
+class ShowSfcPortPair(command.ShowOne):
+ _description = _("Display port pair details")
+
+ def get_parser(self, prog_name):
+ parser = super(ShowSfcPortPair, self).get_parser(prog_name)
+ parser.add_argument(
+ 'port_pair',
+ metavar='',
+ help=_("Port pair to display (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ port_pair_id = client.find_sfc_port_pair(
+ parsed_args.port_pair, ignore_missing=False
+ )['id']
+ obj = client.get_sfc_port_pair(port_pair_id)
+ display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
+ obj, _attr_map_dict, ['location', 'tenant_id'])
+ data = utils.get_dict_properties(obj, columns)
+ return display_columns, data
+
+
+def _get_common_attrs(client_manager, parsed_args, is_create=True):
+ attrs = {}
+ if parsed_args.name is not None:
+ attrs['name'] = parsed_args.name
+ if parsed_args.description is not None:
+ attrs['description'] = parsed_args.description
+ if is_create:
+ _get_attrs(client_manager, attrs, parsed_args)
+ return attrs
+
+
+def _get_attrs(client_manager, attrs, parsed_args):
+ client = client_manager.network
+ if parsed_args.ingress is not None:
+ attrs['ingress'] = client.find_port(
+ parsed_args.ingress, ignore_missing=False
+ )['id']
+ if parsed_args.egress is not None:
+ attrs['egress'] = client.find_port(
+ parsed_args.egress, ignore_missing=False
+ )['id']
+ if parsed_args.service_function_parameters is not None:
+ attrs['service_function_parameters'] = _get_service_function_params(
+ parsed_args.service_function_parameters)
+
+
+def _get_service_function_params(sf_params):
+ attrs = {}
+ for sf_param in sf_params:
+ if 'correlation' in sf_param:
+ if sf_param['correlation'] == 'None':
+ attrs['correlation'] = None
+ else:
+ attrs['correlation'] = sf_param['correlation']
+ if 'weight' in sf_param:
+ attrs['weight'] = sf_param['weight']
+ return attrs
diff --git a/neutronclient/osc/v2/sfc/sfc_port_pair_group.py b/neutronclient/osc/v2/sfc/sfc_port_pair_group.py
new file mode 100755
index 000000000..ec8859b42
--- /dev/null
+++ b/neutronclient/osc/v2/sfc/sfc_port_pair_group.py
@@ -0,0 +1,326 @@
+# Copyright (c) 2017 Huawei Technologies India Pvt.Limited.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from osc_lib.cli import parseractions
+from osc_lib.command import command
+from osc_lib import exceptions
+from osc_lib import utils
+from osc_lib.utils import columns as column_util
+
+from neutronclient._i18n import _
+
+LOG = logging.getLogger(__name__)
+
+resource = 'port_pair_group'
+
+_attr_map = (
+ ('id', 'ID', column_util.LIST_BOTH),
+ ('name', 'Name', column_util.LIST_BOTH),
+ ('port_pairs', 'Port Pair', column_util.LIST_BOTH),
+ ('port_pair_group_parameters', 'Port Pair Group Parameters',
+ column_util.LIST_BOTH),
+ ('description', 'Description', column_util.LIST_LONG_ONLY),
+ ('project_id', 'Project', column_util.LIST_LONG_ONLY),
+ ('is_tap_enabled', 'Tap Enabled', column_util.LIST_BOTH)
+)
+
+_attr_map_dict = {
+ 'id': 'ID',
+ 'name': 'Name',
+ 'description': 'Description',
+ 'port_pairs': 'Port Pair',
+ 'port_pair_group_parameters': 'Port Pair Group Parameters',
+ 'is_tap_enabled': 'Tap Enabled',
+ 'tenant_id': 'Project',
+ 'project_id': 'Project',
+}
+
+
+class CreateSfcPortPairGroup(command.ShowOne):
+ _description = _("Create a port pair group")
+
+ def get_parser(self, prog_name):
+ parser = super(CreateSfcPortPairGroup, self).get_parser(prog_name)
+ parser.add_argument(
+ 'name',
+ metavar='',
+ help=_('Name of the port pair group'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Description for the port pair group'))
+ parser.add_argument(
+ '--port-pair',
+ metavar='',
+ dest='port_pairs',
+ default=[],
+ action='append',
+ help=_('Port pair (name or ID). '
+ 'This option can be repeated.'))
+ tap_enable = parser.add_mutually_exclusive_group()
+ tap_enable.add_argument(
+ '--enable-tap',
+ action='store_true',
+ help=_('Port pairs of this port pair group are deployed as '
+ 'passive tap service function')
+ )
+ tap_enable.add_argument(
+ '--disable-tap',
+ action='store_true',
+ help=_('Port pairs of this port pair group are deployed as l3 '
+ 'service function (default)')
+ )
+ parser.add_argument(
+ '--port-pair-group-parameters',
+ metavar='lb-fields=',
+ action=parseractions.KeyValueAction,
+ help=_('Dictionary of port pair group parameters. '
+ 'Currently only one parameter lb-fields is supported. '
+ ' is a & separated list of load-balancing '
+ 'fields.'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args)
+ obj = client.create_sfc_port_pair_group(**attrs)
+ display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
+ obj, _attr_map_dict, ['location', 'tenant_id'])
+ data = utils.get_dict_properties(obj, columns)
+ return display_columns, data
+
+
+class DeleteSfcPortPairGroup(command.Command):
+ _description = _("Delete a given port pair group")
+
+ def get_parser(self, prog_name):
+ parser = super(DeleteSfcPortPairGroup, self).get_parser(prog_name)
+ parser.add_argument(
+ 'port_pair_group',
+ metavar='',
+ nargs='+',
+ help=_("Port pair group(s) to delete (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ result = 0
+ for ppg in parsed_args.port_pair_group:
+ try:
+ ppg_id = client.find_sfc_port_pair_group(
+ ppg, ignore_missing=False)['id']
+ client.delete_sfc_port_pair_group(ppg_id)
+ except Exception as e:
+ result += 1
+ LOG.error(_("Failed to delete port pair group with name "
+ "or ID '%(ppg)s': %(e)s"), {'ppg': ppg, 'e': e})
+ if result > 0:
+ total = len(parsed_args.port_pair_group)
+ msg = (_("%(result)s of %(total)s port pair group(s) "
+ "failed to delete.") % {'result': result,
+ 'total': total})
+ raise exceptions.CommandError(msg)
+
+
+class ListSfcPortPairGroup(command.Lister):
+ _description = _("List port pair group")
+
+ def get_parser(self, prog_name):
+ parser = super(ListSfcPortPairGroup, self).get_parser(prog_name)
+ parser.add_argument(
+ '--long',
+ action='store_true',
+ default=False,
+ help=_("List additional fields in output")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ data = client.sfc_port_pair_groups()
+ headers, columns = column_util.get_column_definitions(
+ _attr_map, long_listing=parsed_args.long)
+ return (headers,
+ (utils.get_dict_properties(
+ s, columns,
+ ) for s in data))
+
+
+class SetSfcPortPairGroup(command.Command):
+ _description = _("Set port pair group properties")
+
+ def get_parser(self, prog_name):
+ parser = super(SetSfcPortPairGroup, self).get_parser(prog_name)
+ parser.add_argument(
+ 'port_pair_group',
+ metavar='',
+ help=_("Port pair group to modify (name or ID)"))
+ parser.add_argument(
+ '--name',
+ metavar='',
+ help=_('Name of the port pair group'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Description for the port pair group'))
+ parser.add_argument(
+ '--port-pair',
+ metavar='',
+ dest='port_pairs',
+ default=[],
+ action='append',
+ help=_('Port pair (name or ID). '
+ 'This option can be repeated.'))
+ parser.add_argument(
+ '--no-port-pair',
+ action='store_true',
+ help=_('Remove all port pair from port pair group'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ ppg_id = client.find_sfc_port_pair_group(
+ parsed_args.port_pair_group)['id']
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args,
+ is_create=False)
+ if parsed_args.no_port_pair:
+ attrs['port_pairs'] = []
+ if parsed_args.port_pairs:
+ added = [client.find_sfc_port_pair(pp,
+ ignore_missing=False)['id']
+ for pp in parsed_args.port_pairs]
+ if parsed_args.no_port_pair:
+ existing = []
+ else:
+ existing = client.find_sfc_port_pair_group(
+ parsed_args.port_pair_group,
+ ignore_missing=False)['port_pairs']
+ attrs['port_pairs'] = sorted(list(set(existing) | set(added)))
+ try:
+ client.update_sfc_port_pair_group(ppg_id, **attrs)
+ except Exception as e:
+ msg = (_("Failed to update port pair group '%(ppg)s': %(e)s")
+ % {'ppg': parsed_args.port_pair_group, 'e': e})
+ raise exceptions.CommandError(msg)
+
+
+class ShowSfcPortPairGroup(command.ShowOne):
+ _description = _("Display port pair group details")
+
+ def get_parser(self, prog_name):
+ parser = super(ShowSfcPortPairGroup, self).get_parser(prog_name)
+ parser.add_argument(
+ 'port_pair_group',
+ metavar='',
+ help=_("Port pair group to display (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ ppg_id = client.find_sfc_port_pair_group(
+ parsed_args.port_pair_group, ignore_missing=False)['id']
+ obj = client.get_sfc_port_pair_group(ppg_id)
+ display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
+ obj, _attr_map_dict, ['location', 'tenant_id'])
+ data = utils.get_dict_properties(obj, columns)
+ return display_columns, data
+
+
+class UnsetSfcPortPairGroup(command.Command):
+ _description = _("Unset port pairs from port pair group")
+
+ def get_parser(self, prog_name):
+ parser = super(UnsetSfcPortPairGroup, self).get_parser(prog_name)
+ parser.add_argument(
+ 'port_pair_group',
+ metavar='',
+ help=_("Port pair group to unset (name or ID)"))
+ port_pair_group = parser.add_mutually_exclusive_group()
+ port_pair_group.add_argument(
+ '--port-pair',
+ action='append',
+ metavar='',
+ dest='port_pairs',
+ help=_('Remove port pair(s) from the port pair group '
+ '(name or ID). This option can be repeated.'))
+ port_pair_group.add_argument(
+ '--all-port-pair',
+ action='store_true',
+ help=_('Remove all port pairs from the port pair group'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ ppg_id = client.find_sfc_port_pair_group(
+ parsed_args.port_pair_group, ignore_missing=False)['id']
+ attrs = {}
+ if parsed_args.port_pairs:
+ existing = client.find_sfc_port_pair_group(
+ parsed_args.port_pair_group,
+ ignore_missing=False)['port_pairs']
+ removed = [client.find_sfc_port_pair(pp,
+ ignore_missing=False)['id']
+ for pp in parsed_args.port_pairs]
+ attrs['port_pairs'] = list(set(existing) - set(removed))
+ if parsed_args.all_port_pair:
+ attrs['port_pairs'] = []
+ try:
+ client.update_sfc_port_pair_group(ppg_id, **attrs)
+ except Exception as e:
+ msg = (_("Failed to unset port pair group '%(ppg)s': %(e)s")
+ % {'ppg': parsed_args.port_pair_group, 'e': e})
+ raise exceptions.CommandError(msg)
+
+
+def _get_ppg_param(attrs, ppg):
+ attrs['port_pair_group_parameters'] = {}
+ for key, value in ppg.items():
+ if key == 'lb-fields':
+ attrs['port_pair_group_parameters']['lb_fields'] = ([
+ field for field in value.split('&') if field])
+ else:
+ attrs['port_pair_group_parameters'][key] = value
+ return attrs['port_pair_group_parameters']
+
+
+def _get_common_attrs(client_manager, parsed_args, is_create=True):
+ client = client_manager.network
+ attrs = {}
+ if parsed_args.name is not None:
+ attrs['name'] = parsed_args.name
+ if parsed_args.description is not None:
+ attrs['description'] = parsed_args.description
+ if parsed_args.port_pairs:
+ attrs['port_pairs'] = [client.find_sfc_port_pair(
+ pp, ignore_missing=False)['id']
+ for pp in parsed_args.port_pairs]
+
+ if is_create:
+ _get_attrs(attrs, parsed_args)
+ return attrs
+
+
+def _get_attrs(attrs, parsed_args):
+ if parsed_args.port_pair_group_parameters is not None:
+ attrs['port_pair_group_parameters'] = (
+ _get_ppg_param(attrs, parsed_args.port_pair_group_parameters))
+ if parsed_args.enable_tap:
+ attrs['tap_enabled'] = True
+ if parsed_args.disable_tap:
+ attrs['tap_enabled'] = False
diff --git a/neutronclient/osc/v2/sfc/sfc_service_graph.py b/neutronclient/osc/v2/sfc/sfc_service_graph.py
new file mode 100644
index 000000000..2edf2ccd0
--- /dev/null
+++ b/neutronclient/osc/v2/sfc/sfc_service_graph.py
@@ -0,0 +1,272 @@
+# Copyright 2017 Intel Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from osc_lib.command import command
+from osc_lib import exceptions
+from osc_lib import utils
+from osc_lib.utils import columns as column_util
+
+from neutronclient._i18n import _
+
+LOG = logging.getLogger(__name__)
+
+resource = 'service_graph'
+
+_attr_map = (
+ ('id', 'ID', column_util.LIST_BOTH),
+ ('name', 'Name', column_util.LIST_BOTH),
+ ('port_chains', 'Branching Points', column_util.LIST_BOTH),
+ ('description', 'Description', column_util.LIST_LONG_ONLY),
+ ('project_id', 'Project', column_util.LIST_LONG_ONLY),
+)
+
+_attr_map_dict = {
+ 'id': 'ID',
+ 'name': 'Name',
+ 'description': 'Description',
+ 'port_chains': 'Branching Points',
+ 'tenant_id': 'Project',
+ 'project_id': 'Project',
+}
+
+
+class CreateSfcServiceGraph(command.ShowOne):
+ """Create a service graph."""
+ def get_parser(self, prog_name):
+ parser = super(CreateSfcServiceGraph, self).get_parser(prog_name)
+ parser.add_argument(
+ 'name',
+ metavar='',
+ help=_('Name of the service graph.'))
+ parser.add_argument(
+ '--description',
+ help=_('Description for the service graph.'))
+ parser.add_argument(
+ '--branching-point',
+ metavar='SRC_CHAIN:DST_CHAIN_1,DST_CHAIN_2,DST_CHAIN_N',
+ dest='branching_points',
+ action='append',
+ default=[], required=True,
+ help=_('Service graph branching point: the key is the source '
+ 'Port Chain while the value is a list of destination '
+ 'Port Chains. This option can be repeated.'))
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args)
+ try:
+ obj = client.create_sfc_service_graph(**attrs)
+ display_columns, columns = \
+ utils.get_osc_show_columns_for_sdk_resource(
+ obj, _attr_map_dict, ['location', 'tenant_id'])
+ data = utils.get_dict_properties(obj, columns)
+ return display_columns, data
+ except Exception as e:
+ msg = (_("Failed to create service graph using '%(pcs)s': %(e)s")
+ % {'pcs': parsed_args.branching_points, 'e': e})
+ raise exceptions.CommandError(msg)
+
+
+class SetSfcServiceGraph(command.Command):
+ _description = _("Set service graph properties")
+
+ def get_parser(self, prog_name):
+ parser = super(SetSfcServiceGraph, self).get_parser(prog_name)
+ parser.add_argument(
+ '--name',
+ metavar='',
+ help=_('Name of the service graph'))
+ parser.add_argument(
+ '--description',
+ metavar='',
+ help=_('Description for the service graph'))
+ parser.add_argument(
+ 'service_graph',
+ metavar='',
+ help=_("Service graph to modify (name or ID)")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ service_graph_id = client.find_sfc_service_graph(
+ parsed_args.service_graph, ignore_missing=False)['id']
+ attrs = _get_common_attrs(self.app.client_manager, parsed_args,
+ is_create=False)
+ try:
+ client.update_sfc_service_graph(service_graph_id, **attrs)
+ except Exception as e:
+ msg = (_("Failed to update service graph "
+ "'%(service_graph)s': %(e)s")
+ % {'service_graph': parsed_args.service_graph, 'e': e})
+ raise exceptions.CommandError(msg)
+
+
+class DeleteSfcServiceGraph(command.Command):
+ """Delete a given service graph."""
+
+ def get_parser(self, prog_name):
+ parser = super(DeleteSfcServiceGraph, self).get_parser(prog_name)
+ parser.add_argument(
+ 'service_graph',
+ metavar="",
+ nargs='+',
+ help=_("ID or name of the service graph(s) to delete.")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ result = 0
+ for sg in parsed_args.service_graph:
+ try:
+ sg_id = client.find_sfc_service_graph(
+ sg, ignore_missing=False)['id']
+ client.delete_sfc_service_graph(sg_id)
+ except Exception as e:
+ result += 1
+ LOG.error(_("Failed to delete service graph with name "
+ "or ID '%(sg)s': %(e)s"),
+ {'sg': sg, 'e': e})
+ if result > 0:
+ total = len(parsed_args.service_graph)
+ msg = (_("%(result)s of %(total)s service graph(s) "
+ "failed to delete.") % {'result': result,
+ 'total': total})
+ raise exceptions.CommandError(msg)
+
+
+class ListSfcServiceGraph(command.Lister):
+ _description = _("List service graphs")
+
+ def get_parser(self, prog_name):
+ parser = super(ListSfcServiceGraph, self).get_parser(prog_name)
+ parser.add_argument(
+ '--long',
+ action='store_true',
+ default=False,
+ help=_("List additional fields in output")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ data = client.sfc_service_graphs()
+ headers, columns = column_util.get_column_definitions(
+ _attr_map, long_listing=parsed_args.long)
+ return (headers,
+ (utils.get_dict_properties(s, columns)
+ for s in data))
+
+
+class ShowSfcServiceGraph(command.ShowOne):
+ """Show information of a given service graph."""
+
+ def get_parser(self, prog_name):
+ parser = super(ShowSfcServiceGraph, self).get_parser(prog_name)
+ parser.add_argument(
+ 'service_graph',
+ metavar="",
+ help=_("ID or name of the service graph to display.")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.network
+ sg_id = client.find_sfc_service_graph(parsed_args.service_graph,
+ ignore_missing=False)['id']
+ obj = client.get_sfc_service_graph(sg_id)
+ display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
+ obj, _attr_map_dict, ['location', 'tenant_id'])
+ data = utils.get_dict_properties(obj, columns)
+ return display_columns, data
+
+
+def _get_common_attrs(client_manager, parsed_args, is_create=True):
+ attrs = {}
+ if parsed_args.name is not None:
+ attrs['name'] = str(parsed_args.name)
+ if parsed_args.description is not None:
+ attrs['description'] = str(parsed_args.description)
+ if is_create:
+ _get_attrs_for_create(client_manager, attrs, parsed_args)
+ return attrs
+
+
+def _validate_destination_chains(comma_split, attrs, client, sc_):
+ for e in comma_split:
+ if e != "":
+ dc_ = client.find_sfc_port_chain(e, ignore_missing=False)['id']
+ attrs['port_chains'][sc_].append(dc_)
+ if _check_cycle(attrs['port_chains'], sc_, dc_):
+ raise exceptions.CommandError(
+ "Error: Service graph contains a cycle")
+ else:
+ raise exceptions.CommandError(
+ "Error: you must specify at least one "
+ "destination chain for each source chain")
+ return attrs
+
+
+def _check_cycle(graph, new_src, new_dest):
+ for src in graph:
+ if src == new_dest:
+ if _visit(graph, src, new_dest, new_src):
+ return True
+ return False
+
+
+def _visit(graph, src, new_dest, new_src):
+ if src in graph:
+ found_cycle = False
+ for dest in graph[src]:
+ if new_src == dest or found_cycle:
+ return True
+ else:
+ found_cycle = _visit(graph, dest, new_dest, new_src)
+ return False
+
+
+def _get_attrs_for_create(client_manager, attrs, parsed_args):
+ client = client_manager.network
+ if parsed_args.branching_points:
+ attrs['port_chains'] = {}
+ src_chain = None
+ for c in parsed_args.branching_points:
+ if ':' not in c:
+ raise exceptions.CommandError(
+ "Error: You must specify at least one "
+ "destination chain for each source chain.")
+ colon_split = c.split(':')
+ src_chain = colon_split.pop(0)
+ sc_ = client.find_sfc_port_chain(src_chain,
+ ignore_missing=False)['id']
+ for i in colon_split:
+ comma_split = i.split(',')
+ unique = set(comma_split)
+ if len(unique) != len(comma_split):
+ raise exceptions.CommandError(
+ "Error: Duplicate "
+ "destination chains from "
+ "source chain {}".format(src_chain))
+ if sc_ in attrs['port_chains']:
+ raise exceptions.CommandError(
+ "Error: Source chain {} is in "
+ "use already ".format(src_chain))
+ attrs['port_chains'][sc_] = []
+ _validate_destination_chains(
+ comma_split, attrs, client, sc_)
diff --git a/neutronclient/osc/v2/subnet_onboard/__init__.py b/neutronclient/osc/v2/subnet_onboard/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/neutronclient/osc/v2/subnet_onboard/subnet_onboard.py b/neutronclient/osc/v2/subnet_onboard/subnet_onboard.py
new file mode 100644
index 000000000..10c6ab4c2
--- /dev/null
+++ b/neutronclient/osc/v2/subnet_onboard/subnet_onboard.py
@@ -0,0 +1,59 @@
+# Copyright (c) 2019 SUSE Linux Products GmbH
+# All Rights Reserved
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+"""Subnet onboard action implementation"""
+import logging
+
+from osc_lib.command import command
+from osc_lib import exceptions
+
+from neutronclient._i18n import _
+
+LOG = logging.getLogger(__name__)
+
+
+class NetworkOnboardSubnets(command.Command):
+ """Onboard network subnets into a subnet pool"""
+
+ def get_parser(self, prog_name):
+ parser = super(NetworkOnboardSubnets, self).get_parser(prog_name)
+ parser.add_argument(
+ 'network',
+ metavar="",
+ help=_("Onboard all subnets associated with this network")
+ )
+ parser.add_argument(
+ 'subnetpool',
+ metavar="",
+ help=_("Target subnet pool for onboarding subnets")
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.neutronclient
+ subnetpool_id = _get_id(client, parsed_args.subnetpool, 'subnetpool')
+ network_id = _get_id(client, parsed_args.network, 'network')
+ body = {'network_id': network_id}
+ try:
+ client.onboard_network_subnets(subnetpool_id, body)
+ except Exception as e:
+ msg = (_("Failed to onboard subnets for network '%(n)s': %(e)s")
+ % {'n': parsed_args.network, 'e': e})
+ raise exceptions.CommandError(msg)
+
+
+def _get_id(client, id_or_name, resource):
+ return client.find_resource(resource, str(id_or_name))['id']
diff --git a/quantumclient/__init__.py b/neutronclient/osc/v2/utils.py
similarity index 65%
rename from quantumclient/__init__.py
rename to neutronclient/osc/v2/utils.py
index 5558fdb97..b03e29c3d 100644
--- a/quantumclient/__init__.py
+++ b/neutronclient/osc/v2/utils.py
@@ -1,7 +1,4 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2011 Citrix Systems
-# 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
@@ -14,10 +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: Tyler Smith, Cisco Systems
-import gettext
+"""This module is intended to contain methods specific
+to Networking v2 API and its extensions.
+"""
+
+from cliff import columns as cliff_columns
-# gettext must be initialized before any quantumclient imports
-gettext.install('quantumclient', unicode=1)
+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/common/__init__.py b/neutronclient/version.py
similarity index 72%
rename from quantumclient/common/__init__.py
rename to neutronclient/version.py
index 7e695ff08..d597b5c91 100644
--- a/quantumclient/common/__init__.py
+++ b/neutronclient/version.py
@@ -1,6 +1,5 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-# Copyright 2011 Nicira Networks, Inc.
-# All Rights Reserved.
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@@ -13,4 +12,10 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
-# @author: Somik Behera, Nicira Networks, Inc.
+#
+
+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 35d48d06e..000000000
--- a/openstack-common.conf
+++ /dev/null
@@ -1,7 +0,0 @@
-[DEFAULT]
-
-# The list of modules to copy from openstack-common
-modules=setup
-
-# The base module to hold the copy of openstack.common
-base=quantumclient
diff --git a/quantumclient/client.py b/quantumclient/client.py
deleted file mode 100644
index 03c590a21..000000000
--- a/quantumclient/client.py
+++ /dev/null
@@ -1,212 +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.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']
-
- resp, body = self.request(*args, **kargs)
-
- utils.http_log(_logger, args, kargs, 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:
- self.authenticate()
- resp, body = self._cs_request(
- self.management_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')
- 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 1c1f1acd0..000000000
--- a/quantumclient/common/clientmanager.py
+++ /dev/null
@@ -1,81 +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.common import exceptions as exc
-
-from quantumclient.quantum import client as quantum_client
-from quantumclient.client import HTTPClient
-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 734a49894..000000000
--- a/quantumclient/common/exceptions.py
+++ /dev/null
@@ -1,156 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2011 Nicira Networks, Inc
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""
-Quantum base exception handling.
-"""
-
-
-class QuantumException(Exception):
- """Base Quantum Exception
-
- Taken from nova.exception.NovaException
- To correctly use this class, inherit from it and define
- a 'message' property. That message will get printf'd
- with the keyword arguments provided to the constructor.
-
- """
- message = _("An unknown exception occurred.")
-
- def __init__(self, **kwargs):
- try:
- self._error_string = self.message % kwargs
-
- except Exception:
- # at least get the core message out if something happened
- self._error_string = self.message
-
- def __str__(self):
- return self._error_string
-
-
-class NotFound(QuantumException):
- pass
-
-
-class QuantumClientException(QuantumException):
-
- def __init__(self, **kwargs):
- message = kwargs.get('message')
- 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.
- """
- pass
-
-
-class Forbidden(QuantumClientException):
- """
- HTTP 403 - Forbidden: your credentials don't give you access to this
- resource.
- """
- pass
-
-
-class EndpointNotFound(QuantumClientException):
- """Could not find Service or Region in Service Catalog."""
- pass
-
-
-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 6e914673a..000000000
--- a/quantumclient/common/serializer.py
+++ /dev/null
@@ -1,155 +0,0 @@
-from xml.dom import minidom
-
-from quantumclient.common import exceptions as exception
-from quantumclient.common import utils
-
-
-# NOTE(maru): this class is duplicated from quantum.wsgi
-class Serializer(object):
- """Serializes and deserializes dictionaries to certain MIME types."""
-
- def __init__(self, metadata=None, default_xmlns=None):
- """Create a serializer based on the given WSGI environment.
-
- 'metadata' is an optional dict mapping MIME types to information
- needed to serialize a dictionary to that type.
-
- """
- self.metadata = metadata or {}
- self.default_xmlns = default_xmlns
-
- def _get_serialize_handler(self, content_type):
- handlers = {
- 'application/json': self._to_json,
- 'application/xml': self._to_xml,
- }
-
- try:
- return handlers[content_type]
- except Exception:
- raise exception.InvalidContentType(content_type=content_type)
-
- def serialize(self, data, content_type):
- """Serialize a dictionary into the specified content type."""
- return self._get_serialize_handler(content_type)(data)
-
- def deserialize(self, datastring, content_type):
- """Deserialize a string to a dictionary.
-
- The string must be in the format of a supported MIME type.
-
- """
- try:
- return self.get_deserialize_handler(content_type)(datastring)
- except Exception:
- raise exception.MalformedResponseBody(
- reason="Unable to deserialize response body")
-
- def get_deserialize_handler(self, content_type):
- handlers = {
- 'application/json': self._from_json,
- 'application/xml': self._from_xml,
- }
-
- try:
- return handlers[content_type]
- except Exception:
- raise exception.InvalidContentType(content_type=content_type)
-
- def _from_json(self, datastring):
- return utils.loads(datastring)
-
- def _from_xml(self, datastring):
- xmldata = self.metadata.get('application/xml', {})
- plurals = set(xmldata.get('plurals', {}))
- node = minidom.parseString(datastring).childNodes[0]
- return {node.nodeName: self._from_xml_node(node, plurals)}
-
- def _from_xml_node(self, node, listnames):
- """Convert a minidom node to a simple Python type.
-
- listnames is a collection of names of XML nodes whose subnodes should
- be considered list items.
-
- """
- if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
- return node.childNodes[0].nodeValue
- elif node.nodeName in listnames:
- return [self._from_xml_node(n, listnames)
- for n in node.childNodes if n.nodeType != node.TEXT_NODE]
- else:
- result = dict()
- for attr in node.attributes.keys():
- result[attr] = node.attributes[attr].nodeValue
- for child in node.childNodes:
- if child.nodeType != node.TEXT_NODE:
- result[child.nodeName] = self._from_xml_node(child,
- listnames)
- return result
-
- def _to_json(self, data):
- return utils.dumps(data)
-
- def _to_xml(self, data):
- metadata = self.metadata.get('application/xml', {})
- # We expect data to contain a single key which is the XML root.
- root_key = data.keys()[0]
- doc = minidom.Document()
- node = self._to_xml_node(doc, metadata, root_key, data[root_key])
-
- xmlns = node.getAttribute('xmlns')
- if not xmlns and self.default_xmlns:
- node.setAttribute('xmlns', self.default_xmlns)
-
- return node.toprettyxml(indent='', newl='')
-
- def _to_xml_node(self, doc, metadata, nodename, data):
- """Recursive method to convert data members to XML nodes."""
- result = doc.createElement(nodename)
-
- # Set the xml namespace if one is specified
- # TODO(justinsb): We could also use prefixes on the keys
- xmlns = metadata.get('xmlns', None)
- if xmlns:
- result.setAttribute('xmlns', xmlns)
- if type(data) is list:
- collections = metadata.get('list_collections', {})
- if nodename in collections:
- metadata = collections[nodename]
- for item in data:
- node = doc.createElement(metadata['item_name'])
- node.setAttribute(metadata['item_key'], str(item))
- result.appendChild(node)
- return result
- singular = metadata.get('plurals', {}).get(nodename, None)
- if singular is None:
- if nodename.endswith('s'):
- singular = nodename[:-1]
- else:
- singular = 'item'
- for item in data:
- node = self._to_xml_node(doc, metadata, singular, item)
- result.appendChild(node)
- elif type(data) is dict:
- collections = metadata.get('dict_collections', {})
- if nodename in collections:
- metadata = collections[nodename]
- for k, v in data.items():
- node = doc.createElement(metadata['item_name'])
- node.setAttribute(metadata['item_key'], str(k))
- text = doc.createTextNode(str(v))
- node.appendChild(text)
- result.appendChild(node)
- return result
- attrs = metadata.get('attributes', {}).get(nodename, {})
- for k, v in data.items():
- if k in attrs:
- result.setAttribute(k, str(v))
- else:
- node = self._to_xml_node(doc, metadata, k, v)
- result.appendChild(node)
- else:
- # Type is atom.
- node = doc.createTextNode(str(data))
- result.appendChild(node)
- return result
diff --git a/quantumclient/common/utils.py b/quantumclient/common/utils.py
deleted file mode 100644
index bed02e010..000000000
--- a/quantumclient/common/utils.py
+++ /dev/null
@@ -1,170 +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(_logger, args, kwargs, resp, body):
- if not _logger.isEnabledFor(logging.DEBUG):
- return
-
- string_parts = ['curl -i']
- for element in args:
- if element in ('GET', 'POST'):
- 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)
-
- _logger.debug("REQ: %s\n" % "".join(string_parts))
- if 'body' in kwargs and kwargs['body']:
- _logger.debug("REQ BODY: %s\n" % (kwargs['body']))
- _logger.debug("RESP:%s\n", resp)
- _logger.debug("RESP BODY:%s\n", body)
diff --git a/quantumclient/openstack/common/setup.py b/quantumclient/openstack/common/setup.py
deleted file mode 100644
index caf06fa5b..000000000
--- a/quantumclient/openstack/common/setup.py
+++ /dev/null
@@ -1,342 +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):
- fp = open(mailmap, 'r')
- for l in fp:
- l = l.strip()
- if not l.startswith('#') and ' ' in l:
- canonical_email, alias = l.split(' ')
- 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):
- reqs_in = []
- for requirements_file in requirements_files:
- if os.path.exists(requirements_file):
- return open(requirements_file, 'r').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):
- 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 not milestonever:
- milestonever = ""
- post_version = _get_git_post_version()
- revno = post_version.split(".")[-1]
- return "%s~%s.%s%s" % (milestonever, datestamp, revno_prefix, revno)
-
-
-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"))
- else:
- tag_infos = tag_info.split("-")
- base_version = "-".join(tag_infos[:-2])
- revno = tag_infos[-2]
- return "%s.%s" % (base_version, revno)
-
-
-def write_git_changelog():
- """Write a changelog based on the 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("ChangeLog", "w") as changelog_file:
- changelog_file.write(canonicalize_emails(changelog, mailmap))
-
-
-def generate_authors():
- """Create AUTHORS file using git commits."""
- jenkins_email = 'jenkins@review.openstack.org'
- old_authors = 'AUTHORS.in'
- new_authors = '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 | "
- "grep -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())
-
-_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 know 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."""
- open(os.path.join(project, 'versioninfo'), 'w').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 based"""
- 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.split('~')[0]
- else:
- version = read_versioninfo(projectname)
- return version.split('~')[0]
-
-
-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/quantum/client.py b/quantumclient/quantum/client.py
deleted file mode 100644
index c7d7948d1..000000000
--- a/quantumclient/quantum/client.py
+++ /dev/null
@@ -1,51 +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 identity service 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])
diff --git a/quantumclient/quantum/v2_0/__init__.py b/quantumclient/quantum/v2_0/__init__.py
deleted file mode 100644
index 92dc4963c..000000000
--- a/quantumclient/quantum/v2_0/__init__.py
+++ /dev/null
@@ -1,423 +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
-
-
-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(
- '-F', '--fields',
- 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 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
- if values_specs and values_specs[0] == '--':
- del values_specs[0]
- _options = {}
- current_arg = None
- _values_specs = []
- _value_number = 0
- _list_flag = False
- current_item = None
- for _item in values_specs:
- 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 not None:
- _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
- else:
- raise exceptions.CommandError(
- "invalid values_specs %s" % ' '.join(values_specs))
- elif _item == 'list=true':
- _list_flag = True
- continue
- if not _item.startswith('--'):
- if not current_item or '=' in current_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'})
- _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 = {}
- for opt in _options.iterkeys():
- _opt = opt.split('--', 2)[1]
- _value = getattr(_args, _opt.replace('-', '_'))
- if _value is not None:
- result_dict.update({_opt: _value})
- return result_dict
-
-
-class QuantumCommand(command.OpenStackCommand):
- api = 'network'
- log = logging.getLogger(__name__ + '.QuantumCommand')
-
- 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', ], )
-
- return parser
-
-
-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'), )
- self.add_known_arguments(parser)
- add_extra_argument(parser, 'value_specs',
- 'new values for the %s' % self.resource)
- return parser
-
- def add_known_arguments(self, parser):
- pass
-
- def args2body(self, parsed_args):
- return {}
-
- 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
- body = self.args2body(parsed_args)
- _extra_values = parse_args_to_dict(parsed_args.value_specs)
- body[self.resource].update(_extra_values)
- obj_creator = getattr(quantum_client,
- "create_%s" % self.resource)
- data = obj_creator(body)
- # {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 = {'': ''}
- for k, v in info.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)
- info[k] = value
- elif v is None:
- info[k] = ''
- 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='%s_id' % self.resource,
- help='ID of %s to update' % self.resource)
- add_extra_argument(parser, 'value_specs',
- 'new values for the %s' % 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
- value_specs = parsed_args.value_specs
- if not value_specs:
- raise exceptions.CommandError(
- "Must specify new values to update %s" % self.resource)
- data = {self.resource: parse_args_to_dict(value_specs)}
- obj_updator = getattr(quantum_client,
- "update_%s" % self.resource)
- obj_updator(parsed_args.id, data)
- 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
-
- def get_parser(self, prog_name):
- parser = super(DeleteCommand, self).get_parser(prog_name)
- parser.add_argument(
- 'id', metavar='%s_id' % self.resource,
- help='ID of %s to delete' % 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)
- obj_deleter(parsed_args.id)
- print >>self.app.stdout, (_('Deleted %(resource)s: %(id)s')
- % {'id': parsed_args.id,
- 'resource': self.resource})
- return
-
-
-class ListCommand(QuantumCommand, lister.Lister):
- """List resourcs that belong to a given tenant
-
- """
-
- api = 'network'
- resource = None
- log = None
- _formatters = None
-
- def get_parser(self, prog_name):
- parser = super(ListCommand, self).get_parser(prog_name)
- add_show_list_common_argument(parser)
- add_extra_argument(parser, 'filter_specs', 'filters options')
- return parser
-
- def get_data(self, parsed_args):
- self.log.debug('get_data(%s)' % parsed_args)
- quantum_client = self.get_client()
- search_opts = parse_args_to_dict(parsed_args.filter_specs)
-
- self.log.debug('search options: %s', search_opts)
- quantum_client.format = parsed_args.request_format
- fields = parsed_args.fields
- extra_fields = search_opts.get('fields', [])
- if extra_fields:
- if isinstance(extra_fields, list):
- fields.extend(extra_fields)
- else:
- fields.append(extra_fields)
- if fields:
- search_opts.update({'fields': fields})
- if parsed_args.show_details:
- search_opts.update({'verbose': 'True'})
- 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, formatters=self._formatters, )
- for s in info), )
-
-
-class ShowCommand(QuantumCommand, show.ShowOne):
- """Show information of a given resource
-
- """
- HEX_ELEM = '[0-9A-Fa-f]'
- UUID_PATTERN = '-'.join([HEX_ELEM + '{8}', HEX_ELEM + '{4}',
- HEX_ELEM + '{4}', HEX_ELEM + '{4}',
- HEX_ELEM + '{12}'])
- api = 'network'
- resource = None
- log = None
-
- def get_parser(self, prog_name):
- parser = super(ShowCommand, self).get_parser(prog_name)
- add_show_list_common_argument(parser)
- parser.add_argument(
- 'id', metavar='%s_id' % self.resource,
- help='ID or name of %s to look up' % 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}
-
- data = None
- # Error message to be used in case both search by id and name are
- # unsuccessful (if list by name fails it does not return an error)
- not_found_message = "Unable to find resource:%s" % parsed_args.id
-
- # perform search by id only if we are passing a valid UUID
- match = re.match(self.UUID_PATTERN, parsed_args.id)
- if match:
- try:
- obj_shower = getattr(quantum_client,
- "show_%s" % self.resource)
- data = obj_shower(parsed_args.id, **params)
- except exceptions.QuantumClientException as ex:
- logging.debug("Show operation failed with code:%s",
- ex.status_code)
- not_found_message = ex.message
- if ex.status_code != 404:
- logging.exception("Unable to perform show operation")
- raise
-
- # If data is empty, then we got a 404. Try to interpret Id as a name
- if not data:
- logging.debug("Trying to interpret %s as a %s name",
- parsed_args.id,
- self.resource)
- # build search_opts for the name
- search_opts = parse_args_to_dict(["--name=%s" % parsed_args.id])
- search_opts.update(params)
- 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]
- if len(info) > 1:
- logging.info("Multiple occurrences found for: %s",
- parsed_args.id)
- _columns = ['id']
- # put all ids in a single string as formatter for show
- # command will print on record only
- id_string = "\n".join(utils.get_item_properties(
- s, _columns)[0] for s in info)
- return (_columns, (id_string, ), )
- elif len(info) == 0:
- #Nothing was found
- raise exceptions.QuantumClientException(
- message=not_found_message)
- else:
- data = {self.resource: info[0]}
- 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/network.py b/quantumclient/quantum/v2_0/network.py
deleted file mode 100644
index 61d004bbe..000000000
--- a/quantumclient/quantum/v2_0/network.py
+++ /dev/null
@@ -1,84 +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 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 UpdateCommand
-from quantumclient.quantum.v2_0 import ShowCommand
-
-
-def _format_subnets(network):
- try:
- return '\n'.join(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, }
-
-
-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(
- '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})
- 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/port.py b/quantumclient/quantum/v2_0/port.py
deleted file mode 100644
index 9d6a91a5b..000000000
--- a/quantumclient/quantum/v2_0/port.py
+++ /dev/null
@@ -1,106 +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 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_fixed_ips(port):
- try:
- return '\n'.join([utils.dumps(ip) for ip in port['fixed_ips']])
- except Exception:
- return ''
-
-
-class ListPort(ListCommand):
- """List networks that belong to a given tenant."""
-
- resource = 'port'
- log = logging.getLogger(__name__ + '.ListPort')
- _formatters = {'fixed_ips': _format_fixed_ips, }
-
-
-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(
- '--admin_state_down',
- default=True, action='store_false',
- help='set admin state up to false')
- parser.add_argument(
- '--mac_address',
- help='mac address of port')
- parser.add_argument(
- '--device_id',
- help='device id of this port')
- parser.add_argument(
- '--fixed_ip',
- action='append',
- help='desired Ip for this port: '
- 'subnet_id=,ip_address=, '
- 'can be repeated')
- parser.add_argument(
- 'network_id',
- help='Network id this port belongs to')
-
- def args2body(self, parsed_args):
- body = {'port': {'admin_state_up': parsed_args.admin_state_down,
- 'network_id': parsed_args.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})
- ips = []
- if parsed_args.fixed_ip:
- for ip_spec in parsed_args.fixed_ip:
- ips.append(utils.str2dict(ip_spec))
- if ips:
- body['port'].update({'fixed_ips': ips})
- 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')
diff --git a/quantumclient/quantum/v2_0/subnet.py b/quantumclient/quantum/v2_0/subnet.py
deleted file mode 100644
index a43ec2744..000000000
--- a/quantumclient/quantum/v2_0/subnet.py
+++ /dev/null
@@ -1,106 +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 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_allocation_pools(subnet):
- try:
- return '\n'.join([utils.dumps(pool) for pool in
- subnet['allocation_pools']])
- 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, }
-
-
-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('--ip_version', type=int,
- default=4, choices=[4, 6],
- help='IP version with default 4')
- parser.add_argument(
- '--gateway', metavar='gateway',
- help='gateway ip of this subnet')
- parser.add_argument(
- '--allocation_pool',
- action='append',
- help='Allocation pool IP addresses for this subnet: '
- 'start=,end= '
- 'can be repeated')
- parser.add_argument(
- 'network_id',
- help='Network id this subnet belongs to')
- parser.add_argument(
- 'cidr', metavar='cidr',
- help='cidr of subnet to create')
-
- def args2body(self, parsed_args):
- body = {'subnet': {'cidr': parsed_args.cidr,
- 'network_id': parsed_args.network_id,
- 'ip_version': parsed_args.ip_version, }, }
- 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})
- ips = []
- if parsed_args.allocation_pool:
- for ip_spec in parsed_args.allocation_pool:
- ips.append(utils.str2dict(ip_spec))
- if ips:
- body['subnet'].update({'allocation_pools': ips})
-
- 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 a4097580f..000000000
--- a/quantumclient/shell.py
+++ /dev/null
@@ -1,342 +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 itertools
-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
-
-
-gettext.install('quantum', unicode=1)
-VERSION = '2.0'
-QUANTUM_API_VERSION = '2.0'
-
-
-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-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'), }
-
-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):
- 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]
- app.stdout.write(' %-13s %s\n' % (name, one_liner))
- sys.exit(0)
-
-
-class QuantumShell(App):
-
- CONSOLE_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'), )
- for k, v in 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_url', metavar='',
- default=env('OS_AUTH_URL'),
- help='Authentication URL (Env: OS_AUTH_URL)')
-
- parser.add_argument(
- '--os_tenant_name', metavar='',
- default=env('OS_TENANT_NAME'),
- help='Authentication tenant name (Env: OS_TENANT_NAME)')
-
- parser.add_argument(
- '--os_username', metavar='',
- default=utils.env('OS_USERNAME'),
- help='Authentication username (Env: OS_USERNAME)')
-
- parser.add_argument(
- '--os_password', metavar='',
- default=utils.env('OS_PASSWORD'),
- help='Authentication password (Env: OS_PASSWORD)')
-
- parser.add_argument(
- '--os_region_name', metavar='',
- default=env('OS_REGION_NAME'),
- help='Authentication region name (Env: OS_REGION_NAME)')
-
- parser.add_argument(
- '--os_token', metavar='',
- default=env('OS_TOKEN'),
- help='Defaults to env[OS_TOKEN]')
-
- parser.add_argument(
- '--os_url', metavar='',
- default=env('OS_URL'),
- help='Defaults to env[OS_URL]')
-
- return parser
-
- def run(self, argv):
- """Equivalent to the main program for the application.
-
- :param argv: input arguments and options
- :paramtype argv: list of str
- """
- try:
- 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 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 itertools_compressdef(data, selectors):
- # patch 2.6 compress('ABCDEF', [1,0,1,0,1,1]) --> A C E F
- return (d for d, s in itertools.izip(data, selectors) if s)
-
-
-def main(argv=sys.argv[1:]):
- try:
- if not getattr(itertools, 'compress', None):
- setattr(itertools, 'compress', itertools_compressdef)
- 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/test_casual_args.py b/quantumclient/tests/unit/test_casual_args.py
deleted file mode 100644
index 4192ff81b..000000000
--- a/quantumclient/tests/unit/test_casual_args.py
+++ /dev/null
@@ -1,72 +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 unittest
-
-from quantumclient.common import exceptions
-from quantumclient.quantum import v2_0 as quantumV20
-
-
-class CLITestArgs(unittest.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_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_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 b58d65cac..000000000
--- a/quantumclient/tests/unit/test_cli20.py
+++ /dev/null
@@ -1,314 +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 unittest
-
-import mox
-from mox import ContainsKeyValue
-from mox import Comparator
-
-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
-
-
-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(unittest.TestCase):
-
- test_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
-
- def _url(self, path, query=None):
- _url_str = self.endurl + "/v" + API_VERSION + path + "." + FORMAT
- return query and _url_str + "?" + query or _url_str
-
- def setUp(self):
- """Prepare the test environment"""
- self.mox = mox.Mox()
- self.endurl = ENDURL
- self.client = Client(token=TOKEN, endpoint_url=self.endurl)
- self.fake_stdout = FakeStdout()
- sys.stdout = self.fake_stdout
-
- def tearDown(self):
- """Clear the test environment"""
- sys.stdout = sys.__stdout__
-
- def _test_create_resource(self, resource, cmd,
- name, myid, args,
- position_names, position_values, tenant_id=None,
- tags=None, admin_state_up=True):
- self.mox.StubOutWithMock(cmd, "get_client")
- self.mox.StubOutWithMock(self.client.httpclient, "request")
- cmd.get_client().MultipleTimes().AndReturn(self.client)
- if resource == 'subnet':
- 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})
- for i in xrange(len(position_names)):
- body[resource].update({position_names[i]: position_values[i]})
- ress = {resource:
- {'id': myid,
- 'name': name, }, }
- resstr = self.client.serialize(ress)
- # url method body
- path = getattr(self.client, resource + "s_path")
- self.client.httpclient.request(
- self._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)
- 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(myid in _str)
- self.assertTrue(name in _str)
-
- 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(
- self._url(path, query), 'GET',
- body=None,
- headers=ContainsKeyValue('X-Auth-Token',
- TOKEN)).AndReturn((MyResp(200), resstr))
- self.mox.ReplayAll()
- cmd_parser = cmd.get_parser("list_" + resources)
-
- 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('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(
- self._url(path % myid), '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)
-
- 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(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(
- self._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)
-
- 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(myid in _str)
- self.assertTrue('myname' in _str)
-
- def _test_show_resource_by_name(self, resource, cmd, name,
- 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 = {"%ss" % resource:
- [{'id': 'some_id',
- 'name': name, }], }
- resstr = self.client.serialize(expected_res)
- list_path = getattr(self.client, resource + "s_path")
- self.client.httpclient.request(
- self._url(list_path, "%s&name=%s" % (query, name)), 'GET',
- body=None,
- headers=ContainsKeyValue('X-Auth-Token',
- TOKEN)).AndReturn((MyResp(200), resstr))
- self.mox.ReplayAll()
- cmd_parser = cmd.get_parser("show_" + 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(name in _str)
- self.assertTrue('some_id' 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(
- self._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)
-
- 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(myid in _str)
diff --git a/quantumclient/tests/unit/test_cli20_network.py b/quantumclient/tests/unit/test_cli20_network.py
deleted file mode 100644
index ad85dc652..000000000
--- a/quantumclient/tests/unit/test_cli20_network.py
+++ /dev/null
@@ -1,147 +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.common import exceptions
-from quantumclient.tests.unit.test_cli20 import CLITestV20Base
-from quantumclient.tests.unit.test_cli20 import MyApp
-from quantumclient.quantum.v2_0.network import CreateNetwork
-from quantumclient.quantum.v2_0.network import ListNetwork
-from quantumclient.quantum.v2_0.network import UpdateNetwork
-from quantumclient.quantum.v2_0.network import ShowNetwork
-from quantumclient.quantum.v2_0.network import DeleteNetwork
-
-
-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, ]
- _str = 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, ]
- _str = 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, ]
- _str = 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, ]
- _str = self._test_create_resource(resource, cmd, name, myid, args,
- position_names, position_values,
- admin_state_up=False)
-
- def test_list_nets_detail(self):
- """list nets: -D."""
- resources = "networks"
- cmd = ListNetwork(MyApp(sys.stdout), None)
- self._test_list_resources(resources, cmd, True)
-
- def test_list_nets_tags(self):
- """List nets: -- --tags a b."""
- resources = "networks"
- cmd = ListNetwork(MyApp(sys.stdout), None)
- self._test_list_resources(resources, 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_resources(resources, cmd, detail=True, tags=['a', 'b'])
-
- 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_resources(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_show_network_by_name(self):
- """Show net: --fields id --fields name myname."""
- resource = 'network'
- cmd = ShowNetwork(MyApp(sys.stdout), None)
- myname = 'myname'
- args = ['--fields', 'id', '--fields', 'name', myname]
- self._test_show_resource_by_name(resource, cmd, myname,
- 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_port.py b/quantumclient/tests/unit/test_cli20_port.py
deleted file mode 100644
index 676aaea8d..000000000
--- a/quantumclient/tests/unit/test_cli20_port.py
+++ /dev/null
@@ -1,146 +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.tests.unit.test_cli20 import CLITestV20Base
-from quantumclient.tests.unit.test_cli20 import MyApp
-from quantumclient.quantum.v2_0.port import CreatePort
-from quantumclient.quantum.v2_0.port import ListPort
-from quantumclient.quantum.v2_0.port import UpdatePort
-from quantumclient.quantum.v2_0.port import ShowPort
-from quantumclient.quantum.v2_0.port import DeletePort
-
-
-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])
- _str = 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']
- _str = 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])
- _str = 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])
- _str = self._test_create_resource(resource, cmd, name, myid, args,
- position_names, position_values,
- tags=['a', 'b'])
-
- 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_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_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_show_port_by_name(self):
- """Show port: --fields id --fields name myname."""
- resource = 'port'
- cmd = ShowPort(MyApp(sys.stdout), None)
- myname = 'myname'
- args = ['--fields', 'id', '--fields', 'name', myname]
- self._test_show_resource_by_name(resource, cmd, myname,
- 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_subnet.py b/quantumclient/tests/unit/test_cli20_subnet.py
deleted file mode 100644
index 8783ed0ad..000000000
--- a/quantumclient/tests/unit/test_cli20_subnet.py
+++ /dev/null
@@ -1,179 +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.tests.unit.test_cli20 import CLITestV20Base
-from quantumclient.tests.unit.test_cli20 import MyApp
-from quantumclient.quantum.v2_0.subnet import CreateSubnet
-from quantumclient.quantum.v2_0.subnet import ListSubnet
-from quantumclient.quantum.v2_0.subnet import UpdateSubnet
-from quantumclient.quantum.v2_0.subnet import ShowSubnet
-from quantumclient.quantum.v2_0.subnet import DeleteSubnet
-
-
-class CLITestV20Subnet(CLITestV20Base):
-
- def test_create_subnet(self):
- """Create sbunet: --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_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_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_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_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_show_subnet_by_name(self):
- """Show subnet: --fields id --fields name myname."""
- resource = 'subnet'
- cmd = ShowSubnet(MyApp(sys.stdout), None)
- myname = 'myname'
- args = ['--fields', 'id', '--fields', 'name', myname]
- self._test_show_resource_by_name(resource, cmd, myname,
- 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/v2_0/client.py b/quantumclient/v2_0/client.py
deleted file mode 100644
index b906237b0..000000000
--- a/quantumclient/v2_0/client.py
+++ /dev/null
@@ -1,400 +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 exceptions
-from quantumclient.common.serializer 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(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.forma = _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_nets()
- ...
-
- """
-
- #Metadata for deserializing xml
- _serialization_metadata = {
- "application/xml": {
- "attributes": {
- "network": ["id", "name"],
- "port": ["id", "mac_address"],
- "subnet": ["id", "prefix"]},
- "plurals": {
- "networks": "network",
- "ports": "port",
- "subnets": "subnet", }, }, }
-
- networks_path = "/networks"
- network_path = "/networks/%s"
- ports_path = "/ports"
- port_path = "/ports/%s"
- subnets_path = "/subnets"
- subnet_path = "/subnets/%s"
-
- @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))
-
- 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
- error_message = response_body
- _logger.debug("Error message: %s", error_message)
- # Add deserialized error message to exception arguments
- try:
- des_error_body = Serializer().deserialize(error_message,
- self.content_type())
- except:
- # If unable to deserialized body it is probably not a
- # Quantum error
- des_error_body = {'message': error_message}
- # Raise the appropriate exception
- exception_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:
- 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().serialize(data, self.content_type())
- else:
- raise Exception("unable to serialize object of type = '%s'" %
- type(data))
-
- def deserialize(self, data, status_code):
- """
- Deserializes a an xml or json string into a dictionary
- """
- if status_code == 204:
- return data
- return Serializer(self._serialization_metadata).deserialize(
- data, self.content_type())
-
- def content_type(self, format=None):
- """
- Returns the mime-type for either 'xml' or 'json'. Defaults to the
- currently set format
- """
- if not format:
- format = self.format
- return "application/%s" % (format)
-
- def retry_request(self, method, action, body=None,
- headers=None, params=None):
- """
- Call do_request with the default retry configuration. Only
- idempotent requests should retry failed connection attempts.
-
- :raises: ConnectionFailed if the maximum # of retries is exceeded
- """
- max_attempts = self.retries + 1
- for i in xrange(max_attempts):
- try:
- return self.do_request(method, action, body=body,
- headers=headers, params=params)
- except exceptions.ConnectionFailed:
- # Exception has already been logged by do_request()
- if i < self.retries:
- _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)
-
-#if __name__ == '__main__':
-#
-# client20 = Client(username='admin',
-# password='password',
-# auth_url='http://localhost:5000/v2.0',
-# tenant_name='admin')
-# client20 = Client(token='ec796583fcad4aa690b723bc0b25270e',
-# endpoint_url='http://localhost:9696')
-#
-# client20.tenant = 'default'
-# client20.format = 'json'
-# nets = client20.list_networks()
-# print nets
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 394b128fe..102a204d9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,12 +1,70 @@
-[build_sphinx]
-all_files = 1
-build-dir = doc/build
-source-dir = doc/source
-
-[nosetests]
-# NOTE(jkoelker) To run the test suite under nose install the following
-# coverage http://pypi.python.org/pypi/coverage
-# tissue http://pypi.python.org/pypi/tissue (pep8 checker)
-# openstack-nose https://github.com/jkoelker/openstack-nose
-verbosity=2
-detailed-errors=1
+[metadata]
+name = python-neutronclient
+summary = CLI and Client Library for OpenStack Networking
+description_file =
+ README.rst
+author = OpenStack Networking Project
+author_email = openstack-discuss@lists.openstack.org
+home_page = https://docs.openstack.org/python-neutronclient/latest/
+python_requires = >=3.9
+classifier =
+ Environment :: OpenStack
+ Intended Audience :: Developers
+ Intended Audience :: Information Technology
+ Intended Audience :: System Administrators
+ License :: OSI Approved :: Apache Software License
+ Operating System :: POSIX :: Linux
+ Programming Language :: Python
+ Programming Language :: Python :: Implementation :: CPython
+ Programming Language :: Python :: 3 :: Only
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
+ Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3.12
+
+[files]
+packages =
+ neutronclient
+
+[entry_points]
+openstack.cli.extension =
+ neutronclient = neutronclient.osc.plugin
+
+openstack.neutronclient.v2 =
+ sfc_flow_classifier_create = neutronclient.osc.v2.sfc.sfc_flow_classifier:CreateSfcFlowClassifier
+ sfc_flow_classifier_delete = neutronclient.osc.v2.sfc.sfc_flow_classifier:DeleteSfcFlowClassifier
+ sfc_flow_classifier_list = neutronclient.osc.v2.sfc.sfc_flow_classifier:ListSfcFlowClassifier
+ sfc_flow_classifier_set = neutronclient.osc.v2.sfc.sfc_flow_classifier:SetSfcFlowClassifier
+ sfc_flow_classifier_show = neutronclient.osc.v2.sfc.sfc_flow_classifier:ShowSfcFlowClassifier
+ sfc_port_chain_create = neutronclient.osc.v2.sfc.sfc_port_chain:CreateSfcPortChain
+ sfc_port_chain_delete = neutronclient.osc.v2.sfc.sfc_port_chain:DeleteSfcPortChain
+ sfc_port_chain_list = neutronclient.osc.v2.sfc.sfc_port_chain:ListSfcPortChain
+ sfc_port_chain_set = neutronclient.osc.v2.sfc.sfc_port_chain:SetSfcPortChain
+ sfc_port_chain_show = neutronclient.osc.v2.sfc.sfc_port_chain:ShowSfcPortChain
+ sfc_port_chain_unset = neutronclient.osc.v2.sfc.sfc_port_chain:UnsetSfcPortChain
+ sfc_port_pair_create = neutronclient.osc.v2.sfc.sfc_port_pair:CreateSfcPortPair
+ sfc_port_pair_delete = neutronclient.osc.v2.sfc.sfc_port_pair:DeleteSfcPortPair
+ sfc_port_pair_list = neutronclient.osc.v2.sfc.sfc_port_pair:ListSfcPortPair
+ sfc_port_pair_set = neutronclient.osc.v2.sfc.sfc_port_pair:SetSfcPortPair
+ sfc_port_pair_show = neutronclient.osc.v2.sfc.sfc_port_pair:ShowSfcPortPair
+ sfc_port_pair_group_create = neutronclient.osc.v2.sfc.sfc_port_pair_group:CreateSfcPortPairGroup
+ sfc_port_pair_group_delete = neutronclient.osc.v2.sfc.sfc_port_pair_group:DeleteSfcPortPairGroup
+ sfc_port_pair_group_list = neutronclient.osc.v2.sfc.sfc_port_pair_group:ListSfcPortPairGroup
+ sfc_port_pair_group_set = neutronclient.osc.v2.sfc.sfc_port_pair_group:SetSfcPortPairGroup
+ sfc_port_pair_group_show = neutronclient.osc.v2.sfc.sfc_port_pair_group:ShowSfcPortPairGroup
+ sfc_port_pair_group_unset = neutronclient.osc.v2.sfc.sfc_port_pair_group:UnsetSfcPortPairGroup
+ sfc_service_graph_create = neutronclient.osc.v2.sfc.sfc_service_graph:CreateSfcServiceGraph
+ sfc_service_graph_delete = neutronclient.osc.v2.sfc.sfc_service_graph:DeleteSfcServiceGraph
+ sfc_service_graph_set = neutronclient.osc.v2.sfc.sfc_service_graph:SetSfcServiceGraph
+ sfc_service_graph_list = neutronclient.osc.v2.sfc.sfc_service_graph:ListSfcServiceGraph
+ sfc_service_graph_show = neutronclient.osc.v2.sfc.sfc_service_graph:ShowSfcServiceGraph
+
+ 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 aa6a0c0e6..cd35c3c35 100644
--- a/setup.py
+++ b/setup.py
@@ -1,70 +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 = 'Netstack'
-AuthorEmail = 'netstack@lists.launchpad.net'
-Maintainer = ''
-Summary = 'Client functionalities for 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,
- scripts=ProjectScripts,
- dependency_links=dependency_links,
- 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 2a0be2812..000000000
--- a/tools/pip-requires
+++ /dev/null
@@ -1,7 +0,0 @@
-cliff>=0.6.0
-argparse
-httplib2
-prettytable>=0.6.0
-simplejson
-
-
diff --git a/tools/test-requires b/tools/test-requires
deleted file mode 100644
index 3c03917a6..000000000
--- a/tools/test-requires
+++ /dev/null
@@ -1,14 +0,0 @@
-distribute>=0.6.24
-cliff>=0.6.0
-argparse
-httplib2
-prettytable>=0.6.0
-simplejson
-mox
-nose
-nose-exclude
-nosexcover
-openstack.nose_plugin
-pep8
-sphinx>=1.1.2
-
diff --git a/tox.ini b/tox.ini
index fce1185cf..a939f8b65 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,25 +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}
- NOSE_WITH_OPENSTACK=1
- NOSE_OPENSTACK_COLOR=1
- NOSE_OPENSTACK_RED=0.05
- NOSE_OPENSTACK_YELLOW=0.025
- NOSE_OPENSTACK_SHOW_ELAPSED=1
- NOSE_OPENSTACK_STDOUT=1
-deps = -r{toxinidir}/tools/test-requires
-commands = nosetests {posargs}
-
-[tox:jenkins]
-downloadcache = ~/cache/pip
+ LANG=en_US.UTF-8
+ LANGUAGE=en_US:en
+ LC_ALL=C
+ PYTHONWARNINGS=default::DeprecationWarning
+usedevelop = True
+deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/test-requirements.txt
+# Delete bytecodes from normal directories before running tests.
+# Note that bytecodes in dot directories will not be deleted
+# to keep bytecodes of python modules installed into virtualenvs.
+commands = bash -c "find . -type d -name '.?*' -prune -o \
+ \( -type d -name '__pycache__' -o -type f -name '*.py[co]' \) \
+ -print0 | xargs -0 rm -rf"
+ stestr run {posargs}
+allowlist_externals = bash
[testenv:pep8]
-commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,doc .
-
-[testenv:cover]
-setenv = NOSE_WITH_COVERAGE=1
+commands =
+ flake8
+ {[testenv:bandit]commands}
+distribute = false
[testenv:venv]
commands = {posargs}
+
+[testenv:cover]
+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
+
+[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