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:
Alexander Saprykin 2016-07-01 17:57:09 +02:00
parent 91ec1a5784
commit bdb2a39be1
15 changed files with 464 additions and 3 deletions

View File

@ -0,0 +1,2 @@
Murano Plugin Networking SFC
============================

45
doc/source/conf.py Normal file
View File

@ -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'

1
doc/source/index.rst Normal file
View File

@ -0,0 +1 @@
.. include:: ../../README.rst

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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