diff --git a/README.rst b/README.rst index e69de29..d965347 100644 --- a/README.rst +++ b/README.rst @@ -0,0 +1,2 @@ +Murano Plugin Networking SFC +============================ diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..85452a5 --- /dev/null +++ b/doc/source/conf.py @@ -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' + diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..a6210d3 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst diff --git a/murano_plugin_networking_sfc/__init__.py b/murano_plugin_networking_sfc/__init__.py index e69de29..146314b 100644 --- a/murano_plugin_networking_sfc/__init__.py +++ b/murano_plugin_networking_sfc/__init__.py @@ -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 diff --git a/murano_plugin_networking_sfc/client.py b/murano_plugin_networking_sfc/client.py new file mode 100644 index 0000000..0cb9241 --- /dev/null +++ b/murano_plugin_networking_sfc/client.py @@ -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) diff --git a/murano_plugin_networking_sfc/config.py b/murano_plugin_networking_sfc/config.py new file mode 100644 index 0000000..c438516 --- /dev/null +++ b/murano_plugin_networking_sfc/config.py @@ -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 diff --git a/murano_plugin_networking_sfc/error.py b/murano_plugin_networking_sfc/error.py new file mode 100644 index 0000000..a9f98a1 --- /dev/null +++ b/murano_plugin_networking_sfc/error.py @@ -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 diff --git a/murano_plugin_networking_sfc/resource.py b/murano_plugin_networking_sfc/resource.py new file mode 100644 index 0000000..2a8d674 --- /dev/null +++ b/murano_plugin_networking_sfc/resource.py @@ -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' diff --git a/murano_plugin_networking_sfc/tests/unit/__init__.py b/murano_plugin_networking_sfc/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano_plugin_networking_sfc/tests/unit/test_client.py b/murano_plugin_networking_sfc/tests/unit/test_client.py new file mode 100644 index 0000000..24b21c9 --- /dev/null +++ b/murano_plugin_networking_sfc/tests/unit/test_client.py @@ -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)) diff --git a/murano_plugin_networking_sfc/tests/unit/test_resource.py b/murano_plugin_networking_sfc/tests/unit/test_resource.py new file mode 100644 index 0000000..62ce8d3 --- /dev/null +++ b/murano_plugin_networking_sfc/tests/unit/test_resource.py @@ -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) diff --git a/requirements.txt b/requirements.txt index e69de29..40e5c78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.cfg b/setup.cfg index 90a08de..580d91b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/test-requirements.txt b/test-requirements.txt index efb6ba1..41181f0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -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 diff --git a/tox.ini b/tox.ini index 31a2d48..b080a6b 100644 --- a/tox.ini +++ b/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