Implement Networking sfc plugin for Murano
Python plugin for Murano provides low level API for Networking SFC functions in Neutron. Change-Id: Ifd64f0c8bd3b707e03cdd08b2c3cba59caa689c1
This commit is contained in:
parent
91ec1a5784
commit
bdb2a39be1
|
@ -0,0 +1,2 @@
|
||||||
|
Murano Plugin Networking SFC
|
||||||
|
============================
|
|
@ -0,0 +1,45 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = ['oslosphinx']
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'murano-plugin-networking-sfc'
|
||||||
|
copyright = u'2016, Mirantis, Inc'
|
||||||
|
author = u'Mirantis, Inc'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
version = u'1.0.0'
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = u'1.0.0'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This patterns also effect to html_static_path and html_extra_path
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
.. include:: ../../README.rst
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Copyright 2016 Mirantis, 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.
|
||||||
|
|
||||||
|
from murano_plugin_networking_sfc.client import NetworkingSfcClient # noqa
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Copyright 2016 Mirantis, 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.
|
||||||
|
|
||||||
|
from murano.common import auth_utils
|
||||||
|
from murano.dsl import session_local_storage
|
||||||
|
from neutronclient.v2_0 import client as n_client
|
||||||
|
from oslo_config import cfg
|
||||||
|
import six
|
||||||
|
|
||||||
|
from murano_plugin_networking_sfc import config
|
||||||
|
from murano_plugin_networking_sfc import resource
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class ClientMeta(type):
|
||||||
|
|
||||||
|
def __init__(cls, name, bases, attrs):
|
||||||
|
super(ClientMeta, cls).__init__(name, bases, attrs)
|
||||||
|
|
||||||
|
for resource_cls in attrs['resources']:
|
||||||
|
for action in resource_cls.allowed_actions:
|
||||||
|
if action in resource_cls.plural_actions:
|
||||||
|
name = resource_cls.plural_name
|
||||||
|
else:
|
||||||
|
name = resource_cls.name
|
||||||
|
name = '{0}_{1}'.format(action, name)
|
||||||
|
|
||||||
|
setattr(cls, name, ClientMeta.resource_wrapper(
|
||||||
|
resource_cls, action))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resource_wrapper(resource_cls, action):
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
obj = resource_cls(self.client)
|
||||||
|
return getattr(obj, action)(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(ClientMeta)
|
||||||
|
class NetworkingSfcClient(object):
|
||||||
|
|
||||||
|
resources = [
|
||||||
|
resource.PortChain,
|
||||||
|
resource.PortPair,
|
||||||
|
resource.PortPairGroup,
|
||||||
|
resource.FlowClassifier,
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, this):
|
||||||
|
self._owner = this.find_owner('io.murano.Environment')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_plugin(cls):
|
||||||
|
cls.CONF = config.init_config(CONF)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
region = None
|
||||||
|
if self._owner is not None:
|
||||||
|
region = self._owner['region']
|
||||||
|
return self._get_client(region)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@session_local_storage.execution_session_memoize
|
||||||
|
def _get_client(region):
|
||||||
|
params = auth_utils.get_session_client_parameters(
|
||||||
|
service_type='network', conf=CONF, region=region)
|
||||||
|
return n_client.Client(**params)
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Copyright 2016 Mirantis, 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.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
|
||||||
|
def init_config(conf):
|
||||||
|
opts = [
|
||||||
|
cfg.StrOpt('endpoint_type', default='publicURL')
|
||||||
|
]
|
||||||
|
conf.register_opts(opts, group="networking_sfc")
|
||||||
|
return conf.networking_sfc
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Copyright 2016 Mirantis, 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.
|
||||||
|
|
||||||
|
|
||||||
|
class APIError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(APIError):
|
||||||
|
pass
|
|
@ -0,0 +1,96 @@
|
||||||
|
# Copyright 2016 Mirantis, 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.
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
from neutronclient.common import exceptions as n_err
|
||||||
|
|
||||||
|
from murano_plugin_networking_sfc import error
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResourceWrapper(object):
|
||||||
|
|
||||||
|
allowed_actions = ['create', 'list', 'show', 'update', 'delete']
|
||||||
|
plural_actions = ['list']
|
||||||
|
|
||||||
|
@abc.abstractproperty
|
||||||
|
def name(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractproperty
|
||||||
|
def plural_name(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, client):
|
||||||
|
self._client = client
|
||||||
|
|
||||||
|
def _get_neutron_function(self, resource_name, action):
|
||||||
|
function_name = '{0}_{1}'.format(action, resource_name)
|
||||||
|
return getattr(self._client, function_name)
|
||||||
|
|
||||||
|
def _prepare_request(self, params):
|
||||||
|
return {self.name: params}
|
||||||
|
|
||||||
|
def create(self, **kwargs):
|
||||||
|
request = self._prepare_request(kwargs)
|
||||||
|
response = self._get_neutron_function(self.name, 'create')(request)
|
||||||
|
return response[self.name]
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
return self._get_neutron_function(self.plural_name, 'list')()
|
||||||
|
|
||||||
|
def show(self, id_):
|
||||||
|
try:
|
||||||
|
return self._get_neutron_function(self.name, 'show')(id_)
|
||||||
|
except n_err.NotFound as exc:
|
||||||
|
raise error.NotFound(exc.message)
|
||||||
|
|
||||||
|
def update(self, id_, **kwargs):
|
||||||
|
kwargs['id'] = id_
|
||||||
|
request = self._prepare_request(kwargs)
|
||||||
|
try:
|
||||||
|
response = self._get_neutron_function(self.name, 'update')(request)
|
||||||
|
except n_err.NotFound as exc:
|
||||||
|
raise error.NotFound(exc.message)
|
||||||
|
return response[self.name]
|
||||||
|
|
||||||
|
def delete(self, id_):
|
||||||
|
try:
|
||||||
|
return self._get_neutron_function(self.name, 'delete')(id_)
|
||||||
|
except n_err.NotFound as exc:
|
||||||
|
raise error.NotFound(exc.message)
|
||||||
|
|
||||||
|
|
||||||
|
class PortChain(BaseResourceWrapper):
|
||||||
|
|
||||||
|
name = 'port_chain'
|
||||||
|
plural_name = 'port_chains'
|
||||||
|
|
||||||
|
|
||||||
|
class PortPair(BaseResourceWrapper):
|
||||||
|
|
||||||
|
name = 'port_pair'
|
||||||
|
plural_name = 'port_pairs'
|
||||||
|
|
||||||
|
|
||||||
|
class PortPairGroup(BaseResourceWrapper):
|
||||||
|
|
||||||
|
name = 'port_pair_group'
|
||||||
|
plural_name = 'port_pair_groups'
|
||||||
|
|
||||||
|
|
||||||
|
class FlowClassifier(BaseResourceWrapper):
|
||||||
|
|
||||||
|
name = 'flow_classifier'
|
||||||
|
plural_name = 'flow_classifiers'
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Copyright 2016 Mirantis, 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from murano_plugin_networking_sfc import client
|
||||||
|
|
||||||
|
|
||||||
|
class TestNetworkingSfcClient(unittest.TestCase):
|
||||||
|
|
||||||
|
ALL_FUNCTIONS = [
|
||||||
|
'create_flow_classifier',
|
||||||
|
'delete_flow_classifier',
|
||||||
|
'list_flow_classifiers',
|
||||||
|
'show_flow_classifier',
|
||||||
|
'update_flow_classifier',
|
||||||
|
|
||||||
|
'create_port_chain',
|
||||||
|
'delete_port_chain',
|
||||||
|
'list_port_chains',
|
||||||
|
'show_port_chain',
|
||||||
|
'update_port_chain',
|
||||||
|
|
||||||
|
'create_port_pair',
|
||||||
|
'delete_port_pair',
|
||||||
|
'list_port_pairs',
|
||||||
|
'show_port_pair',
|
||||||
|
'update_port_pair',
|
||||||
|
|
||||||
|
'create_port_pair_group',
|
||||||
|
'delete_port_pair_group',
|
||||||
|
'list_port_pair_groups',
|
||||||
|
'show_port_pair_group',
|
||||||
|
'update_port_pair_group',
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
patcher = mock.patch.object(client.NetworkingSfcClient, 'client')
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
self.n_client = patcher.start()
|
||||||
|
|
||||||
|
self.client = client.NetworkingSfcClient(mock.MagicMock())
|
||||||
|
|
||||||
|
def test_client_function_call(self):
|
||||||
|
flow_id = 'flow-id'
|
||||||
|
port_pair_group_id = 'port-pair-group-id'
|
||||||
|
self.client.create_port_chain(flow_classifiers=[flow_id],
|
||||||
|
port_pair_groups=[port_pair_group_id])
|
||||||
|
self.n_client.create_port_chain.assert_called_once_with({
|
||||||
|
'port_chain': {
|
||||||
|
'flow_classifiers': [
|
||||||
|
flow_id,
|
||||||
|
],
|
||||||
|
'port_pair_groups': [
|
||||||
|
port_pair_group_id
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_client_function_call_unknown(self):
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
|
self.client.invalid_function()
|
||||||
|
|
||||||
|
def test_client_api(self):
|
||||||
|
for func_name in self.ALL_FUNCTIONS:
|
||||||
|
self.assertTrue(hasattr(client.NetworkingSfcClient, func_name))
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Copyright 2016 Mirantis, 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import unittest
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from neutronclient.common import exceptions as n_err
|
||||||
|
|
||||||
|
from murano_plugin_networking_sfc import error
|
||||||
|
from murano_plugin_networking_sfc import resource
|
||||||
|
|
||||||
|
|
||||||
|
class SampleResource(resource.BaseResourceWrapper):
|
||||||
|
|
||||||
|
name = 'sample'
|
||||||
|
plural_name = 'samples'
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseResourceWrapper(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.n_client = mock.MagicMock()
|
||||||
|
|
||||||
|
self.client = SampleResource(self.n_client)
|
||||||
|
|
||||||
|
def test_create_resource(self):
|
||||||
|
foo = str(uuid.uuid4())
|
||||||
|
bar = 16
|
||||||
|
expected_data = {'param_foo': foo, 'param_bar': bar}
|
||||||
|
self.n_client.create_sample.side_effect = lambda req: req
|
||||||
|
|
||||||
|
response = self.client.create(param_foo=foo, param_bar=bar)
|
||||||
|
self.n_client.create_sample.assert_called_once_with({
|
||||||
|
'sample': expected_data,
|
||||||
|
})
|
||||||
|
self.assertEqual(response, expected_data)
|
||||||
|
|
||||||
|
def test_delete_resource(self):
|
||||||
|
self.client.delete(32)
|
||||||
|
self.n_client.delete_sample.assert_called_once_with(32)
|
||||||
|
|
||||||
|
def test_delete_resource_not_found(self):
|
||||||
|
self.n_client.delete_sample.side_effect = n_err.NotFound()
|
||||||
|
with self.assertRaises(error.NotFound):
|
||||||
|
self.client.delete(32)
|
||||||
|
|
||||||
|
def test_list_resources(self):
|
||||||
|
self.client.list()
|
||||||
|
self.n_client.list_samples.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_show_resource(self):
|
||||||
|
self.client.show(64)
|
||||||
|
self.n_client.show_sample.assert_called_once_with(64)
|
||||||
|
|
||||||
|
def test_show_resource_not_found(self):
|
||||||
|
self.n_client.show_sample.side_effect = n_err.NotFound()
|
||||||
|
with self.assertRaises(error.NotFound):
|
||||||
|
self.client.show(64)
|
||||||
|
|
||||||
|
def test_update_resource(self):
|
||||||
|
foo = str(uuid.uuid4())
|
||||||
|
bar = 16
|
||||||
|
expected_data = {'id': 8, 'param_foo': foo, 'param_bar': bar}
|
||||||
|
self.n_client.update_sample.side_effect = lambda req: req
|
||||||
|
|
||||||
|
response = self.client.update(8, param_foo=foo, param_bar=bar)
|
||||||
|
self.n_client.update_sample.assert_called_once_with({
|
||||||
|
'sample': expected_data,
|
||||||
|
})
|
||||||
|
self.assertEqual(response, expected_data)
|
||||||
|
|
||||||
|
def test_update_resource_not_found(self):
|
||||||
|
self.n_client.update_sample.side_effect = n_err.NotFound()
|
||||||
|
with self.assertRaises(error.NotFound):
|
||||||
|
self.client.update(8, param_foo='foo', param_bar=16)
|
|
@ -0,0 +1,4 @@
|
||||||
|
python-neutronclient>=2.6.0,!=4.1.0
|
||||||
|
networking-sfc>=1.0.0
|
||||||
|
|
||||||
|
-e git+https://git.openstack.org/openstack/murano#egg=murano
|
|
@ -9,3 +9,6 @@ author = Mirantis, Inc
|
||||||
[files]
|
[files]
|
||||||
packages = murano_plugin_networking_sfc
|
packages = murano_plugin_networking_sfc
|
||||||
|
|
||||||
|
[entry_points]
|
||||||
|
io.murano.extensions =
|
||||||
|
networking_sfc.NetworkingSfcClient = murano_plugin_networking_sfc:NetworkingSfcClient
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
pytest>=2.9.2 # MIT
|
pytest>=2.9.2 # MIT
|
||||||
mock>=2.0.0 # BSD
|
mock>=2.0.0 # BSD
|
||||||
hacking<0.12,>=0.11.0
|
hacking<0.12,>=0.11.0
|
||||||
|
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
|
||||||
|
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
|
||||||
|
|
10
tox.ini
10
tox.ini
|
@ -1,22 +1,26 @@
|
||||||
[tox]
|
[tox]
|
||||||
minversion = 2.3.1
|
minversion = 2.3.1
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
envlist = py27,pep8
|
envlist = py35,py34,py27,pep8
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
usedevelop=True
|
usedevelop=True
|
||||||
|
|
||||||
[testenv:py27]
|
|
||||||
deps = -r{toxinidir}/requirements.txt
|
deps = -r{toxinidir}/requirements.txt
|
||||||
-r{toxinidir}/test-requirements.txt
|
-r{toxinidir}/test-requirements.txt
|
||||||
commands =
|
commands =
|
||||||
py.test {posargs:murano_plugin_networking_sfc/tests}
|
py.test {posargs:murano_plugin_networking_sfc/tests}
|
||||||
|
|
||||||
|
[testenv:docs]
|
||||||
|
commands = python setup.py build_sphinx
|
||||||
|
|
||||||
[testenv:pep8]
|
[testenv:pep8]
|
||||||
deps = hacking>=0.11.0
|
deps = hacking>=0.11.0
|
||||||
commands =
|
commands =
|
||||||
flake8 {posargs:murano_plugin_networking_sfc}
|
flake8 {posargs:murano_plugin_networking_sfc}
|
||||||
|
|
||||||
|
[testenv:venv]
|
||||||
|
commands = {posargs}
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
show-pep8 = True
|
show-pep8 = True
|
||||||
show-source = True
|
show-source = True
|
||||||
|
|
Loading…
Reference in New Issue