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]
|
||||
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
|
||||
mock>=2.0.0 # BSD
|
||||
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]
|
||||
minversion = 2.3.1
|
||||
skipsdist = True
|
||||
envlist = py27,pep8
|
||||
envlist = py35,py34,py27,pep8
|
||||
|
||||
[testenv]
|
||||
usedevelop=True
|
||||
|
||||
[testenv:py27]
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands =
|
||||
py.test {posargs:murano_plugin_networking_sfc/tests}
|
||||
|
||||
[testenv:docs]
|
||||
commands = python setup.py build_sphinx
|
||||
|
||||
[testenv:pep8]
|
||||
deps = hacking>=0.11.0
|
||||
commands =
|
||||
flake8 {posargs:murano_plugin_networking_sfc}
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[flake8]
|
||||
show-pep8 = True
|
||||
show-source = True
|
||||
|
|
Loading…
Reference in New Issue