commit e489f47798d0920df303c8f6e61eae603297e3d3 Author: Akihiro Motoki Date: Mon May 22 13:36:41 2017 +0900 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48546e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.pyc +*.swp +*.egg*/ +.coverage +.tox/ +build/ +cover/ +dist/ +AUTHORS +ChangeLog diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..c8aec64 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: + + 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/neutron-fwaas-dashbaard diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..10ccef9 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,13 @@ +========================================== +Neutron FWaaS Dashboard Style Commandments +========================================== + +Read the OpenStack Style Commandments +http://docs.openstack.org/developer/hacking/ + +Project Specific Commandments +----------------------------- + +- Read the Horizon contributing documentation at + http://docs.openstack.org/developer/horizon/contributing.html +- [M322] Method's default argument shouldn't be mutable. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..3faf2ce --- /dev/null +++ b/README.rst @@ -0,0 +1,9 @@ +======================== +Neutron FWaaS Dashboard +======================== + +OpenStack Dashboard panels for Neutron FWaaS + +* Documentation: http://docs.openstack.org/developer/neutron-fwaas-dashboard +* Source: http://git.openstack.org/cgit/openstack/neutron-fwaas-dashboard +* Bugs: http://bugs.launchpad.net/neutron-fwaas-dashboard diff --git a/babel-django.cfg b/babel-django.cfg new file mode 100644 index 0000000..ad09d34 --- /dev/null +++ b/babel-django.cfg @@ -0,0 +1,5 @@ +[extractors] +django = django_babel.extract:extract_django + +[python: **.py] +[django: templates/**.html] diff --git a/babel-djangojs.cfg b/babel-djangojs.cfg new file mode 100644 index 0000000..a8273b6 --- /dev/null +++ b/babel-djangojs.cfg @@ -0,0 +1,14 @@ +[extractors] +# We use a custom extractor to find translatable strings in AngularJS +# templates. The extractor is included in horizon.utils for now. +# See http://babel.pocoo.org/docs/messages/#referencing-extraction-methods for +# details on how this works. +angular = horizon.utils.babel_extract_angular:extract_angular + +[javascript: **.js] + +# We need to look into all static folders for HTML files. +# The **/static ensures that we also search within +# /openstack_dashboard/dashboards/XYZ/static which will ensure +# that plugins are also translated. +[angular: **/static/**.html] diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..306de8d --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,220 @@ +# 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. +# +# Horizon documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 27 11:38:59 2011. +# +# 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. + +from __future__ import print_function + +import django +import os +import subprocess +import sys +import warnings + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) + +sys.path.insert(0, ROOT) + +# This is required for ReadTheDocs.org, but isn't a bad idea anyway. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', + 'neutron_fwaas_dashboard.test.settings') + +import neutron_fwaas_dashboard.version + +django.setup() + + +# 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 = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'oslosphinx', +] + +# 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 = u'Neutron FWaaS Dashboard' +copyright = u'2017, OpenStack Foundation' + +# 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 = neutron_fwaas_dashboard.version.version_info.version_string() +# The full version, including alpha/beta/rc tags. +release = neutron_fwaas_dashboard.version.version_info.release_string() + +# 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 = ['neutron_fwaas_dashboard.'] + +primary_domain = 'py' +nitpicky = 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_path = ['.'] +# html_theme = '_theme' + +# 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 = { + "nosidebar": "false" +} + +# 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 = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' +git_cmd = ["git", "log", "--pretty=format:'%ad, commit %h'", "--date=local", + "-n1"] +try: + html_last_updated_fmt = subprocess.Popen( + git_cmd, stdout=subprocess.PIPE).communicate()[0] +except Exception: + warnings.warn('Cannot get last updated time from git repository. ' + 'Not setting "html_last_updated_fmt".') + +# 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 = 'neutronfwaasdashboarddoc' diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..adfc931 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,29 @@ +.. + Copyright 2017 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. + +================================ +Neutron FWaaS Dashboard Project +================================ + +Introduction +============ + +Neutron FWaaS Dashboard is a horizon plugin for Neutron FWaaS. + +Release Notes +============= + +See http://docs.openstack.org/releasenotes/neutron-fwaas-dashboard/. diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..b1a10ad --- /dev/null +++ b/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# 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 +import sys + +from django.core.management import execute_from_command_line # noqa + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", + "neutron_fwaas_dashboard.test.settings") + execute_from_command_line(sys.argv) diff --git a/neutron_fwaas_dashboard/__init__.py b/neutron_fwaas_dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron_fwaas_dashboard/api/__init__.py b/neutron_fwaas_dashboard/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron_fwaas_dashboard/api/fwaas.py b/neutron_fwaas_dashboard/api/fwaas.py new file mode 100644 index 0000000..dc521de --- /dev/null +++ b/neutron_fwaas_dashboard/api/fwaas.py @@ -0,0 +1,328 @@ +# Copyright 2013, Big Switch 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. + +from __future__ import absolute_import + +from collections import OrderedDict + +from horizon.utils import memoized + +from openstack_dashboard.api import neutron +from openstack_dashboard.contrib.developer.profiler import api as profiler + +neutronclient = neutron.neutronclient + + +class Rule(neutron.NeutronAPIDictWrapper): + """Wrapper for neutron firewall rule.""" + + def get_dict(self): + rule_dict = self._apidict + rule_dict['rule_id'] = rule_dict['id'] + return rule_dict + + +class Policy(neutron.NeutronAPIDictWrapper): + """Wrapper for neutron firewall policy.""" + + def get_dict(self): + policy_dict = self._apidict + policy_dict['policy_id'] = policy_dict['id'] + return policy_dict + + +class Firewall(neutron.NeutronAPIDictWrapper): + """Wrapper for neutron firewall.""" + + def __init__(self, apiresource): + apiresource['admin_state'] = \ + 'UP' if apiresource['admin_state_up'] else 'DOWN' + super(Firewall, self).__init__(apiresource) + + def get_dict(self): + firewall_dict = self._apidict + firewall_dict['firewall_id'] = firewall_dict['id'] + return firewall_dict + + +def rule_create(request, **kwargs): + """Create a firewall rule + + :param request: request context + :param name: name for rule + :param description: description for rule + :param protocol: protocol for rule + :param action: action for rule + :param source_ip_address: source IP address or subnet + :param source_port: integer in [1, 65535] or range in a:b + :param destination_ip_address: destination IP address or subnet + :param destination_port: integer in [1, 65535] or range in a:b + :param shared: boolean (default false) + :param enabled: boolean (default true) + :return: Rule object + """ + body = {'firewall_rule': kwargs} + rule = neutronclient(request).create_firewall_rule( + body).get('firewall_rule') + return Rule(rule) + + +@profiler.trace +def rule_list(request, **kwargs): + return _rule_list(request, expand_policy=True, **kwargs) + + +@profiler.trace +def rule_list_for_tenant(request, tenant_id, **kwargs): + """Return a rule list available for the tenant. + + The list contains rules owned by the tenant and shared rules. + This is required because Neutron returns all resources including + all tenants if a user has admin role. + """ + rules = rule_list(request, tenant_id=tenant_id, shared=False, **kwargs) + shared_rules = rule_list(request, shared=True, **kwargs) + return rules + shared_rules + + +def _rule_list(request, expand_policy, **kwargs): + rules = neutronclient(request).list_firewall_rules( + **kwargs).get('firewall_rules') + if expand_policy and rules: + policies = _policy_list(request, expand_rule=False) + policy_dict = OrderedDict((p.id, p) for p in policies) + for rule in rules: + rule['policy'] = policy_dict.get(rule['firewall_policy_id']) + return [Rule(r) for r in rules] + + +@profiler.trace +def rule_get(request, rule_id): + return _rule_get(request, rule_id, expand_policy=True) + + +def _rule_get(request, rule_id, expand_policy): + rule = neutronclient(request).show_firewall_rule( + rule_id).get('firewall_rule') + if expand_policy: + if rule['firewall_policy_id']: + rule['policy'] = _policy_get(request, rule['firewall_policy_id'], + expand_rule=False) + else: + rule['policy'] = None + return Rule(rule) + + +@profiler.trace +def rule_delete(request, rule_id): + neutronclient(request).delete_firewall_rule(rule_id) + + +@profiler.trace +def rule_update(request, rule_id, **kwargs): + body = {'firewall_rule': kwargs} + rule = neutronclient(request).update_firewall_rule( + rule_id, body).get('firewall_rule') + return Rule(rule) + + +@profiler.trace +def policy_create(request, **kwargs): + """Create a firewall policy + + :param request: request context + :param name: name for policy + :param description: description for policy + :param firewall_rules: ordered list of rules in policy + :param shared: boolean (default false) + :param audited: boolean (default false) + :return: Policy object + """ + body = {'firewall_policy': kwargs} + policy = neutronclient(request).create_firewall_policy( + body).get('firewall_policy') + return Policy(policy) + + +@profiler.trace +def policy_list(request, **kwargs): + return _policy_list(request, expand_rule=True, **kwargs) + + +@profiler.trace +def policy_list_for_tenant(request, tenant_id, **kwargs): + """Return a policy list available for the tenant. + + The list contains policies owned by the tenant and shared policies. + This is required because Neutron returns all resources including + all tenants if a user has admin role. + """ + policies = policy_list(request, tenant_id=tenant_id, + shared=False, **kwargs) + shared_policies = policy_list(request, shared=True, **kwargs) + return policies + shared_policies + + +def _policy_list(request, expand_rule, **kwargs): + policies = neutronclient(request).list_firewall_policies( + **kwargs).get('firewall_policies') + if expand_rule and policies: + rules = _rule_list(request, expand_policy=False) + rule_dict = OrderedDict((rule.id, rule) for rule in rules) + for p in policies: + p['rules'] = [rule_dict.get(rule) for rule in p['firewall_rules']] + return [Policy(p) for p in policies] + + +@profiler.trace +def policy_get(request, policy_id): + return _policy_get(request, policy_id, expand_rule=True) + + +def _policy_get(request, policy_id, expand_rule): + policy = neutronclient(request).show_firewall_policy( + policy_id).get('firewall_policy') + if expand_rule: + policy_rules = policy['firewall_rules'] + if policy_rules: + rules = _rule_list(request, expand_policy=False, + firewall_policy_id=policy_id) + rule_dict = OrderedDict((rule.id, rule) for rule in rules) + policy['rules'] = [rule_dict.get(rule) for rule in policy_rules] + else: + policy['rules'] = [] + return Policy(policy) + + +@profiler.trace +def policy_delete(request, policy_id): + neutronclient(request).delete_firewall_policy(policy_id) + + +@profiler.trace +def policy_update(request, policy_id, **kwargs): + body = {'firewall_policy': kwargs} + policy = neutronclient(request).update_firewall_policy( + policy_id, body).get('firewall_policy') + return Policy(policy) + + +@profiler.trace +def policy_insert_rule(request, policy_id, **kwargs): + policy = neutronclient(request).firewall_policy_insert_rule( + policy_id, kwargs) + return Policy(policy) + + +@profiler.trace +def policy_remove_rule(request, policy_id, **kwargs): + policy = neutronclient(request).firewall_policy_remove_rule( + policy_id, kwargs) + return Policy(policy) + + +@profiler.trace +def firewall_create(request, **kwargs): + """Create a firewall for specified policy + + :param request: request context + :param name: name for firewall + :param description: description for firewall + :param firewall_policy_id: policy id used by firewall + :param shared: boolean (default false) + :param admin_state_up: boolean (default true) + :return: Firewall object + """ + body = {'firewall': kwargs} + firewall = neutronclient(request).create_firewall(body).get('firewall') + return Firewall(firewall) + + +@profiler.trace +def firewall_list(request, **kwargs): + return _firewall_list(request, expand_policy=True, **kwargs) + + +@profiler.trace +def firewall_list_for_tenant(request, tenant_id, **kwargs): + """Return a firewall list available for the tenant. + + The list contains firewalls owned by the tenant and shared firewalls. + This is required because Neutron returns all resources including + all tenants if a user has admin role. + """ + # NOTE(amotoki): At now 'shared' attribute is not visible in Neutron + # and there is no way to query shared firewalls explicitly. + # Thus this method returns the same as when tenant_id is specified, + # but I would like to have this method for symmetry to firewall + # rules and policies to avoid unnecessary confusion. + return firewall_list(request, tenant_id=tenant_id, **kwargs) + + +def _firewall_list(request, expand_policy, **kwargs): + firewalls = neutronclient(request).list_firewalls( + **kwargs).get('firewalls') + if expand_policy and firewalls: + policies = _policy_list(request, expand_rule=False) + policy_dict = OrderedDict((p.id, p) for p in policies) + for fw in firewalls: + fw['policy'] = policy_dict.get(fw['firewall_policy_id']) + return [Firewall(f) for f in firewalls] + + +@profiler.trace +def firewall_get(request, firewall_id): + return _firewall_get(request, firewall_id, expand_policy=True) + + +def _firewall_get(request, firewall_id, expand_policy): + firewall = neutronclient(request).show_firewall( + firewall_id).get('firewall') + if expand_policy: + policy_id = firewall['firewall_policy_id'] + if policy_id: + firewall['policy'] = _policy_get(request, policy_id, + expand_rule=False) + else: + firewall['policy'] = None + return Firewall(firewall) + + +@profiler.trace +def firewall_delete(request, firewall_id): + neutronclient(request).delete_firewall(firewall_id) + + +@profiler.trace +def firewall_update(request, firewall_id, **kwargs): + body = {'firewall': kwargs} + firewall = neutronclient(request).update_firewall( + firewall_id, body).get('firewall') + return Firewall(firewall) + + +@profiler.trace +@memoized.memoized +def firewall_unassociated_routers_list(request, tenant_id): + all_routers = neutron.router_list(request, tenant_id=tenant_id) + tenant_firewalls = firewall_list_for_tenant(request, tenant_id=tenant_id) + firewall_router_ids = [rid + for fw in tenant_firewalls + for rid in getattr(fw, 'router_ids', [])] + + available_routers = [r for r in all_routers + if r.id not in firewall_router_ids] + available_routers = sorted(available_routers, + key=lambda router: router.name_or_id) + return available_routers diff --git a/neutron_fwaas_dashboard/dashboards/__init__.py b/neutron_fwaas_dashboard/dashboards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron_fwaas_dashboard/dashboards/project/__init__.py b/neutron_fwaas_dashboard/dashboards/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron_fwaas_dashboard/dashboards/project/forms.py b/neutron_fwaas_dashboard/dashboards/project/forms.py new file mode 100644 index 0000000..b0ba999 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/forms.py @@ -0,0 +1,404 @@ +# Copyright 2013, Big Switch 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. + +import logging + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages +from horizon.utils import validators + +from openstack_dashboard import api + +from neutron_fwaas_dashboard.api import fwaas as api_fwaas + +port_validator = validators.validate_port_or_colon_separated_port_range + +LOG = logging.getLogger(__name__) + + +class UpdateRule(forms.SelfHandlingForm): + name = forms.CharField(max_length=80, label=_("Name"), required=False) + description = forms.CharField( + required=False, + max_length=80, label=_("Description")) + protocol = forms.ThemableChoiceField( + label=_("Protocol"), required=False, + choices=[('TCP', _('TCP')), ('UDP', _('UDP')), ('ICMP', _('ICMP')), + ('ANY', _('ANY'))], + help_text=_('Protocol for the firewall rule')) + action = forms.ThemableChoiceField( + label=_("Action"), required=False, + choices=[('ALLOW', _('ALLOW')), ('DENY', _('DENY')), + ('REJECT', _('REJECT'))], + help_text=_('Action for the firewall rule')) + source_ip_address = forms.IPField( + label=_("Source IP Address/Subnet"), + version=forms.IPv4 | forms.IPv6, + required=False, mask=True, + help_text=_('Source IP address or subnet')) + destination_ip_address = forms.IPField( + label=_('Destination IP Address/Subnet'), + version=forms.IPv4 | forms.IPv6, + required=False, mask=True, + help_text=_('Destination IP address or subnet')) + source_port = forms.CharField( + max_length=80, + label=_("Source Port/Port Range"), + required=False, + validators=[port_validator], + help_text=_('Source port (integer in [1, 65535] or range in a:b)')) + destination_port = forms.CharField( + max_length=80, + label=_("Destination Port/Port Range"), + required=False, + validators=[port_validator], + help_text=_('Destination port (integer in [1, 65535] or range' + ' in a:b)')) + ip_version = forms.ThemableChoiceField( + label=_("IP Version"), required=False, + choices=[('4', '4'), ('6', '6')], + help_text=_('IP Version for Firewall Rule')) + shared = forms.BooleanField(label=_("Shared"), required=False) + enabled = forms.BooleanField(label=_("Enabled"), required=False) + + failure_url = 'horizon:project:firewalls:index' + + def handle(self, request, context): + rule_id = self.initial['rule_id'] + name_or_id = context.get('name') or rule_id + if context['protocol'] == 'ANY': + context['protocol'] = None + for f in ['source_ip_address', 'destination_ip_address', + 'source_port', 'destination_port']: + if not context[f]: + context[f] = None + try: + rule = api_fwaas.rule_update(request, rule_id, **context) + msg = _('Rule %s was successfully updated.') % name_or_id + messages.success(request, msg) + return rule + except Exception as e: + LOG.error('Failed to update rule %(id)s: %(reason)s', + {'id': rule_id, 'reason': e}) + msg = (_('Failed to update rule %(name)s: %(reason)s') % + {'name': name_or_id, 'reason': e}) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + + +class UpdatePolicy(forms.SelfHandlingForm): + name = forms.CharField(max_length=80, label=_("Name"), required=False) + description = forms.CharField(required=False, + max_length=80, label=_("Description")) + shared = forms.BooleanField(label=_("Shared"), required=False) + audited = forms.BooleanField(label=_("Audited"), required=False) + + failure_url = 'horizon:project:firewalls:index' + + def handle(self, request, context): + policy_id = self.initial['policy_id'] + name_or_id = context.get('name') or policy_id + try: + policy = api_fwaas.policy_update(request, policy_id, **context) + msg = _('Policy %s was successfully updated.') % name_or_id + messages.success(request, msg) + return policy + except Exception as e: + LOG.error('Failed to update policy %(id)s: %(reason)s', + {'id': policy_id, 'reason': e}) + msg = (_('Failed to update policy %(name)s: %(reason)s') % + {'name': name_or_id, 'reason': e}) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + + +class UpdateFirewall(forms.SelfHandlingForm): + name = forms.CharField(max_length=80, + label=_("Name"), + required=False) + description = forms.CharField(max_length=80, + label=_("Description"), + required=False) + firewall_policy_id = forms.ThemableChoiceField(label=_("Policy")) + admin_state_up = forms.BooleanField(label=_("Enable Admin State"), + required=False) + failure_url = 'horizon:project:firewalls:index' + + def __init__(self, request, *args, **kwargs): + super(UpdateFirewall, self).__init__(request, *args, **kwargs) + + try: + tenant_id = self.request.user.tenant_id + policies = api_fwaas.policy_list_for_tenant(request, tenant_id) + policies = sorted(policies, key=lambda policy: policy.name) + except Exception: + exceptions.handle(request, + _('Unable to retrieve policy list.')) + policies = [] + + policy_id = kwargs['initial']['firewall_policy_id'] + policy_name = [p.name for p in policies if p.id == policy_id][0] + + firewall_policy_id_choices = [(policy_id, policy_name)] + for p in policies: + if p.id != policy_id: + firewall_policy_id_choices.append((p.id, p.name_or_id)) + + self.fields['firewall_policy_id'].choices = firewall_policy_id_choices + + def handle(self, request, context): + firewall_id = self.initial['firewall_id'] + name_or_id = context.get('name') or firewall_id + try: + firewall = api_fwaas.firewall_update(request, firewall_id, + **context) + msg = _('Firewall %s was successfully updated.') % name_or_id + messages.success(request, msg) + return firewall + except Exception as e: + LOG.error('Failed to update firewall %(id)s: %(reason)s', + {'id': firewall_id, 'reason': e}) + msg = (_('Failed to update firewall %(name)s: %(reason)s') % + {'name': name_or_id, 'reason': e}) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + + +class InsertRuleToPolicy(forms.SelfHandlingForm): + firewall_rule_id = forms.ThemableChoiceField(label=_("Insert Rule")) + insert_before = forms.ThemableChoiceField(label=_("Before"), + required=False) + insert_after = forms.ThemableChoiceField(label=_("After"), + required=False) + + failure_url = 'horizon:project:firewalls:index' + + def __init__(self, request, *args, **kwargs): + super(InsertRuleToPolicy, self).__init__(request, *args, **kwargs) + + try: + tenant_id = self.request.user.tenant_id + all_rules = api_fwaas.rule_list_for_tenant(request, tenant_id) + all_rules = sorted(all_rules, key=lambda rule: rule.name_or_id) + + available_rules = [r for r in all_rules + if not r.firewall_policy_id] + + current_rules = [] + for r in kwargs['initial']['firewall_rules']: + r_obj = [rule for rule in all_rules if r == rule.id][0] + current_rules.append(r_obj) + + available_choices = [(r.id, r.name_or_id) for r in available_rules] + current_choices = [(r.id, r.name_or_id) for r in current_rules] + + except Exception as e: + LOG.error('Failed to retrieve available rules: %s', e) + msg = _('Failed to retrieve available rules: %s') % e + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + + self.fields['firewall_rule_id'].choices = available_choices + self.fields['insert_before'].choices = [('', '')] + current_choices + self.fields['insert_after'].choices = [('', '')] + current_choices + + def handle(self, request, context): + policy_id = self.initial['policy_id'] + policy_name_or_id = self.initial['name'] or policy_id + try: + insert_rule_id = context['firewall_rule_id'] + insert_rule = api_fwaas.rule_get(request, insert_rule_id) + body = {'firewall_rule_id': insert_rule_id, + 'insert_before': context['insert_before'], + 'insert_after': context['insert_after']} + policy = api_fwaas.policy_insert_rule(request, policy_id, **body) + msg = _('Rule %(rule)s was successfully inserted to policy ' + '%(policy)s.') % { + 'rule': insert_rule.name or insert_rule.id, + 'policy': policy_name_or_id} + messages.success(request, msg) + return policy + except Exception as e: + LOG.error('Failed to insert rule to policy %(id)s: %(reason)s', + {'id': policy_id, 'reason': e}) + msg = (_('Failed to insert rule to policy %(name)s: %(reason)s') % + {'name': policy_id, 'reason': e}) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + + +class RemoveRuleFromPolicy(forms.SelfHandlingForm): + firewall_rule_id = forms.ThemableChoiceField(label=_("Remove Rule")) + + failure_url = 'horizon:project:firewalls:index' + + def __init__(self, request, *args, **kwargs): + super(RemoveRuleFromPolicy, self).__init__(request, *args, **kwargs) + + try: + tenant_id = request.user.tenant_id + all_rules = api_fwaas.rule_list_for_tenant(request, tenant_id) + + current_rules = [] + for r in kwargs['initial']['firewall_rules']: + r_obj = [rule for rule in all_rules if r == rule.id][0] + current_rules.append(r_obj) + + current_choices = [(r.id, r.name_or_id) for r in current_rules] + except Exception as e: + LOG.error('Failed to retrieve current rules in policy %(id)s: ' + '%(reason)s', + {'id': self.initial['policy_id'], 'reason': e}) + msg = (_('Failed to retrieve current rules in policy %(name)s: ' + '%(reason)s') % + {'name': self.initial['name'], 'reason': e}) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + + self.fields['firewall_rule_id'].choices = current_choices + + def handle(self, request, context): + policy_id = self.initial['policy_id'] + policy_name_or_id = self.initial['name'] or policy_id + try: + remove_rule_id = context['firewall_rule_id'] + remove_rule = api_fwaas.rule_get(request, remove_rule_id) + body = {'firewall_rule_id': remove_rule_id} + policy = api_fwaas.policy_remove_rule(request, policy_id, **body) + msg = _('Rule %(rule)s was successfully removed from policy ' + '%(policy)s.') % { + 'rule': remove_rule.name or remove_rule.id, + 'policy': policy_name_or_id} + messages.success(request, msg) + return policy + except Exception as e: + LOG.error('Failed to remove rule from policy %(id)s: %(reason)s', + {'id': policy_id, 'reason': e}) + msg = (_('Failed to remove rule from policy %(name)s: %(reason)s') + % {'name': self.initial['name'], 'reason': e}) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + + +class AddRouterToFirewall(forms.SelfHandlingForm): + router_ids = forms.MultipleChoiceField( + label=_("Add Routers"), + required=False, + widget=forms.ThemableCheckboxSelectMultiple(), + help_text=_("Add selected router(s) to the firewall.")) + + failure_url = 'horizon:project:firewalls:index' + + def __init__(self, request, *args, **kwargs): + super(AddRouterToFirewall, self).__init__(request, *args, **kwargs) + try: + router_choices = self.get_router_choices(request, kwargs) + self.fields['router_ids'].choices = router_choices + except Exception as e: + LOG.error('Failed to retrieve available routers: %s', e) + msg = _('Failed to retrieve available routers: %s') % e + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + + def get_router_choices(self, request, kwargs): + tenant_id = self.request.user.tenant_id + routers_list = api_fwaas.firewall_unassociated_routers_list( + request, tenant_id) + return [(r.id, r.name_or_id) for r in routers_list] + + def get_new_router_ids(self, context): + existing_router_ids = self.initial['router_ids'] + add_router_ids = context['router_ids'] + return add_router_ids + existing_router_ids + + def handle(self, request, context): + firewall_id = self.initial['firewall_id'] + firewall_name_or_id = self.initial['name'] or firewall_id + try: + body = {'router_ids': self.get_new_router_ids(context)} + firewall = api_fwaas.firewall_update(request, firewall_id, **body) + msg = (_('Router(s) was/were successfully added to firewall ' + '%(firewall)s.') % + {'firewall': firewall_name_or_id}) + messages.success(request, msg) + return firewall + except Exception as e: + LOG.error('Failed to add router(s) to firewall %(id)s: %(reason)s', + {'id': firewall_id, 'reason': e}) + msg = (_('Failed to add router(s) to firewall %(name)s: ' + '%(reason)s') % + {'name': firewall_name_or_id, 'reason': e}) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + + +class RemoveRouterFromFirewall(forms.SelfHandlingForm): + router_ids = forms.MultipleChoiceField( + label=_("Associated Routers"), + required=False, + widget=forms.ThemableCheckboxSelectMultiple(), + help_text=_("Unselect the router(s) to be removed from firewall.")) + + failure_url = 'horizon:project:firewalls:index' + + def __init__(self, request, *args, **kwargs): + super(RemoveRouterFromFirewall, self).__init__(request, + *args, **kwargs) + try: + router_choices = self.get_router_choices(request, kwargs) + self.fields['router_ids'].choices = router_choices + except Exception as e: + LOG.error('Failed to retrieve current routers in firewall %(id)s: ' + '%(reason)s', + {'id': self.initial['firewall_id'], 'reason': e}) + msg = (_('Failed to retrieve current routers in firewall ' + '%(name)s: %(reason)s') % + {'name': self.initial['name'], 'reason': e}) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) + + def get_router_choices(self, request, kwargs): + tenant_id = self.request.user.tenant_id + all_routers = api.neutron.router_list(request, tenant_id=tenant_id) + current_routers = [r for r in all_routers + if r['id'] in kwargs['initial']['router_ids']] + return [(r.id, r.name_or_id) for r in current_routers] + + def get_new_router_ids(self, context): + # context[router_ids] is router IDs to be kept. + return context['router_ids'] + + def handle(self, request, context): + firewall_id = self.initial['firewall_id'] + firewall_name_or_id = self.initial['name'] or firewall_id + try: + body = {'router_ids': self.get_new_router_ids(context)} + firewall = api_fwaas.firewall_update(request, firewall_id, **body) + msg = (_('Router(s) was successfully removed from firewall ' + '%(firewall)s.') % + {'firewall': firewall_name_or_id}) + messages.success(request, msg) + return firewall + except Exception as e: + LOG.error('Failed to remove router(s) from firewall %(id)s: ' + '%(reason)s', {'id': firewall_id, 'reason': e}) + msg = (_('Failed to remove router(s) from firewall %(name)s: ' + '%(reason)s') % + {'name': firewall_name_or_id, 'reason': e}) + redirect = reverse(self.failure_url) + exceptions.handle(request, msg, redirect=redirect) diff --git a/neutron_fwaas_dashboard/dashboards/project/panel.py b/neutron_fwaas_dashboard/dashboards/project/panel.py new file mode 100644 index 0000000..d51dedf --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/panel.py @@ -0,0 +1,43 @@ +# 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 django.utils.translation import ugettext_lazy as _ + +import horizon + +from openstack_dashboard.api import neutron + +LOG = logging.getLogger(__name__) + + +class Firewall(horizon.Panel): + name = _("Firewalls") + slug = "firewalls" + permissions = ('openstack.services.network',) + + def allowed(self, context): + request = context['request'] + if not request.user.has_perms(self.permissions): + return False + try: + if not neutron.is_extension_supported(request, 'fwaas'): + return False + except Exception: + LOG.error("Call to list enabled services failed. This is likely " + "due to a problem communicating with the Neutron " + "endpoint. Firewalls panel will not be displayed.") + return False + if not super(Firewall, self).allowed(context): + return False + return True diff --git a/neutron_fwaas_dashboard/dashboards/project/tables.py b/neutron_fwaas_dashboard/dashboards/project/tables.py new file mode 100644 index 0000000..ed6fdc1 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/tables.py @@ -0,0 +1,425 @@ +# Copyright 2013, Big Switch 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. + +import logging + +from django.core.urlresolvers import reverse +from django.template import defaultfilters as filters +from django.utils.translation import pgettext_lazy +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import exceptions +from horizon import tables +from openstack_dashboard import api +from openstack_dashboard import policy + +from neutron_fwaas_dashboard.api import fwaas as api_fwaas + +LOG = logging.getLogger(__name__) + + +class AddRuleLink(tables.LinkAction): + name = "addrule" + verbose_name = _("Add Rule") + url = "horizon:project:firewalls:addrule" + classes = ("ajax-modal",) + icon = "plus" + policy_rules = (("network", "create_firewall_rule"),) + + +class AddPolicyLink(tables.LinkAction): + name = "addpolicy" + verbose_name = _("Add Policy") + url = "horizon:project:firewalls:addpolicy" + classes = ("ajax-modal", "btn-addpolicy",) + icon = "plus" + policy_rules = (("network", "create_firewall_policy"),) + + +class AddFirewallLink(tables.LinkAction): + name = "addfirewall" + verbose_name = _("Create Firewall") + url = "horizon:project:firewalls:addfirewall" + classes = ("ajax-modal",) + icon = "plus" + policy_rules = (("network", "create_firewall"),) + + +class DeleteRuleLink(policy.PolicyTargetMixin, tables.DeleteAction): + name = "deleterule" + policy_rules = (("network", "delete_firewall_rule"),) + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Rule", + u"Delete Rules", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Scheduled deletion of Rule", + u"Scheduled deletion of Rules", + count + ) + + def allowed(self, request, datum=None): + if datum and datum.policy: + return False + return True + + def delete(self, request, obj_id): + try: + api_fwaas.rule_delete(request, obj_id) + except Exception as e: + exceptions.handle(request, _('Unable to delete rule. %s') % e) + + +class DeletePolicyLink(policy.PolicyTargetMixin, tables.DeleteAction): + name = "deletepolicy" + policy_rules = (("network", "delete_firewall_policy"),) + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Policy", + u"Delete Policies", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Scheduled deletion of Policy", + u"Scheduled deletion of Policies", + count + ) + + def delete(self, request, obj_id): + try: + api_fwaas.policy_delete(request, obj_id) + except Exception as e: + exceptions.handle(request, _('Unable to delete policy. %s') % e) + + +class DeleteFirewallLink(policy.PolicyTargetMixin, + tables.DeleteAction): + name = "deletefirewall" + policy_rules = (("network", "delete_firewall"),) + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Firewall", + u"Delete Firewalls", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Scheduled deletion of Firewall", + u"Scheduled deletion of Firewalls", + count + ) + + def delete(self, request, obj_id): + try: + api_fwaas.firewall_delete(request, obj_id) + except Exception as e: + exceptions.handle(request, _('Unable to delete firewall. %s') % e) + + +class UpdateRuleLink(policy.PolicyTargetMixin, tables.LinkAction): + name = "updaterule" + verbose_name = _("Edit Rule") + classes = ("ajax-modal", "btn-update",) + policy_rules = (("network", "update_firewall_rule"),) + + def get_link_url(self, rule): + base_url = reverse("horizon:project:firewalls:updaterule", + kwargs={'rule_id': rule.id}) + return base_url + + +class UpdatePolicyLink(policy.PolicyTargetMixin, tables.LinkAction): + name = "updatepolicy" + verbose_name = _("Edit Policy") + classes = ("ajax-modal", "btn-update",) + policy_rules = (("network", "update_firewall_policy"),) + + def get_link_url(self, policy): + base_url = reverse("horizon:project:firewalls:updatepolicy", + kwargs={'policy_id': policy.id}) + return base_url + + +class UpdateFirewallLink(policy.PolicyTargetMixin, tables.LinkAction): + name = "updatefirewall" + verbose_name = _("Edit Firewall") + classes = ("ajax-modal", "btn-update",) + policy_rules = (("network", "update_firewall"),) + + def get_link_url(self, firewall): + base_url = reverse("horizon:project:firewalls:updatefirewall", + kwargs={'firewall_id': firewall.id}) + return base_url + + def allowed(self, request, firewall): + if firewall.status in ("PENDING_CREATE", + "PENDING_UPDATE", + "PENDING_DELETE"): + return False + return True + + +class InsertRuleToPolicyLink(policy.PolicyTargetMixin, + tables.LinkAction): + name = "insertrule" + verbose_name = _("Insert Rule") + classes = ("ajax-modal", "btn-update",) + policy_rules = (("network", "get_firewall_policy"), + ("network", "insert_rule"),) + + def get_link_url(self, policy): + base_url = reverse("horizon:project:firewalls:insertrule", + kwargs={'policy_id': policy.id}) + return base_url + + +class RemoveRuleFromPolicyLink(policy.PolicyTargetMixin, + tables.LinkAction): + name = "removerule" + verbose_name = _("Remove Rule") + classes = ("ajax-modal",) + policy_rules = (("network", "get_firewall_policy"), + ("network", "remove_rule"),) + action_type = "danger" + + def get_link_url(self, policy): + base_url = reverse("horizon:project:firewalls:removerule", + kwargs={'policy_id': policy.id}) + return base_url + + def allowed(self, request, policy): + if len(policy.rules) > 0: + return True + return False + + +class AddRouterToFirewallLink(policy.PolicyTargetMixin, + tables.LinkAction): + name = "addrouter" + verbose_name = _("Add Router") + classes = ("ajax-modal", "btn-update",) + policy_rules = (("network", "get_firewall"), + ("network", "add_router"),) + + def get_link_url(self, firewall): + base_url = reverse("horizon:project:firewalls:addrouter", + kwargs={'firewall_id': firewall.id}) + return base_url + + def allowed(self, request, firewall): + if not api.neutron.is_extension_supported(request, + 'fwaasrouterinsertion'): + return False + tenant_id = firewall['tenant_id'] + available_routers = api_fwaas.firewall_unassociated_routers_list( + request, tenant_id) + return bool(available_routers) + + +class RemoveRouterFromFirewallLink(policy.PolicyTargetMixin, + tables.LinkAction): + name = "removerouter" + verbose_name = _("Remove Router") + classes = ("ajax-modal", "btn-update",) + policy_rules = (("network", "get_firewall"), + ("network", "remove_router"),) + + def get_link_url(self, firewall): + base_url = reverse("horizon:project:firewalls:removerouter", + kwargs={'firewall_id': firewall.id}) + return base_url + + def allowed(self, request, firewall): + if not api.neutron.is_extension_supported(request, + 'fwaasrouterinsertion'): + return False + return bool(firewall['router_ids']) + + +def get_rules_name(datum): + return ', '.join([rule.name or rule.id[:13] + for rule in datum.rules]) + + +def get_routers_name(firewall): + if firewall.routers: + return ', '.join(router.name_or_id for router in firewall.routers) + + +def get_policy_name(datum): + if datum.policy: + return datum.policy.name or datum.policy.id + + +def get_policy_link(datum): + if datum.policy: + return reverse('horizon:project:firewalls:policydetails', + kwargs={'policy_id': datum.policy.id}) + + +class RulesTable(tables.DataTable): + ACTION_DISPLAY_CHOICES = ( + ("Allow", pgettext_lazy("Action Name of a Firewall Rule", u"ALLOW")), + ("Deny", pgettext_lazy("Action Name of a Firewall Rule", u"DENY")), + ("Reject", pgettext_lazy("Action Name of a Firewall Rule", u"REJECT")), + ) + name = tables.Column("name_or_id", + verbose_name=_("Name"), + link="horizon:project:firewalls:ruledetails") + description = tables.Column('description', verbose_name=_('Description')) + protocol = tables.Column("protocol", + filters=(lambda v: filters.default(v, _("ANY")), + filters.upper,), + verbose_name=_("Protocol")) + source_ip_address = tables.Column("source_ip_address", + verbose_name=_("Source IP")) + source_port = tables.Column("source_port", + verbose_name=_("Source Port")) + destination_ip_address = tables.Column("destination_ip_address", + verbose_name=_("Destination IP")) + destination_port = tables.Column("destination_port", + verbose_name=_("Destination Port")) + action = tables.Column("action", + display_choices=ACTION_DISPLAY_CHOICES, + verbose_name=_("Action")) + shared = tables.Column("shared", + verbose_name=_("Shared"), + filters=(filters.yesno, filters.capfirst)) + enabled = tables.Column("enabled", + verbose_name=_("Enabled"), + filters=(filters.yesno, filters.capfirst)) + firewall_policy_id = tables.Column(get_policy_name, + link=get_policy_link, + verbose_name=_("In Policy")) + + def get_object_display(self, rule): + return rule.name_or_id + + class Meta(object): + name = "rulestable" + verbose_name = _("Rules") + table_actions = (AddRuleLink, + DeleteRuleLink, + tables.NameFilterAction) + row_actions = (UpdateRuleLink, DeleteRuleLink) + + +class PoliciesTable(tables.DataTable): + name = tables.Column("name_or_id", + verbose_name=_("Name"), + link="horizon:project:firewalls:policydetails") + description = tables.Column('description', verbose_name=_('Description')) + firewall_rules = tables.Column(get_rules_name, + verbose_name=_("Rules")) + shared = tables.Column("shared", + verbose_name=_("Shared"), + filters=(filters.yesno, filters.capfirst)) + audited = tables.Column("audited", + verbose_name=_("Audited"), + filters=(filters.yesno, filters.capfirst)) + + def get_object_display(self, policy): + return policy.name_or_id + + class Meta(object): + name = "policiestable" + verbose_name = _("Policies") + table_actions = (AddPolicyLink, + DeletePolicyLink, + tables.NameFilterAction) + row_actions = (UpdatePolicyLink, InsertRuleToPolicyLink, + RemoveRuleFromPolicyLink, DeletePolicyLink) + + +class FirewallsTable(tables.DataTable): + STATUS_DISPLAY_CHOICES = ( + ("Active", pgettext_lazy("Current status of a Firewall", + u"Active")), + ("Down", pgettext_lazy("Current status of a Firewall", + u"Down")), + ("Error", pgettext_lazy("Current status of a Firewall", + u"Error")), + ("Created", pgettext_lazy("Current status of a Firewall", + u"Created")), + ("Pending_Create", pgettext_lazy("Current status of a Firewall", + u"Pending Create")), + ("Pending_Update", pgettext_lazy("Current status of a Firewall", + u"Pending Update")), + ("Pending_Delete", pgettext_lazy("Current status of a Firewall", + u"Pending Delete")), + ("Inactive", pgettext_lazy("Current status of a Firewall", + u"Inactive")), + ) + ADMIN_STATE_DISPLAY_CHOICES = ( + ("UP", pgettext_lazy("Admin state of a Firewall", u"UP")), + ("DOWN", pgettext_lazy("Admin state of a Firewall", u"DOWN")), + ) + + name = tables.Column("name_or_id", + verbose_name=_("Name"), + link="horizon:project:firewalls:firewalldetails") + description = tables.Column('description', verbose_name=_('Description')) + firewall_policy_id = tables.Column(get_policy_name, + link=get_policy_link, + verbose_name=_("Policy")) + router_ids = tables.Column(get_routers_name, + verbose_name=_("Associated Routers")) + status = tables.Column("status", + verbose_name=_("Status"), + display_choices=STATUS_DISPLAY_CHOICES) + admin_state = tables.Column("admin_state", + verbose_name=_("Admin State"), + display_choices=ADMIN_STATE_DISPLAY_CHOICES) + + def get_object_display(self, firewall): + return firewall.name_or_id + + class Meta(object): + name = "firewallstable" + verbose_name = _("Firewalls") + table_actions = (AddFirewallLink, + DeleteFirewallLink, + tables.NameFilterAction) + row_actions = (UpdateFirewallLink, DeleteFirewallLink, + AddRouterToFirewallLink, RemoveRouterFromFirewallLink) + + def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs): + super(FirewallsTable, self).__init__( + request, data=data, + needs_form_wrapper=needs_form_wrapper, **kwargs) + try: + if not api.neutron.is_extension_supported(request, + 'fwaasrouterinsertion'): + del self.columns['router_ids'] + except Exception as e: + LOG.error('Failed to verify extension support %s', e) + msg = _('Failed to verify extension support %s') % e + exceptions.handle(request, msg) diff --git a/neutron_fwaas_dashboard/dashboards/project/tabs.py b/neutron_fwaas_dashboard/dashboards/project/tabs.py new file mode 100644 index 0000000..4e419be --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/tabs.py @@ -0,0 +1,142 @@ +# Copyright 2013, Big Switch 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. + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tabs + +from openstack_dashboard import api + +from neutron_fwaas_dashboard.api import fwaas as api_fwaas +from neutron_fwaas_dashboard.dashboards.project import tables + +FirewallsTable = tables.FirewallsTable +PoliciesTable = tables.PoliciesTable +RulesTable = tables.RulesTable + + +class RulesTab(tabs.TableTab): + table_classes = (RulesTable,) + name = _("Firewall Rules") + slug = "rules" + template_name = "horizon/common/_detail_table.html" + + def get_rulestable_data(self): + try: + tenant_id = self.request.user.tenant_id + request = self.tab_group.request + rules = api_fwaas.rule_list_for_tenant(request, tenant_id) + except Exception: + rules = [] + exceptions.handle(self.tab_group.request, + _('Unable to retrieve rules list.')) + + return rules + + +class PoliciesTab(tabs.TableTab): + table_classes = (PoliciesTable,) + name = _("Firewall Policies") + slug = "policies" + template_name = "horizon/common/_detail_table.html" + + def get_policiestable_data(self): + try: + tenant_id = self.request.user.tenant_id + request = self.tab_group.request + policies = api_fwaas.policy_list_for_tenant(request, tenant_id) + except Exception: + policies = [] + exceptions.handle(self.tab_group.request, + _('Unable to retrieve policies list.')) + + return policies + + +class FirewallsTab(tabs.TableTab): + table_classes = (FirewallsTable,) + name = _("Firewalls") + slug = "firewalls" + template_name = "horizon/common/_detail_table.html" + + def get_firewallstable_data(self): + try: + tenant_id = self.request.user.tenant_id + request = self.tab_group.request + firewalls = api_fwaas.firewall_list_for_tenant(request, tenant_id) + + if api.neutron.is_extension_supported(request, + 'fwaasrouterinsertion'): + routers = api.neutron.router_list(request, tenant_id=tenant_id) + + for fw in firewalls: + router_list = [r for r in routers + if r['id'] in fw['router_ids']] + fw.get_dict()['routers'] = router_list + + except Exception: + firewalls = [] + exceptions.handle(self.tab_group.request, + _('Unable to retrieve firewall list.')) + + return firewalls + + +class RuleDetailsTab(tabs.Tab): + name = _("Rule") + slug = "ruledetails" + template_name = "project/firewalls/_rule_details.html" + + def get_context_data(self, request): + return {"rule": self.tab_group.kwargs['rule']} + + +class PolicyDetailsTab(tabs.Tab): + name = _("Policy") + slug = "policydetails" + template_name = "project/firewalls/_policy_details.html" + + def get_context_data(self, request): + return {"policy": self.tab_group.kwargs['policy']} + + +class FirewallDetailsTab(tabs.Tab): + name = _("Firewall") + slug = "firewalldetails" + template_name = "project/firewalls/_firewall_details.html" + + def get_context_data(self, request): + return {"firewall": self.tab_group.kwargs['firewall']} + + +class FirewallTabs(tabs.TabGroup): + slug = "fwtabs" + tabs = (FirewallsTab, PoliciesTab, RulesTab) + sticky = True + + +class RuleDetailsTabs(tabs.TabGroup): + slug = "ruletabs" + tabs = (RuleDetailsTab,) + + +class PolicyDetailsTabs(tabs.TabGroup): + slug = "policytabs" + tabs = (PolicyDetailsTab,) + + +class FirewallDetailsTabs(tabs.TabGroup): + slug = "firewalltabs" + tabs = (FirewallDetailsTab,) diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_add_router_to_firewall.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_add_router_to_firewall.html new file mode 100644 index 0000000..0d23e5d --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_add_router_to_firewall.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Choose the router(s) you want to add." %}

+ +{% endblock %} \ No newline at end of file diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_firewall_details.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_firewall_details.html new file mode 100644 index 0000000..ec67a18 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_firewall_details.html @@ -0,0 +1,41 @@ +{% load i18n sizeformat parse_date %} + +
+
+
{% trans "Name" %}
+
{{ firewall.name|default:_("-") }}
+ +
{% trans "Description" %}
+
{{ firewall.description|default:_("-") }}
+ +
{% trans "ID" %}
+
{{ firewall.id }}
+ +
{% trans "Project ID" %}
+
{{ firewall.tenant_id }}
+ +
{% trans "Policy ID" %}
+
+ {% url 'horizon:project:firewalls:policydetails' firewall.firewall_policy_id as policy_url %} + {{ firewall.policy.name|default:firewall.policy.id }} +
+ +
{% trans "Status" %}
+
{{ firewall.status }}
+ +
{% trans "Admin State Up" %}
+
{{ firewall.admin_state_up|yesno|capfirst }}
+ +
{% trans "Routers" %}
+
+ {% if routers %} + {% for router in routers %} + {% url 'horizon:project:routers:detail' router.id as router_url %} + {{ router.name|default:router.id}}
+ {% endfor %} + {% else %} + {% trans "-" %} + {% endif %} +
+
+
diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_insert_rule_to_policy.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_insert_rule_to_policy.html new file mode 100644 index 0000000..7247bfc --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_insert_rule_to_policy.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Choose the rule you want to insert. Specify either the rule you want to insert immediately before, or the rule to insert immediately after. If both are specified, the prior takes precedence." %}

+{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_policy_details.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_policy_details.html new file mode 100644 index 0000000..1160a81 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_policy_details.html @@ -0,0 +1,35 @@ +{% load i18n sizeformat parse_date %} + +
+
+
{% trans "Name" %}
+
{{ policy.name|default:_("-") }}
+ +
{% trans "Description" %}
+
{{ policy.description|default:_("-") }}
+ +
{% trans "ID" %}
+
{{ policy.id }}
+ +
{% trans "Project ID" %}
+
{{ policy.tenant_id }}
+ +
{% trans "Rules" %}
+
+ {% if policy.rules %} + {% for rule in policy.rules %} + {% url 'horizon:project:firewalls:ruledetails' rule.id as rule_url %} + {{ rule.position }} : {{ rule.name|default:rule.id }}
+ {% endfor %} + {% else %} + {% trans "-" %} + {% endif %} +
+ +
{% trans "Shared" %}
+
{{ policy.shared|yesno|capfirst }}
+ +
{% trans "Audited" %}
+
{{ policy.audited|yesno|capfirst }}
+
+
diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_remove_router_from_firewall.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_remove_router_from_firewall.html new file mode 100644 index 0000000..79123ae --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_remove_router_from_firewall.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Unselect the routers you want to disassociate from the firewall." %}

+{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_remove_rule_from_policy.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_remove_rule_from_policy.html new file mode 100644 index 0000000..09a76d7 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_remove_rule_from_policy.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Choose the rule you want to remove." %}

+{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_rule_details.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_rule_details.html new file mode 100644 index 0000000..06732bf --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_rule_details.html @@ -0,0 +1,54 @@ +{% load i18n sizeformat parse_date %} + +
+
+
{% trans "Name" %}
+
{{ rule.name|default:_("-") }}
+ +
{% trans "Description" %}
+
{{ rule.description|default:_("-") }}
+ +
{% trans "ID" %}
+
{{ rule.id }}
+ +
{% trans "Project ID" %}
+
{{ rule.tenant_id }}
+ +
{% trans "Action" %}
+
{{ rule.action|upper }}
+ +
{% trans "Protocol" %}
+
{{ rule.protocol|default:_("ANY")|upper }}
+ +
{% trans "Source IP Address" %}
+
{{ rule.source_ip_address|default:_("ANY") }}
+ +
{% trans "Source Port" %}
+
{{ rule.source_port|default:_("ANY") }}
+ +
{% trans "Destination IP Address" %}
+
{{ rule.destination_ip_address|default:_("ANY") }}
+ +
{% trans "Destination Port"%}
+
{{ rule.destination_port|default:_("ANY") }}
+ +
{% trans "Used in Policy" %}
+
+ {% if rule.policy %} + {% url 'horizon:project:firewalls:policydetails' rule.policy.id as policy_url %} + {{ rule.policy.name|default:rule.policy.id }} + {% else %} + {% trans "-" %} + {% endif %} +
+ +
{% trans "Position in Policy" %}
+
{{ rule.position|default:_("-") }}
+ +
{% trans "Shared" %}
+
{{ rule.shared|yesno|capfirst }}
+ +
{% trans "Enabled" %}
+
{{ rule.enabled|yesno|capfirst }}
+
+
diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_router_help.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_router_help.html new file mode 100644 index 0000000..9808b0b --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_router_help.html @@ -0,0 +1,3 @@ +{% load i18n %} + +

{% blocktrans %}Choose router(s) from Available Routers to Selected Routers by push button or drag and drop. {% endblocktrans %}

diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_routers.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_routers.html new file mode 100644 index 0000000..6fca5c9 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_routers.html @@ -0,0 +1,35 @@ +{% load i18n %} + + +
+
+

{% trans "Selected Routers" %}

+
    +

    {% trans "Available Routers" %}

    +
      +
      +
      + {% include "project/firewalls/_update_router_help.html" %} +
      +
      + +
      +
      +
      + {% include "horizon/common/_form_fields.html" %} +
      +
      +
      + {{ step.get_help_text }} +
      +
      + + diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_rule_help.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_rule_help.html new file mode 100644 index 0000000..4a0b232 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_rule_help.html @@ -0,0 +1,6 @@ +{% load i18n horizon %} + +

      {% blocktrans trimmed %} +Choose rule(s) from Available Rules to Selected Rule by push button or +drag and drop, you may change their order by drag and drop as well. +{% endblocktrans %}

      diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_rules.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_rules.html new file mode 100644 index 0000000..320ee9e --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_update_rules.html @@ -0,0 +1,35 @@ +{% load i18n %} + + +
      +
      +

      {% trans "Selected Rules" %}

      +
        +

        {% trans "Available Rules" %}

        +
          +
          +
          + {% include "project/firewalls/_update_rule_help.html" %} +
          +
          + +
          +
          +
          + {% include "horizon/common/_form_fields.html" %} +
          +
          +
          + {{ step.get_help_text }} +
          +
          + + diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_updatefirewall.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_updatefirewall.html new file mode 100644 index 0000000..fe9baba --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_updatefirewall.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

          {% trans "Description:" %}

          +

          {% trans "You may update firewall details here." %}

          +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_updatepolicy.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_updatepolicy.html new file mode 100644 index 0000000..26c68d5 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_updatepolicy.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

          {% trans "Description:" %}

          +

          {% trans "You may update policy details here. Use 'Insert Rule' or 'Remove Rule' links instead to insert or remove a rule" %}

          +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_updaterule.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_updaterule.html new file mode 100644 index 0000000..b2b8104 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/_updaterule.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

          {% trans "Description:" %}

          +

          {% trans "You may update rule details here." %}

          +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/add_router_to_firewall.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/add_router_to_firewall.html new file mode 100644 index 0000000..739cde6 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/add_router_to_firewall.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add Router to Firewall" %}{% endblock %} + +{% block main %} + {% include 'project/firewalls/_add_router_to_firewall.html' %} +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/addfirewall.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/addfirewall.html new file mode 100644 index 0000000..0f004a2 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/addfirewall.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add New Firewall" %}{% endblock %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/addpolicy.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/addpolicy.html new file mode 100644 index 0000000..24b1dfa --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/addpolicy.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add New Policy" %}{% endblock %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/addrule.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/addrule.html new file mode 100644 index 0000000..4efa561 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/addrule.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add New Rule" %}{% endblock %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/details_tabs.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/details_tabs.html new file mode 100644 index 0000000..e07cac9 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/details_tabs.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Firewalls" %}{% endblock %} + +{% block main %} +
          +
          + {{ tab_group.render }} +
          +
          +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/insert_rule_to_policy.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/insert_rule_to_policy.html new file mode 100644 index 0000000..6e095ca --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/insert_rule_to_policy.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Insert Rule to Policy" %}{% endblock %} + +{% block main %} + {% include 'project/firewalls/_insert_rule_to_policy.html' %} +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/remove_router_from_firewall.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/remove_router_from_firewall.html new file mode 100644 index 0000000..f793d9a --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/remove_router_from_firewall.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Remove Router from Firewall" %}{% endblock %} + +{% block main %} + {% include 'project/firewalls/_remove_router_from_firewall.html' %} +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/remove_rule_from_policy.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/remove_rule_from_policy.html new file mode 100644 index 0000000..9af0c29 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/remove_rule_from_policy.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Remove Rule from Policy" %}{% endblock %} + +{% block main %} + {% include 'project/firewalls/_remove_rule_from_policy.html' %} +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/updatefirewall.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/updatefirewall.html new file mode 100644 index 0000000..f4211ca --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/updatefirewall.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Edit Firewall" %}{% endblock %} + +{% block main %} + {% include 'project/firewalls/_updatefirewall.html' %} +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/updatepolicy.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/updatepolicy.html new file mode 100644 index 0000000..e1cecf5 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/updatepolicy.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Edit Policy" %}{% endblock %} + +{% block main %} + {% include 'project/firewalls/_updatepolicy.html' %} +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/updaterule.html b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/updaterule.html new file mode 100644 index 0000000..ad23089 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/templates/firewalls/updaterule.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Edit Rule" %}{% endblock %} + +{% block main %} + {% include 'project/firewalls/_updaterule.html' %} +{% endblock %} diff --git a/neutron_fwaas_dashboard/dashboards/project/tests.py b/neutron_fwaas_dashboard/dashboards/project/tests.py new file mode 100644 index 0000000..4777f8e --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/tests.py @@ -0,0 +1,860 @@ +# Copyright 2013, Big Switch 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. + +from mox3.mox import IsA + +from django.core.urlresolvers import reverse +from django import http + +from openstack_dashboard import api + +from neutron_fwaas_dashboard.api import fwaas as api_fwaas +from neutron_fwaas_dashboard.test import helpers as test + + +class FirewallTests(test.TestCase): + class AttributeDict(dict): + def __getattr__(self, attr): + return self[attr] + + def __setattr__(self, attr, value): + self[attr] = value + + DASHBOARD = 'project' + INDEX_URL = reverse('horizon:%s:firewalls:index' % DASHBOARD) + + ADDRULE_PATH = 'horizon:%s:firewalls:addrule' % DASHBOARD + ADDPOLICY_PATH = 'horizon:%s:firewalls:addpolicy' % DASHBOARD + ADDFIREWALL_PATH = 'horizon:%s:firewalls:addfirewall' % DASHBOARD + + RULE_DETAIL_PATH = 'horizon:%s:firewalls:ruledetails' % DASHBOARD + POLICY_DETAIL_PATH = 'horizon:%s:firewalls:policydetails' % DASHBOARD + FIREWALL_DETAIL_PATH = 'horizon:%s:firewalls:firewalldetails' % DASHBOARD + + UPDATERULE_PATH = 'horizon:%s:firewalls:updaterule' % DASHBOARD + UPDATEPOLICY_PATH = 'horizon:%s:firewalls:updatepolicy' % DASHBOARD + UPDATEFIREWALL_PATH = 'horizon:%s:firewalls:updatefirewall' % DASHBOARD + + INSERTRULE_PATH = 'horizon:%s:firewalls:insertrule' % DASHBOARD + REMOVERULE_PATH = 'horizon:%s:firewalls:removerule' % DASHBOARD + + ADDROUTER_PATH = 'horizon:%s:firewalls:addrouter' % DASHBOARD + REMOVEROUTER_PATH = 'horizon:%s:firewalls:removerouter' % DASHBOARD + + def set_up_expect(self, fwaas_router_extension=True): + # retrieve rules + tenant_id = self.tenant.id + + api.neutron.is_extension_supported( + IsA(http.HttpRequest), 'fwaasrouterinsertion' + ).MultipleTimes().AndReturn(fwaas_router_extension) + + api_fwaas.rule_list_for_tenant( + IsA(http.HttpRequest), + tenant_id).AndReturn(self.fw_rules.list()) + + # retrieves policies + policies = self.fw_policies.list() + api_fwaas.policy_list_for_tenant( + IsA(http.HttpRequest), tenant_id).AndReturn(policies) + + # retrieves firewalls + firewalls = self.firewalls.list() + api_fwaas.firewall_list_for_tenant( + IsA(http.HttpRequest), tenant_id).AndReturn(firewalls) + + routers = self.routers.list() + api.neutron.router_list( + IsA(http.HttpRequest), tenant_id=tenant_id).AndReturn(routers) + api_fwaas.firewall_unassociated_routers_list( + IsA(http.HttpRequest), tenant_id).\ + MultipleTimes().AndReturn(routers) + + def set_up_expect_with_exception(self): + tenant_id = self.tenant.id + + api.neutron.is_extension_supported( + IsA(http.HttpRequest), 'fwaasrouterinsertion').AndReturn(True) + + api_fwaas.rule_list_for_tenant( + IsA(http.HttpRequest), + tenant_id).AndRaise(self.exceptions.neutron) + api_fwaas.policy_list_for_tenant( + IsA(http.HttpRequest), + tenant_id).AndRaise(self.exceptions.neutron) + api_fwaas.firewall_list_for_tenant( + IsA(http.HttpRequest), + tenant_id).AndRaise(self.exceptions.neutron) + + @test.create_stubs({api_fwaas: ('firewall_list_for_tenant', + 'policy_list_for_tenant', + 'rule_list_for_tenant', + 'firewall_unassociated_routers_list',), + api.neutron: ('is_extension_supported', + 'router_list',), }) + def test_index_firewalls(self): + self.set_up_expect() + + self.mox.ReplayAll() + + tenant_id = self.tenant.id + + res = self.client.get(self.INDEX_URL, tenant_id=tenant_id) + + self.assertTemplateUsed(res, '%s/firewalls/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['table'].data), + len(self.firewalls.list())) + + # TODO(absubram): Change test_index_firewalls for with and without + # router extensions. + + @test.create_stubs({api_fwaas: ('firewall_list_for_tenant', + 'policy_list_for_tenant', + 'rule_list_for_tenant', + 'firewall_unassociated_routers_list',), + api.neutron: ('is_extension_supported', + 'router_list',), }) + def test_index_policies(self): + self.set_up_expect() + + self.mox.ReplayAll() + + tenant_id = self.tenant.id + + res = self.client.get(self.INDEX_URL + '?tab=fwtabs__policies', + tenant_id=tenant_id) + + self.assertTemplateUsed(res, '%s/firewalls/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['policiestable_table'].data), + len(self.fw_policies.list())) + + @test.create_stubs({api_fwaas: ('firewall_list_for_tenant', + 'policy_list_for_tenant', + 'rule_list_for_tenant', + 'firewall_unassociated_routers_list',), + api.neutron: ('is_extension_supported', + 'router_list',), }) + def test_index_rules(self): + self.set_up_expect() + + self.mox.ReplayAll() + + tenant_id = self.tenant.id + + res = self.client.get(self.INDEX_URL + '?tab=fwtabs__rules', + tenant_id=tenant_id) + + self.assertTemplateUsed(res, '%s/firewalls/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['rulestable_table'].data), + len(self.fw_rules.list())) + + @test.create_stubs({api_fwaas: ('firewall_list_for_tenant', + 'policy_list_for_tenant', + 'rule_list_for_tenant'), + api.neutron: ('is_extension_supported',), }) + def test_index_exception_firewalls(self): + self.set_up_expect_with_exception() + + self.mox.ReplayAll() + + tenant_id = self.tenant.id + + res = self.client.get(self.INDEX_URL, tenant_id=tenant_id) + + self.assertTemplateUsed(res, + '%s/firewalls/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, + 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['table'].data), 0) + + @test.create_stubs({api_fwaas: ('firewall_list_for_tenant', + 'policy_list_for_tenant', + 'rule_list_for_tenant'), + api.neutron: ('is_extension_supported',), }) + def test_index_exception_policies(self): + self.set_up_expect_with_exception() + + self.mox.ReplayAll() + + tenant_id = self.tenant.id + + res = self.client.get(self.INDEX_URL + '?tab=fwtabs__policies', + tenant_id=tenant_id) + + self.assertTemplateUsed(res, + '%s/firewalls/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, + 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['policiestable_table'].data), 0) + + @test.create_stubs({api_fwaas: ('firewall_list_for_tenant', + 'policy_list_for_tenant', + 'rule_list_for_tenant'), + api.neutron: ('is_extension_supported',), }) + def test_index_exception_rules(self): + self.set_up_expect_with_exception() + + self.mox.ReplayAll() + + tenant_id = self.tenant.id + + res = self.client.get(self.INDEX_URL + '?tab=fwtabs__rules', + tenant_id=tenant_id) + + self.assertTemplateUsed(res, + '%s/firewalls/details_tabs.html' + % self.DASHBOARD) + self.assertTemplateUsed(res, + 'horizon/common/_detail_table.html') + self.assertEqual(len(res.context['rulestable_table'].data), 0) + + @test.create_stubs({api_fwaas: ('rule_create',), }) + def test_add_rule_post(self): + rule1 = self.fw_rules.first() + + form_data = {'name': rule1.name, + 'description': rule1.description, + 'protocol': rule1.protocol, + 'action': rule1.action, + 'source_ip_address': rule1.source_ip_address, + 'source_port': rule1.source_port, + 'destination_ip_address': rule1.destination_ip_address, + 'destination_port': rule1.destination_port, + 'shared': rule1.shared, + 'enabled': rule1.enabled, + 'ip_version': rule1.ip_version + } + + api_fwaas.rule_create( + IsA(http.HttpRequest), **form_data).AndReturn(rule1) + + self.mox.ReplayAll() + + res = self.client.post(reverse(self.ADDRULE_PATH), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('rule_create',), }) + def test_add_rule_post_src_None(self): + rule1 = self.fw_rules.first() + form_data = {'name': rule1.name, + 'description': rule1.description, + 'protocol': rule1.protocol, + 'action': rule1.action, + 'destination_ip_address': rule1.destination_ip_address, + 'destination_port': rule1.destination_port, + 'shared': rule1.shared, + 'enabled': rule1.enabled, + 'ip_version': rule1.ip_version + } + + api_fwaas.rule_create( + IsA(http.HttpRequest), **form_data).AndReturn(rule1) + + self.mox.ReplayAll() + + res = self.client.post(reverse(self.ADDRULE_PATH), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('rule_create',), }) + def test_add_rule_post_dest_None(self): + rule1 = self.fw_rules.first() + form_data = {'name': rule1.name, + 'description': rule1.description, + 'protocol': rule1.protocol, + 'action': rule1.action, + 'source_ip_address': rule1.source_ip_address, + 'source_port': rule1.source_port, + 'shared': rule1.shared, + 'enabled': rule1.enabled, + 'ip_version': rule1.ip_version + } + + api_fwaas.rule_create( + IsA(http.HttpRequest), **form_data).AndReturn(rule1) + + self.mox.ReplayAll() + + res = self.client.post(reverse(self.ADDRULE_PATH), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + def test_add_rule_post_with_error(self): + rule1 = self.fw_rules.first() + + form_data = {'name': rule1.name, + 'description': rule1.description, + 'protocol': 'abc', + 'action': 'pass', + 'source_ip_address': rule1.source_ip_address, + 'source_port': rule1.source_port, + 'destination_ip_address': rule1.destination_ip_address, + 'destination_port': rule1.destination_port, + 'shared': rule1.shared, + 'enabled': rule1.enabled, + 'ip_version': 6 + } + + self.mox.ReplayAll() + + res = self.client.post(reverse(self.ADDRULE_PATH), form_data) + + self.assertFormErrors(res, 3) + + @test.create_stubs({api_fwaas: ('policy_create', + 'rule_list_for_tenant'), }) + def test_add_policy_post(self): + policy = self.fw_policies.first() + rules = self.fw_rules.list() + tenant_id = self.tenant.id + form_data = {'name': policy.name, + 'description': policy.description, + 'firewall_rules': policy.firewall_rules, + 'shared': policy.shared, + 'audited': policy.audited + } + post_data = {'name': policy.name, + 'description': policy.description, + 'rule': policy.firewall_rules, + 'shared': policy.shared, + 'audited': policy.audited + } + + # NOTE: SelectRulesAction.populate_rule_choices() lists rule not + # associated with any policy. We need to ensure that rules specified + # in policy.firewall_rules in post_data (above) are not associated + # with any policy. Test data in neutron_data is data in a stable state, + # so we need to modify here. + for rule in rules: + if rule.id in policy.firewall_rules: + rule.firewall_policy_id = rule.policy = None + api_fwaas.rule_list_for_tenant( + IsA(http.HttpRequest), tenant_id).AndReturn(rules) + api_fwaas.policy_create( + IsA(http.HttpRequest), **form_data).AndReturn(policy) + + self.mox.ReplayAll() + + res = self.client.post(reverse(self.ADDPOLICY_PATH), post_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('policy_create', + 'rule_list_for_tenant'), }) + def test_add_policy_post_with_error(self): + policy = self.fw_policies.first() + rules = self.fw_rules.list() + tenant_id = self.tenant.id + form_data = {'description': policy.description, + 'firewall_rules': None, + 'shared': policy.shared, + 'audited': policy.audited + } + api_fwaas.rule_list_for_tenant( + IsA(http.HttpRequest), tenant_id).AndReturn(rules) + + self.mox.ReplayAll() + + res = self.client.post(reverse(self.ADDPOLICY_PATH), form_data) + + self.assertFormErrors(res, 1) + + def _test_add_firewall_post(self, router_extension=False): + firewall = self.firewalls.first() + policies = self.fw_policies.list() + tenant_id = self.tenant.id + if router_extension: + routers = self.routers.list() + firewalls = self.firewalls.list() + + form_data = {'name': firewall.name, + 'description': firewall.description, + 'firewall_policy_id': firewall.firewall_policy_id, + 'admin_state_up': firewall.admin_state_up + } + if router_extension: + form_data['router_ids'] = firewall.router_ids + api.neutron.router_list( + IsA(http.HttpRequest), tenant_id=tenant_id).AndReturn(routers) + api_fwaas.firewall_list_for_tenant( + IsA(http.HttpRequest), + tenant_id=tenant_id).AndReturn(firewalls) + + api.neutron.is_extension_supported( + IsA(http.HttpRequest), + 'fwaasrouterinsertion').AndReturn(router_extension) + api_fwaas.policy_list_for_tenant( + IsA(http.HttpRequest), tenant_id).AndReturn(policies) + api_fwaas.firewall_create( + IsA(http.HttpRequest), **form_data).AndReturn(firewall) + + self.mox.ReplayAll() + + res = self.client.post(reverse(self.ADDFIREWALL_PATH), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('firewall_create', + 'policy_list_for_tenant',), + api.neutron: ('is_extension_supported',), }) + def test_add_firewall_post(self): + self._test_add_firewall_post() + + # @test.create_stubs({api_fwaas: ('firewall_create', + # 'policy_list_for_tenant', + # 'firewall_list_for_tenant',), + # api.neutron: ('is_extension_supported', + # 'router_list'), }) + # def test_add_firewall_post_with_router_extension(self): + # self._test_add_firewall_post(router_extension=True) + # TODO(absubram): Fix test_add_firewall_post_with_router_extension + # It currently fails because views.py is not + # initializing the AddRouter workflow? + + @test.create_stubs({api_fwaas: ('firewall_create', + 'policy_list_for_tenant',), + api.neutron: ('is_extension_supported',), }) + def test_add_firewall_post_with_error(self): + firewall = self.firewalls.first() + policies = self.fw_policies.list() + tenant_id = self.tenant.id + form_data = {'name': firewall.name, + 'description': firewall.description, + 'firewall_policy_id': None, + 'admin_state_up': firewall.admin_state_up + } + api.neutron.is_extension_supported( + IsA(http.HttpRequest), + 'fwaasrouterinsertion').AndReturn(False) + api_fwaas.policy_list_for_tenant( + IsA(http.HttpRequest), tenant_id).AndReturn(policies) + + self.mox.ReplayAll() + + res = self.client.post(reverse(self.ADDFIREWALL_PATH), form_data) + + self.assertFormErrors(res, 1) + + @test.create_stubs({api_fwaas: ('rule_get',)}) + def test_update_rule_get(self): + rule = self.fw_rules.first() + + api_fwaas.rule_get(IsA(http.HttpRequest), rule.id).AndReturn(rule) + + self.mox.ReplayAll() + + res = self.client.get(reverse(self.UPDATERULE_PATH, args=(rule.id,))) + + self.assertTemplateUsed(res, 'project/firewalls/updaterule.html') + + @test.create_stubs({api_fwaas: ('rule_get', 'rule_update')}) + def test_update_rule_post(self): + rule = self.fw_rules.first() + + api_fwaas.rule_get(IsA(http.HttpRequest), rule.id).AndReturn(rule) + + data = {'name': 'new name', + 'description': 'new desc', + 'protocol': 'ICMP', + 'action': 'ALLOW', + 'shared': False, + 'enabled': True, + 'ip_version': rule.ip_version, + 'source_ip_address': rule.source_ip_address, + 'destination_ip_address': None, + 'source_port': None, + 'destination_port': rule.destination_port, + } + + api_fwaas.rule_update(IsA(http.HttpRequest), rule.id, **data)\ + .AndReturn(rule) + + self.mox.ReplayAll() + + form_data = data.copy() + form_data['destination_ip_address'] = '' + form_data['source_port'] = '' + + res = self.client.post( + reverse(self.UPDATERULE_PATH, args=(rule.id,)), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('rule_get', 'rule_update')}) + def test_update_protocol_any_rule_post(self): + # protocol any means protocol == None in neutron context. + rule = self.fw_rules.get(protocol=None) + + api_fwaas.rule_get(IsA(http.HttpRequest), rule.id).AndReturn(rule) + + data = {'name': 'new name', + 'description': 'new desc', + 'protocol': 'ICMP', + 'action': 'ALLOW', + 'shared': False, + 'enabled': True, + 'ip_version': rule.ip_version, + 'source_ip_address': rule.source_ip_address, + 'destination_ip_address': None, + 'source_port': None, + 'destination_port': rule.destination_port, + } + + api_fwaas.rule_update(IsA(http.HttpRequest), rule.id, **data)\ + .AndReturn(rule) + + self.mox.ReplayAll() + + form_data = data.copy() + form_data['destination_ip_address'] = '' + form_data['source_port'] = '' + + res = self.client.post( + reverse(self.UPDATERULE_PATH, args=(rule.id,)), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('rule_get', 'rule_update')}) + def test_update_rule_protocol_to_ANY_post(self): + rule = self.fw_rules.first() + + api_fwaas.rule_get(IsA(http.HttpRequest), rule.id).AndReturn(rule) + + data = {'name': 'new name', + 'description': 'new desc', + 'protocol': None, + 'action': 'ALLOW', + 'shared': False, + 'enabled': True, + 'ip_version': rule.ip_version, + 'source_ip_address': rule.source_ip_address, + 'destination_ip_address': None, + 'source_port': None, + 'destination_port': rule.destination_port, + } + api_fwaas.rule_update(IsA(http.HttpRequest), rule.id, **data)\ + .AndReturn(rule) + + self.mox.ReplayAll() + + form_data = data.copy() + form_data['destination_ip_address'] = '' + form_data['source_port'] = '' + form_data['protocol'] = 'ANY' + + res = self.client.post( + reverse(self.UPDATERULE_PATH, args=(rule.id,)), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('policy_get',)}) + def test_update_policy_get(self): + policy = self.fw_policies.first() + + api_fwaas.policy_get(IsA(http.HttpRequest), + policy.id).AndReturn(policy) + + self.mox.ReplayAll() + + res = self.client.get( + reverse(self.UPDATEPOLICY_PATH, args=(policy.id,))) + + self.assertTemplateUsed(res, 'project/firewalls/updatepolicy.html') + + @test.create_stubs({api_fwaas: ('policy_get', 'policy_update', + 'rule_list_for_tenant')}) + def test_update_policy_post(self): + policy = self.fw_policies.first() + + api_fwaas.policy_get(IsA(http.HttpRequest), + policy.id).AndReturn(policy) + + data = {'name': 'new name', + 'description': 'new desc', + 'shared': True, + 'audited': False + } + + api_fwaas.policy_update(IsA(http.HttpRequest), policy.id, **data)\ + .AndReturn(policy) + + self.mox.ReplayAll() + + res = self.client.post( + reverse(self.UPDATEPOLICY_PATH, args=(policy.id,)), data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('firewall_get', 'policy_list_for_tenant')}) + def test_update_firewall_get(self): + firewall = self.firewalls.first() + policies = self.fw_policies.list() + tenant_id = self.tenant.id + + api_fwaas.policy_list_for_tenant( + IsA(http.HttpRequest), tenant_id).AndReturn(policies) + + api_fwaas.firewall_get(IsA(http.HttpRequest), + firewall.id).AndReturn(firewall) + + self.mox.ReplayAll() + + res = self.client.get( + reverse(self.UPDATEFIREWALL_PATH, args=(firewall.id,))) + + self.assertTemplateUsed(res, 'project/firewalls/updatefirewall.html') + + @test.create_stubs({api_fwaas: ('firewall_get', 'policy_list_for_tenant', + 'firewall_update')}) + def test_update_firewall_post(self): + firewall = self.firewalls.first() + tenant_id = self.tenant.id + api_fwaas.firewall_get(IsA(http.HttpRequest), + firewall.id).AndReturn(firewall) + + data = {'name': 'new name', + 'description': 'new desc', + 'firewall_policy_id': firewall.firewall_policy_id, + 'admin_state_up': False + } + + policies = self.fw_policies.list() + api_fwaas.policy_list_for_tenant( + IsA(http.HttpRequest), tenant_id).AndReturn(policies) + + api_fwaas.firewall_update(IsA(http.HttpRequest), firewall.id, **data)\ + .AndReturn(firewall) + + self.mox.ReplayAll() + + res = self.client.post( + reverse(self.UPDATEFIREWALL_PATH, args=(firewall.id,)), data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('policy_get', 'policy_insert_rule', + 'rule_list_for_tenant', 'rule_get')}) + def test_policy_insert_rule(self): + policy = self.fw_policies.first() + tenant_id = self.tenant.id + rules = self.fw_rules.list() + + new_rule_id = rules[2].id + + data = {'firewall_rule_id': new_rule_id, + 'insert_before': rules[1].id, + 'insert_after': rules[0].id} + + api_fwaas.policy_get(IsA(http.HttpRequest), + policy.id).AndReturn(policy) + + policy.firewall_rules = [rules[0].id, + new_rule_id, + rules[1].id] + + api_fwaas.rule_list_for_tenant( + IsA(http.HttpRequest), tenant_id).AndReturn(rules) + api_fwaas.rule_get( + IsA(http.HttpRequest), new_rule_id).AndReturn(rules[2]) + api_fwaas.policy_insert_rule(IsA(http.HttpRequest), policy.id, **data)\ + .AndReturn(policy) + + self.mox.ReplayAll() + + res = self.client.post( + reverse(self.INSERTRULE_PATH, args=(policy.id,)), data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('policy_get', 'policy_remove_rule', + 'rule_list_for_tenant', 'rule_get')}) + def test_policy_remove_rule(self): + policy = self.fw_policies.first() + tenant_id = self.tenant.id + rules = self.fw_rules.list() + + remove_rule_id = policy.firewall_rules[0] + left_rule_id = policy.firewall_rules[1] + + data = {'firewall_rule_id': remove_rule_id} + + after_remove_policy_dict = {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'tenant_id': '1', + 'name': 'policy1', + 'description': 'policy description', + 'firewall_rules': [left_rule_id], + 'audited': True, + 'shared': True} + after_remove_policy = api_fwaas.Policy(after_remove_policy_dict) + + api_fwaas.policy_get(IsA(http.HttpRequest), + policy.id).AndReturn(policy) + api_fwaas.rule_list_for_tenant( + IsA(http.HttpRequest), tenant_id).AndReturn(rules) + api_fwaas.rule_get( + IsA(http.HttpRequest), remove_rule_id).AndReturn(rules[0]) + api_fwaas.policy_remove_rule(IsA(http.HttpRequest), policy.id, **data)\ + .AndReturn(after_remove_policy) + + self.mox.ReplayAll() + + res = self.client.post( + reverse(self.REMOVERULE_PATH, args=(policy.id,)), data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('firewall_get', + 'firewall_list_for_tenant', + 'firewall_update', + 'firewall_unassociated_routers_list')}) + def test_firewall_add_router(self): + tenant_id = self.tenant.id + firewall = self.firewalls.first() + routers = self.routers.list() + + existing_router_ids = firewall.router_ids + add_router_ids = [routers[1].id] + + form_data = {'router_ids': add_router_ids} + post_data = {'router_ids': add_router_ids + existing_router_ids} + + api_fwaas.firewall_get( + IsA(http.HttpRequest), firewall.id).AndReturn(firewall) + api_fwaas.firewall_unassociated_routers_list( + IsA(http.HttpRequest), tenant_id).AndReturn(routers) + + firewall.router_ids = [add_router_ids, existing_router_ids] + + api_fwaas.firewall_update( + IsA(http.HttpRequest), + firewall.id, **post_data).AndReturn(firewall) + + self.mox.ReplayAll() + + res = self.client.post( + reverse(self.ADDROUTER_PATH, args=(firewall.id,)), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('firewall_get', + 'firewall_update'), + api.neutron: ('router_list',), }) + def test_firewall_remove_router(self): + firewall = self.firewalls.first() + tenant_id = self.tenant.id + routers = self.routers.list() + existing_router_ids = firewall.router_ids + + form_data = {'router_ids': existing_router_ids} + + api_fwaas.firewall_get( + IsA(http.HttpRequest), firewall.id).AndReturn(firewall) + api.neutron.router_list( + IsA(http.HttpRequest), tenant_id=tenant_id).AndReturn(routers) + firewall.router_ids = [] + api_fwaas.firewall_update( + IsA(http.HttpRequest), + firewall.id, **form_data).AndReturn(firewall) + + self.mox.ReplayAll() + + res = self.client.post( + reverse(self.REMOVEROUTER_PATH, args=(firewall.id,)), form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, str(self.INDEX_URL)) + + @test.create_stubs({api_fwaas: ('rule_list_for_tenant', + 'rule_delete'), + api.neutron: ('is_extension_supported',)}) + def test_delete_rule(self): + api.neutron.is_extension_supported( + IsA(http.HttpRequest), 'fwaasrouterinsertion').AndReturn(True) + + rule = self.fw_rules.list()[2] + api_fwaas.rule_list_for_tenant( + IsA(http.HttpRequest), + self.tenant.id).AndReturn(self.fw_rules.list()) + api_fwaas.rule_delete(IsA(http.HttpRequest), rule.id) + self.mox.ReplayAll() + + form_data = {"action": "rulestable__deleterule__%s" % rule.id} + res = self.client.post(self.INDEX_URL, form_data) + + self.assertNoFormErrors(res) + + @test.create_stubs({api_fwaas: ('policy_list_for_tenant', + 'policy_delete'), + api.neutron: ('is_extension_supported',)}) + def test_delete_policy(self): + api.neutron.is_extension_supported( + IsA(http.HttpRequest), 'fwaasrouterinsertion').AndReturn(True) + + policy = self.fw_policies.first() + api_fwaas.policy_list_for_tenant( + IsA(http.HttpRequest), + self.tenant.id).AndReturn(self.fw_policies.list()) + api_fwaas.policy_delete(IsA(http.HttpRequest), policy.id) + self.mox.ReplayAll() + + form_data = {"action": "policiestable__deletepolicy__%s" % policy.id} + res = self.client.post(self.INDEX_URL, form_data) + + self.assertNoFormErrors(res) + + @test.create_stubs({api_fwaas: ('firewall_list_for_tenant', + 'firewall_delete'), + api.neutron: ('is_extension_supported', + 'router_list',)}) + def test_delete_firewall(self): + api.neutron.is_extension_supported( + IsA(http.HttpRequest), 'fwaasrouterinsertion' + ).MultipleTimes().AndReturn(True) + + routers = self.routers.list() + api.neutron.router_list( + IsA(http.HttpRequest), tenant_id=self.tenant.id).AndReturn(routers) + + fwl = self.firewalls.first() + api_fwaas.firewall_list_for_tenant( + IsA(http.HttpRequest), self.tenant.id).AndReturn([fwl]) + api_fwaas.firewall_delete(IsA(http.HttpRequest), fwl.id) + self.mox.ReplayAll() + + form_data = {"action": "firewallstable__deletefirewall__%s" % fwl.id} + res = self.client.post(self.INDEX_URL, form_data) + + self.assertNoFormErrors(res) diff --git a/neutron_fwaas_dashboard/dashboards/project/urls.py b/neutron_fwaas_dashboard/dashboards/project/urls.py new file mode 100644 index 0000000..c836863 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/urls.py @@ -0,0 +1,51 @@ +# Copyright 2013, Big Switch 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. + +from django.conf.urls import url + +from neutron_fwaas_dashboard.dashboards.project import views + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^\?tab=fwtabs__firewalls$', + views.IndexView.as_view(), name='firewalls'), + url(r'^\?tab=fwtabs__rules$', views.IndexView.as_view(), name='rules'), + url(r'^\?tab=fwtabs__policies$', + views.IndexView.as_view(), name='policies'), + url(r'^addrule$', views.AddRuleView.as_view(), name='addrule'), + url(r'^addpolicy$', views.AddPolicyView.as_view(), name='addpolicy'), + url(r'^addfirewall/(?P[^/]+)/$', + views.AddFirewallView.as_view(), name='addfirewall'), + url(r'^addfirewall$', views.AddFirewallView.as_view(), name='addfirewall'), + url(r'^insertrule/(?P[^/]+)/$', + views.InsertRuleToPolicyView.as_view(), name='insertrule'), + url(r'^removerule/(?P[^/]+)/$', + views.RemoveRuleFromPolicyView.as_view(), name='removerule'), + url(r'^updaterule/(?P[^/]+)/$', + views.UpdateRuleView.as_view(), name='updaterule'), + url(r'^updatepolicy/(?P[^/]+)/$', + views.UpdatePolicyView.as_view(), name='updatepolicy'), + url(r'^updatefirewall/(?P[^/]+)/$', + views.UpdateFirewallView.as_view(), name='updatefirewall'), + url(r'^rule/(?P[^/]+)/$', + views.RuleDetailsView.as_view(), name='ruledetails'), + url(r'^policy/(?P[^/]+)/$', + views.PolicyDetailsView.as_view(), name='policydetails'), + url(r'^addrouter/(?P[^/]+)/$', + views.AddRouterToFirewallView.as_view(), name='addrouter'), + url(r'^removerouter/(?P[^/]+)/$', + views.RemoveRouterFromFirewallView.as_view(), name='removerouter'), + url(r'^firewall/(?P[^/]+)/$', + views.FirewallDetailsView.as_view(), name='firewalldetails'), +] diff --git a/neutron_fwaas_dashboard/dashboards/project/views.py b/neutron_fwaas_dashboard/dashboards/project/views.py new file mode 100644 index 0000000..bf1f2a0 --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/views.py @@ -0,0 +1,439 @@ +# Copyright 2013, Big Switch 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. + +from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tabs +from horizon.utils import memoized +from horizon import workflows + +from openstack_dashboard import api + +from neutron_fwaas_dashboard.api import fwaas as api_fwaas +from neutron_fwaas_dashboard.dashboards.project import forms as fw_forms +from neutron_fwaas_dashboard.dashboards.project import tabs as fw_tabs +from neutron_fwaas_dashboard.dashboards.project \ + import workflows as fw_workflows + +AddRouterToFirewall = fw_forms.AddRouterToFirewall +InsertRuleToPolicy = fw_forms.InsertRuleToPolicy +RemoveRouterFromFirewall = fw_forms.RemoveRouterFromFirewall +RemoveRuleFromPolicy = fw_forms.RemoveRuleFromPolicy +UpdateFirewall = fw_forms.UpdateFirewall +UpdatePolicy = fw_forms.UpdatePolicy +UpdateRule = fw_forms.UpdateRule + +FirewallDetailsTabs = fw_tabs.FirewallDetailsTabs +FirewallTabs = fw_tabs.FirewallTabs +PolicyDetailsTabs = fw_tabs.PolicyDetailsTabs +RuleDetailsTabs = fw_tabs.RuleDetailsTabs + +AddFirewall = fw_workflows.AddFirewall +AddPolicy = fw_workflows.AddPolicy +AddRule = fw_workflows.AddRule + + +class IndexView(tabs.TabbedTableView): + tab_group_class = FirewallTabs + template_name = 'project/firewalls/details_tabs.html' + page_title = _("Firewalls") + + +class AddRuleView(workflows.WorkflowView): + workflow_class = AddRule + template_name = "project/firewalls/addrule.html" + page_title = _("Add New Rule") + + +class AddPolicyView(workflows.WorkflowView): + workflow_class = AddPolicy + template_name = "project/firewalls/addpolicy.html" + page_title = _("Add New Policy") + + +class AddFirewallView(workflows.WorkflowView): + workflow_class = AddFirewall + template_name = "project/firewalls/addfirewall.html" + page_title = _("Add New Firewall") + + def get_workflow(self): + if api.neutron.is_extension_supported(self.request, + 'fwaasrouterinsertion'): + AddFirewall.register(fw_workflows.SelectRoutersStep) + workflow = super(AddFirewallView, self).get_workflow() + return workflow + + +class RuleDetailsView(tabs.TabView): + tab_group_class = (RuleDetailsTabs) + template_name = 'horizon/common/_detail.html' + page_title = "{{ rule.name|default:rule.id }}" + failure_url = reverse_lazy('horizon:project:firewalls:index') + + def get_context_data(self, **kwargs): + context = super(RuleDetailsView, self).get_context_data(**kwargs) + rule = self.get_data() + table = fw_tabs.RulesTable(self.request) + breadcrumb = [ + (_("Rules"), reverse_lazy('horizon:project:firewalls:rules'))] + context["custom_breadcrumb"] = breadcrumb + context["rule"] = rule + context["url"] = self.failure_url + context["actions"] = table.render_row_actions(rule) + return context + + @memoized.memoized_method + def get_data(self): + try: + rule_id = self.kwargs['rule_id'] + rule = api_fwaas.rule_get(self.request, rule_id) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve rule details.'), + redirect=self.failure_url) + return rule + + def get_tabs(self, request, *args, **kwargs): + rule = self.get_data() + return self.tab_group_class(request, rule=rule, **kwargs) + + +class PolicyDetailsView(tabs.TabView): + tab_group_class = (PolicyDetailsTabs) + template_name = 'horizon/common/_detail.html' + page_title = "{{ policy.name|default:policy.id }}" + failure_url = reverse_lazy('horizon:project:firewalls:index') + + def get_context_data(self, **kwargs): + context = super(PolicyDetailsView, self).get_context_data(**kwargs) + policy = self.get_data() + table = fw_tabs.PoliciesTable(self.request) + breadcrumb = [ + (_("Policies"), + reverse_lazy('horizon:project:firewalls:policies'))] + context["custom_breadcrumb"] = breadcrumb + context["policy"] = policy + context["url"] = self.failure_url + context["actions"] = table.render_row_actions(policy) + return context + + @memoized.memoized_method + def get_data(self): + try: + policy_id = self.kwargs['policy_id'] + policy = api_fwaas.policy_get(self.request, policy_id) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve policy details.'), + redirect=self.failure_url) + return policy + + def get_tabs(self, request, *args, **kwargs): + policy = self.get_data() + return self.tab_group_class(request, policy=policy, **kwargs) + + +class FirewallDetailsView(tabs.TabView): + tab_group_class = (FirewallDetailsTabs) + template_name = 'horizon/common/_detail.html' + page_title = "{{ firewall.name|default:firewall.id }}" + failure_url = reverse_lazy('horizon:project:firewalls:index') + + def get_context_data(self, **kwargs): + context = super(FirewallDetailsView, self).get_context_data(**kwargs) + firewall = self.get_data() + routers = self.get_routers_data(firewall) + table = fw_tabs.FirewallsTable(self.request) + context["firewall"] = firewall + context["routers"] = routers + context["url"] = self.failure_url + context["actions"] = table.render_row_actions(firewall) + return context + + @memoized.memoized_method + def get_data(self): + try: + firewall_id = self.kwargs['firewall_id'] + firewall = api_fwaas.firewall_get(self.request, firewall_id) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve firewall details.'), + redirect=self.failure_url) + return firewall + + @memoized.memoized_method + def get_routers_data(self, firewall): + routers = [] + try: + if api.neutron.is_extension_supported(self.request, + 'fwaasrouterinsertion'): + tenant_id = self.request.user.tenant_id + tenant_routers = api.neutron.router_list(self.request, + tenant_id=tenant_id) + router_ids = firewall.get_dict()['router_ids'] + routers = [r for r in tenant_routers + if r['id'] in router_ids] + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve list of routers.'), ) + return routers + + def get_tabs(self, request, *args, **kwargs): + firewall = self.get_data() + return self.tab_group_class(request, firewall=firewall, **kwargs) + + +class UpdateRuleView(forms.ModalFormView): + form_class = UpdateRule + form_id = "update_rule_form" + template_name = "project/firewalls/updaterule.html" + context_object_name = 'rule' + submit_label = _("Save Changes") + submit_url = "horizon:project:firewalls:updaterule" + success_url = reverse_lazy("horizon:project:firewalls:index") + page_title = _("Edit Rule {{ name }}") + + def get_context_data(self, **kwargs): + context = super(UpdateRuleView, self).get_context_data(**kwargs) + context['rule_id'] = self.kwargs['rule_id'] + args = (self.kwargs['rule_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + obj = self._get_object() + if obj: + context['name'] = obj.name_or_id + return context + + @memoized.memoized_method + def _get_object(self, *args, **kwargs): + rule_id = self.kwargs['rule_id'] + try: + rule = api_fwaas.rule_get(self.request, rule_id) + return rule + except Exception: + redirect = self.success_url + msg = _('Unable to retrieve rule details.') + exceptions.handle(self.request, msg, redirect=redirect) + + def get_initial(self): + rule = self._get_object() + initial = rule.get_dict() + protocol = initial['protocol'] + initial['protocol'] = protocol.upper() if protocol else 'ANY' + initial['action'] = initial['action'].upper() + return initial + + +class UpdatePolicyView(forms.ModalFormView): + form_class = UpdatePolicy + form_id = "update_policy_form" + template_name = "project/firewalls/updatepolicy.html" + context_object_name = 'policy' + submit_label = _("Save Changes") + submit_url = "horizon:project:firewalls:updatepolicy" + success_url = reverse_lazy("horizon:project:firewalls:index") + page_title = _("Edit Policy {{ name }}") + + def get_context_data(self, **kwargs): + context = super(UpdatePolicyView, self).get_context_data(**kwargs) + context["policy_id"] = self.kwargs['policy_id'] + args = (self.kwargs['policy_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + obj = self._get_object() + if obj: + context['name'] = obj.name_or_id + return context + + @memoized.memoized_method + def _get_object(self, *args, **kwargs): + policy_id = self.kwargs['policy_id'] + try: + policy = api_fwaas.policy_get(self.request, policy_id) + return policy + except Exception: + redirect = self.success_url + msg = _('Unable to retrieve policy details.') + exceptions.handle(self.request, msg, redirect=redirect) + + def get_initial(self): + policy = self._get_object() + initial = policy.get_dict() + return initial + + +class UpdateFirewallView(forms.ModalFormView): + form_class = UpdateFirewall + form_id = "update_firewall_form" + template_name = "project/firewalls/updatefirewall.html" + context_object_name = 'firewall' + submit_label = _("Save Changes") + submit_url = "horizon:project:firewalls:updatefirewall" + success_url = reverse_lazy("horizon:project:firewalls:index") + page_title = _("Edit Firewall {{ name }}") + + def get_context_data(self, **kwargs): + context = super(UpdateFirewallView, self).get_context_data(**kwargs) + context["firewall_id"] = self.kwargs['firewall_id'] + args = (self.kwargs['firewall_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + obj = self._get_object() + if obj: + context['name'] = obj.name + return context + + @memoized.memoized_method + def _get_object(self, *args, **kwargs): + firewall_id = self.kwargs['firewall_id'] + try: + firewall = api_fwaas.firewall_get(self.request, + firewall_id) + return firewall + except Exception: + redirect = self.success_url + msg = _('Unable to retrieve firewall details.') + exceptions.handle(self.request, msg, redirect=redirect) + + def get_initial(self): + firewall = self._get_object() + initial = firewall.get_dict() + return initial + + +class InsertRuleToPolicyView(forms.ModalFormView): + form_class = InsertRuleToPolicy + form_id = "update_policy_form" + template_name = "project/firewalls/insert_rule_to_policy.html" + context_object_name = 'policy' + submit_url = "horizon:project:firewalls:insertrule" + submit_label = _("Save Changes") + success_url = reverse_lazy("horizon:project:firewalls:index") + page_title = _("Insert Rule to Policy") + + def get_context_data(self, **kwargs): + context = super(InsertRuleToPolicyView, + self).get_context_data(**kwargs) + context["policy_id"] = self.kwargs['policy_id'] + args = (self.kwargs['policy_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + obj = self._get_object() + if obj: + context['name'] = obj.name_or_id + return context + + @memoized.memoized_method + def _get_object(self, *args, **kwargs): + policy_id = self.kwargs['policy_id'] + try: + policy = api_fwaas.policy_get(self.request, policy_id) + return policy + except Exception: + redirect = self.success_url + msg = _('Unable to retrieve policy details.') + exceptions.handle(self.request, msg, redirect=redirect) + + def get_initial(self): + policy = self._get_object() + initial = policy.get_dict() + initial['policy_id'] = initial['id'] + return initial + + +class RemoveRuleFromPolicyView(forms.ModalFormView): + form_class = RemoveRuleFromPolicy + form_id = "update_policy_form" + template_name = "project/firewalls/remove_rule_from_policy.html" + context_object_name = 'policy' + submit_label = _("Save Changes") + submit_url = "horizon:project:firewalls:removerule" + success_url = reverse_lazy("horizon:project:firewalls:index") + page_title = _("Remove Rule from Policy") + + def get_context_data(self, **kwargs): + context = super(RemoveRuleFromPolicyView, + self).get_context_data(**kwargs) + context["policy_id"] = self.kwargs['policy_id'] + args = (self.kwargs['policy_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + obj = self._get_object() + if obj: + context['name'] = obj.name_or_id + return context + + @memoized.memoized_method + def _get_object(self, *args, **kwargs): + policy_id = self.kwargs['policy_id'] + try: + policy = api_fwaas.policy_get(self.request, policy_id) + return policy + except Exception: + redirect = self.success_url + msg = _('Unable to retrieve policy details.') + exceptions.handle(self.request, msg, redirect=redirect) + + def get_initial(self): + policy = self._get_object() + initial = policy.get_dict() + initial['policy_id'] = initial['id'] + return initial + + +class RouterCommonView(forms.ModalFormView): + form_id = "update_firewall_form" + context_object_name = 'firewall' + submit_label = _("Save Changes") + success_url = reverse_lazy("horizon:project:firewalls:index") + + def get_context_data(self, **kwargs): + context = super(RouterCommonView, + self).get_context_data(**kwargs) + context["firewall_id"] = self.kwargs['firewall_id'] + args = (self.kwargs['firewall_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + obj = self._get_object() + if obj: + context['name'] = obj.name_or_id + return context + + @memoized.memoized_method + def _get_object(self, *args, **kwargs): + firewall_id = self.kwargs['firewall_id'] + try: + firewall = api_fwaas.firewall_get(self.request, firewall_id) + return firewall + except Exception: + redirect = self.success_url + msg = _('Unable to retrieve firewall details.') + exceptions.handle(self.request, msg, redirect=redirect) + + def get_initial(self): + firewall = self._get_object() + initial = firewall.get_dict() + return initial + + +class AddRouterToFirewallView(RouterCommonView): + form_class = AddRouterToFirewall + template_name = "project/firewalls/add_router_to_firewall.html" + submit_url = "horizon:project:firewalls:addrouter" + page_title = _("Add Router to Firewall") + + +class RemoveRouterFromFirewallView(RouterCommonView): + form_class = RemoveRouterFromFirewall + template_name = "project/firewalls/remove_router_from_firewall.html" + submit_url = "horizon:project:firewalls:removerouter" + page_title = _("Remove Router from Firewall") diff --git a/neutron_fwaas_dashboard/dashboards/project/workflows.py b/neutron_fwaas_dashboard/dashboards/project/workflows.py new file mode 100644 index 0000000..082631e --- /dev/null +++ b/neutron_fwaas_dashboard/dashboards/project/workflows.py @@ -0,0 +1,409 @@ +# Copyright 2013, Big Switch 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. + +from django.utils.translation import ugettext_lazy as _ +import netaddr + +from horizon import exceptions +from horizon import forms +from horizon.utils import validators +from horizon import workflows + +from neutron_fwaas_dashboard.api import fwaas as api_fwaas + +port_validator = validators.validate_port_or_colon_separated_port_range + + +class AddRuleAction(workflows.Action): + name = forms.CharField( + max_length=80, + label=_("Name"), + required=False) + description = forms.CharField( + max_length=80, + label=_("Description"), + required=False) + protocol = forms.ThemableChoiceField( + label=_("Protocol"), + choices=[('tcp', _('TCP')), + ('udp', _('UDP')), + ('icmp', _('ICMP')), + ('any', _('ANY'))], + widget=forms.ThemableSelectWidget(attrs={ + 'class': 'switchable', + 'data-slug': 'protocol', + })) + action = forms.ThemableChoiceField( + label=_("Action"), + choices=[('allow', _('ALLOW')), + ('deny', _('DENY')), + ('reject', _('REJECT'))],) + source_ip_address = forms.IPField( + label=_("Source IP Address/Subnet"), + version=forms.IPv4 | forms.IPv6, + required=False, mask=True) + destination_ip_address = forms.IPField( + label=_("Destination IP Address/Subnet"), + version=forms.IPv4 | forms.IPv6, + required=False, mask=True) + source_port = forms.CharField( + max_length=80, + label=_("Source Port/Port Range"), + widget=forms.TextInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'protocol', + 'data-protocol-tcp': _("Source Port/Port Range"), + 'data-protocol-udp': _("Source Port/Port Range"), + }), + required=False, + validators=[port_validator]) + destination_port = forms.CharField( + max_length=80, + label=_("Destination Port/Port Range"), + widget=forms.TextInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'protocol', + 'data-protocol-tcp': _("Destination Port/Port Range"), + 'data-protocol-udp': _("Destination Port/Port Range"), + }), + required=False, + validators=[port_validator]) + ip_version = forms.ThemableChoiceField( + label=_("IP Version"), required=False, + choices=[('4', '4'), ('6', '6')]) + shared = forms.BooleanField( + label=_("Shared"), initial=False, required=False) + enabled = forms.BooleanField( + label=_("Enabled"), initial=True, required=False) + + def __init__(self, request, *args, **kwargs): + super(AddRuleAction, self).__init__(request, *args, **kwargs) + + def _check_ip_addr_and_ip_version(self, cleaned_data): + ip_version = int(str(cleaned_data.get('ip_version'))) + src_ip = cleaned_data.get('source_ip_address') + dst_ip = cleaned_data.get('destination_ip_address') + msg = _('Source/Destination Network Address and IP version ' + 'are inconsistent. Please make them consistent.') + if (src_ip and + netaddr.IPNetwork(src_ip).version != ip_version): + self._errors['ip_version'] = self.error_class([msg]) + + elif (dst_ip and + netaddr.IPNetwork(dst_ip).version != ip_version): + self._errors['ip_version'] = self.error_class([msg]) + + def clean(self): + cleaned_data = super(AddRuleAction, self).clean() + self._check_ip_addr_and_ip_version(cleaned_data) + + class Meta(object): + name = _("Rule") + permissions = ('openstack.services.network',) + help_text = _("Create a firewall rule.\n\n" + "A Firewall rule is an association of the following " + "attributes:\n\n" + "
        • IP Addresses: The addresses from/to which the " + "traffic filtration needs to be applied.
        • " + "
        • IP Version: The type of IP packets (IP V4/V6) " + "that needs to be filtered.
        • " + "
        • Protocol: Type of packets (UDP, ICMP, TCP, Any) " + "that needs to be checked.
        • " + "
        • Action: Action is the type of filtration " + "required, it can be Reject/Deny/Allow data " + "packets.
        • \n" + "The protocol and action fields are required, all " + "others are optional.") + + +class AddRuleStep(workflows.Step): + action_class = AddRuleAction + contributes = ("name", "description", "protocol", "action", + "source_ip_address", "source_port", + "destination_ip_address", "destination_port", + "enabled", "shared", "ip_version") + + def contribute(self, data, context): + context = super(AddRuleStep, self).contribute(data, context) + if data: + if context['protocol'] == 'any': + del context['protocol'] + for field in ['source_port', + 'destination_port', + 'source_ip_address', + 'destination_ip_address']: + if not context[field]: + del context[field] + return context + + +class AddRule(workflows.Workflow): + slug = "addrule" + name = _("Add Rule") + finalize_button_name = _("Add") + success_message = _('Added Rule "%s".') + failure_message = _('Unable to add Rule "%s".') + success_url = "horizon:project:firewalls:index" + # fwaas is designed to support a wide range of vendor + # firewalls. Considering the multitude of vendor firewall + # features in place today, firewall_rule definition can + # involve more complex configuration over time. Hence, + # a workflow instead of a single form is used for + # firewall_rule add to be ready for future extension. + default_steps = (AddRuleStep,) + + def format_status_message(self, message): + return message % self.context.get('name') + + def handle(self, request, context): + try: + api_fwaas.rule_create(request, **context) + return True + except Exception as e: + msg = self.format_status_message(self.failure_message) + str(e) + exceptions.handle(request, msg) + return False + + +class SelectRulesAction(workflows.Action): + rule = forms.MultipleChoiceField( + label=_("Rules"), + required=False, + widget=forms.ThemableCheckboxSelectMultiple(), + help_text=_("Create a policy with selected rules.")) + + class Meta(object): + name = _("Rules") + permissions = ('openstack.services.network',) + help_text = _("Select rules for your policy.") + + def populate_rule_choices(self, request, context): + try: + tenant_id = self.request.user.tenant_id + rules = api_fwaas.rule_list_for_tenant(request, tenant_id) + rules = sorted(rules, + key=lambda rule: rule.name_or_id) + rule_list = [(rule.id, rule.name_or_id) for rule in rules + if not rule.firewall_policy_id] + except Exception as e: + rule_list = [] + exceptions.handle(request, + _('Unable to retrieve rules (%(error)s).') % { + 'error': str(e)}) + return rule_list + + +class SelectRulesStep(workflows.Step): + action_class = SelectRulesAction + template_name = "project/firewalls/_update_rules.html" + contributes = ("firewall_rules",) + + def contribute(self, data, context): + if data: + rules = self.workflow.request.POST.getlist("rule") + if rules: + rules = [r for r in rules if r != ''] + context['firewall_rules'] = rules + return context + + +class SelectRoutersAction(workflows.Action): + router = forms.MultipleChoiceField( + label=_("Routers"), + required=False, + widget=forms.ThemableCheckboxSelectMultiple(), + help_text=_("Create a firewall with selected routers.")) + + class Meta(object): + name = _("Routers") + permissions = ('openstack.services.network',) + help_text = _("Select routers for your firewall.") + + def populate_router_choices(self, request, context): + try: + tenant_id = self.request.user.tenant_id + routers_list = api_fwaas.firewall_unassociated_routers_list( + request, tenant_id) + + except Exception as e: + routers_list = [] + exceptions.handle(request, + _('Unable to retrieve routers (%(error)s).') % { + 'error': str(e)}) + routers_list = [(router.id, router.name_or_id) + for router in routers_list] + return routers_list + + +class SelectRoutersStep(workflows.Step): + action_class = SelectRoutersAction + template_name = "project/firewalls/_update_routers.html" + contributes = ("router_ids", "all_routers_selected", + "Select No Routers") + + def contribute(self, data, context): + if data: + routers = self.workflow.request.POST.getlist("router") + if routers: + routers = [r for r in routers if r != ''] + context['router_ids'] = routers + else: + context['router_ids'] = [] + return context + + +class AddPolicyAction(workflows.Action): + name = forms.CharField(max_length=80, + label=_("Name")) + description = forms.CharField(max_length=80, + label=_("Description"), + required=False) + shared = forms.BooleanField(label=_("Shared"), + initial=False, + required=False) + audited = forms.BooleanField(label=_("Audited"), + initial=False, + required=False) + + def __init__(self, request, *args, **kwargs): + super(AddPolicyAction, self).__init__(request, *args, **kwargs) + + class Meta(object): + name = _("Policy") + permissions = ('openstack.services.network',) + help_text = _("Create a firewall policy with an ordered list " + "of firewall rules.\n\n" + "A firewall policy is an ordered collection of firewall " + "rules. So if the traffic matches the first rule, the " + "other rules are not executed. If the traffic does not " + "match the current rule, then the next rule is " + "executed. A firewall policy has the following " + "attributes:\n\n" + "
        • Shared: A firewall policy can be shared across " + "tenants. Thus it can also be made part of an audit " + "workflow wherein the firewall policy can be audited " + "by the relevant entity that is authorized.
        • " + "
        • Audited: When audited is set to True, it indicates " + "that the firewall policy has been audited. " + "Each time the firewall policy or the associated " + "firewall rules are changed, this attribute will be " + "set to False and will have to be explicitly set to " + "True through an update operation.
        • \n" + "The name field is required, all others are optional.") + + +class AddPolicyStep(workflows.Step): + action_class = AddPolicyAction + contributes = ("name", "description", "shared", "audited") + + def contribute(self, data, context): + context = super(AddPolicyStep, self).contribute(data, context) + if data: + return context + + +class AddPolicy(workflows.Workflow): + slug = "addpolicy" + name = _("Add Policy") + finalize_button_name = _("Add") + success_message = _('Added Policy "%s".') + failure_message = _('Unable to add Policy "%s".') + success_url = "horizon:project:firewalls:index" + default_steps = (AddPolicyStep, SelectRulesStep) + + def format_status_message(self, message): + return message % self.context.get('name') + + def handle(self, request, context): + try: + api_fwaas.policy_create(request, **context) + return True + except Exception as e: + msg = self.format_status_message(self.failure_message) + str(e) + exceptions.handle(request, msg) + return False + + +class AddFirewallAction(workflows.Action): + name = forms.CharField(max_length=80, + label=_("Name"), + required=False) + description = forms.CharField(max_length=80, + label=_("Description"), + required=False) + firewall_policy_id = forms.ThemableChoiceField(label=_("Policy")) + admin_state_up = forms.BooleanField(label=_("Enable Admin State"), + initial=True, + required=False) + + def __init__(self, request, *args, **kwargs): + super(AddFirewallAction, self).__init__(request, *args, **kwargs) + + firewall_policy_id_choices = [('', _("Select a Policy"))] + try: + tenant_id = self.request.user.tenant_id + policies = api_fwaas.policy_list_for_tenant(request, tenant_id) + policies = sorted(policies, key=lambda policy: policy.name) + except Exception as e: + exceptions.handle( + request, + _('Unable to retrieve policy list (%(error)s).') % { + 'error': str(e)}) + policies = [] + for p in policies: + firewall_policy_id_choices.append((p.id, p.name_or_id)) + self.fields['firewall_policy_id'].choices = firewall_policy_id_choices + + class Meta(object): + name = _("Firewall") + permissions = ('openstack.services.network',) + help_text = _("Create a firewall based on a policy.\n\n" + "A firewall represents a logical firewall resource that " + "a tenant can instantiate and manage. A firewall must " + "be associated with one policy, all other fields are " + "optional.") + + +class AddFirewallStep(workflows.Step): + action_class = AddFirewallAction + contributes = ("name", "firewall_policy_id", "description", + "admin_state_up") + + +class AddFirewall(workflows.Workflow): + slug = "addfirewall" + name = _("Add Firewall") + finalize_button_name = _("Add") + success_message = _('Added Firewall "%s".') + failure_message = _('Unable to add Firewall "%s".') + success_url = "horizon:project:firewalls:index" + # fwaas is designed to support a wide range of vendor + # firewalls. Considering the multitude of vendor firewall + # features in place today, firewall definition can + # involve more complex configuration over time. Hence, + # a workflow instead of a single form is used for + # firewall_rule add to be ready for future extension. + default_steps = (AddFirewallStep, ) + + def format_status_message(self, message): + return message % self.context.get('name') + + def handle(self, request, context): + try: + api_fwaas.firewall_create(request, **context) + return True + except Exception as e: + msg = self.format_status_message(self.failure_message) + str(e) + exceptions.handle(request, msg) + return False diff --git a/neutron_fwaas_dashboard/enabled/_1460_project_firewalls_panel.py b/neutron_fwaas_dashboard/enabled/_1460_project_firewalls_panel.py new file mode 100644 index 0000000..e43a124 --- /dev/null +++ b/neutron_fwaas_dashboard/enabled/_1460_project_firewalls_panel.py @@ -0,0 +1,9 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'firewalls' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'project' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'network' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'neutron_fwaas_dashboard.dashboards.project.panel.Firewall' diff --git a/neutron_fwaas_dashboard/enabled/__init__.py b/neutron_fwaas_dashboard/enabled/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron_fwaas_dashboard/locale/.placeholder b/neutron_fwaas_dashboard/locale/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/neutron_fwaas_dashboard/test/__init__.py b/neutron_fwaas_dashboard/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron_fwaas_dashboard/test/api_tests/__init__.py b/neutron_fwaas_dashboard/test/api_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron_fwaas_dashboard/test/api_tests/fwaas_tests.py b/neutron_fwaas_dashboard/test/api_tests/fwaas_tests.py new file mode 100644 index 0000000..0071b51 --- /dev/null +++ b/neutron_fwaas_dashboard/test/api_tests/fwaas_tests.py @@ -0,0 +1,427 @@ +# Copyright 2013, Big Switch 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. + +from neutronclient.v2_0.client import Client as neutronclient + +from neutron_fwaas_dashboard.api import fwaas as api_fwaas +from neutron_fwaas_dashboard.test import helpers as test + + +class FwaasApiTests(test.APITestCase): + @test.create_stubs({neutronclient: ('create_firewall_rule',)}) + def test_rule_create(self): + rule1 = self.fw_rules.first() + rule1_dict = self.api_fw_rules.first() + form_data = {'name': rule1.name, + 'description': rule1.description, + 'protocol': rule1.protocol, + 'action': rule1.action, + 'source_ip_address': rule1.source_ip_address, + 'source_port': rule1.source_port, + 'destination_ip_address': rule1.destination_ip_address, + 'destination_port': rule1.destination_port, + 'shared': rule1.shared, + 'enabled': rule1.enabled + } + form_dict = {'firewall_rule': form_data} + ret_dict = {'firewall_rule': rule1_dict} + neutronclient.create_firewall_rule(form_dict).AndReturn(ret_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.rule_create(self.request, **form_data) + self.assertIsInstance(ret_val, api_fwaas.Rule) + self.assertEqual(rule1.name, ret_val.name) + self.assertTrue(ret_val.id) + + def _assert_rule_return_value(self, ret_val, exp_rule): + self.assertIsInstance(ret_val, api_fwaas.Rule) + self.assertEqual(exp_rule.name, ret_val.name) + self.assertTrue(ret_val.id) + if exp_rule.policy: + self.assertEqual(exp_rule.firewall_policy_id, ret_val.policy.id) + self.assertEqual(exp_rule.policy.name, ret_val.policy.name) + else: + self.assertIsNone(ret_val.policy) + + @test.create_stubs({neutronclient: ('list_firewall_rules', + 'list_firewall_policies')}) + def test_rule_list(self): + exp_rules = self.fw_rules.list() + api_rules = {'firewall_rules': self.api_fw_rules.list()} + api_policies = {'firewall_policies': self.api_fw_policies.list()} + + neutronclient.list_firewall_rules().AndReturn(api_rules) + neutronclient.list_firewall_policies().AndReturn(api_policies) + self.mox.ReplayAll() + + ret_val = api_fwaas.rule_list(self.request) + for (v, d) in zip(ret_val, exp_rules): + self._assert_rule_return_value(v, d) + + @test.create_stubs({neutronclient: ('list_firewall_rules', + 'list_firewall_policies')}) + def test_rule_list_for_tenant(self): + tenant_id = self.request.user.project_id + exp_rules = self.fw_rules.list() + api_rules = {'firewall_rules': self.api_fw_rules.list()} + api_policies = {'firewall_policies': self.api_fw_policies.list()} + + neutronclient.list_firewall_rules( + tenant_id=tenant_id, + shared=False).AndReturn({'firewall_rules': []}) + neutronclient.list_firewall_rules(shared=True) \ + .AndReturn(api_rules) + neutronclient.list_firewall_policies().AndReturn(api_policies) + self.mox.ReplayAll() + + ret_val = api_fwaas.rule_list_for_tenant(self.request, tenant_id) + for (v, d) in zip(ret_val, exp_rules): + self._assert_rule_return_value(v, d) + + @test.create_stubs({neutronclient: ('show_firewall_rule', + 'show_firewall_policy')}) + def test_rule_get(self): + exp_rule = self.fw_rules.first() + ret_dict = {'firewall_rule': self.api_fw_rules.first()} + policy_dict = {'firewall_policy': self.api_fw_policies.first()} + + neutronclient.show_firewall_rule(exp_rule.id).AndReturn(ret_dict) + neutronclient.show_firewall_policy( + exp_rule.firewall_policy_id).AndReturn(policy_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.rule_get(self.request, exp_rule.id) + self._assert_rule_return_value(ret_val, exp_rule) + + @test.create_stubs({neutronclient: ('update_firewall_rule',)}) + def test_rule_update(self): + rule = self.fw_rules.first() + rule_dict = self.api_fw_rules.first() + + rule.name = 'new name' + rule.description = 'new desc' + rule.protocol = 'icmp' + rule.action = 'deny' + rule.shared = True + rule.enabled = False + + rule_dict['name'] = 'new name' + rule_dict['description'] = 'new desc' + rule_dict['protocol'] = 'icmp' + rule_dict['action'] = 'deny' + rule_dict['shared'] = True + rule_dict['enabled'] = False + + form_data = {'name': rule.name, + 'description': rule.description, + 'protocol': rule.protocol, + 'action': rule.action, + 'shared': rule.shared, + 'enabled': rule.enabled + } + form_dict = {'firewall_rule': form_data} + ret_dict = {'firewall_rule': rule_dict} + + neutronclient.update_firewall_rule( + rule.id, form_dict).AndReturn(ret_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.rule_update(self.request, + rule.id, **form_data) + self.assertIsInstance(ret_val, api_fwaas.Rule) + self.assertEqual(rule.name, ret_val.name) + self.assertTrue(ret_val.id) + + @test.create_stubs({neutronclient: ('create_firewall_policy', )}) + def test_policy_create(self): + policy1 = self.fw_policies.first() + policy1_dict = self.api_fw_policies.first() + + form_data = {'name': policy1.name, + 'description': policy1.description, + 'firewall_rules': policy1.firewall_rules, + 'shared': policy1.shared, + 'audited': policy1.audited + } + form_dict = {'firewall_policy': form_data} + ret_dict = {'firewall_policy': policy1_dict} + + neutronclient.create_firewall_policy(form_dict).AndReturn(ret_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.policy_create(self.request, **form_data) + self.assertIsInstance(ret_val, api_fwaas.Policy) + self.assertEqual(policy1.name, ret_val.name) + self.assertTrue(ret_val.id) + + def _assert_policy_return_value(self, ret_val, exp_policy): + self.assertIsInstance(ret_val, api_fwaas.Policy) + self.assertEqual(exp_policy.name, ret_val.name) + self.assertTrue(ret_val.id) + self.assertEqual(len(exp_policy.firewall_rules), len(ret_val.rules)) + self.assertEqual(len(exp_policy.firewall_rules), + len(ret_val.firewall_rules)) + for (r, exp_r) in zip(ret_val.rules, exp_policy.rules): + self.assertEqual(exp_r.id, r.id) + + @test.create_stubs({neutronclient: ('list_firewall_policies', + 'list_firewall_rules')}) + def test_policy_list(self): + exp_policies = self.fw_policies.list() + policies_dict = {'firewall_policies': self.api_fw_policies.list()} + rules_dict = {'firewall_rules': self.api_fw_rules.list()} + + neutronclient.list_firewall_policies().AndReturn(policies_dict) + neutronclient.list_firewall_rules().AndReturn(rules_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.policy_list(self.request) + for (v, d) in zip(ret_val, exp_policies): + self._assert_policy_return_value(v, d) + + @test.create_stubs({neutronclient: ('list_firewall_policies', + 'list_firewall_rules')}) + def test_policy_list_for_tenant(self): + tenant_id = self.request.user.project_id + exp_policies = self.fw_policies.list() + policies_dict = {'firewall_policies': self.api_fw_policies.list()} + rules_dict = {'firewall_rules': self.api_fw_rules.list()} + + neutronclient.list_firewall_policies( + tenant_id=tenant_id, + shared=False).AndReturn({'firewall_policies': []}) + neutronclient.list_firewall_policies( + shared=True).AndReturn(policies_dict) + neutronclient.list_firewall_rules().AndReturn(rules_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.policy_list_for_tenant(self.request, tenant_id) + for (v, d) in zip(ret_val, exp_policies): + self._assert_policy_return_value(v, d) + + @test.create_stubs({neutronclient: ('show_firewall_policy', + 'list_firewall_rules')}) + def test_policy_get(self): + exp_policy = self.fw_policies.first() + policy_dict = self.api_fw_policies.first() + # The first two rules are associated with the first policy. + api_rules = self.api_fw_rules.list()[:2] + + ret_dict = {'firewall_policy': policy_dict} + neutronclient.show_firewall_policy(exp_policy.id).AndReturn(ret_dict) + filters = {'firewall_policy_id': exp_policy.id} + ret_dict = {'firewall_rules': api_rules} + neutronclient.list_firewall_rules(**filters).AndReturn(ret_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.policy_get(self.request, exp_policy.id) + self._assert_policy_return_value(ret_val, exp_policy) + + @test.create_stubs({neutronclient: ('show_firewall_policy',)}) + def test_policy_get_no_rule(self): + # 2nd policy is not associated with any rules. + exp_policy = self.fw_policies.list()[1] + policy_dict = self.api_fw_policies.list()[1] + + ret_dict = {'firewall_policy': policy_dict} + neutronclient.show_firewall_policy(exp_policy.id).AndReturn(ret_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.policy_get(self.request, exp_policy.id) + self.assertIsInstance(ret_val, api_fwaas.Policy) + self.assertEqual(exp_policy.name, ret_val.name) + self.assertTrue(ret_val.id) + self.assertFalse(len(ret_val.rules)) + + @test.create_stubs({neutronclient: ('update_firewall_policy',)}) + def test_policy_update(self): + policy = self.fw_policies.first() + policy_dict = self.api_fw_policies.first() + + policy.name = 'new name' + policy.description = 'new desc' + policy.shared = True + policy.audited = False + + policy_dict['name'] = 'new name' + policy_dict['description'] = 'new desc' + policy_dict['shared'] = True + policy_dict['audited'] = False + + form_data = {'name': policy.name, + 'description': policy.description, + 'shared': policy.shared, + 'audited': policy.audited + } + + form_dict = {'firewall_policy': form_data} + ret_dict = {'firewall_policy': policy_dict} + + neutronclient.update_firewall_policy( + policy.id, form_dict).AndReturn(ret_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.policy_update(self.request, + policy.id, **form_data) + self.assertIsInstance(ret_val, api_fwaas.Policy) + self.assertEqual(policy.name, ret_val.name) + self.assertTrue(ret_val.id) + + @test.create_stubs({neutronclient: ('firewall_policy_insert_rule',)}) + def test_policy_insert_rule(self): + policy = self.fw_policies.first() + policy_dict = self.api_fw_policies.first() + + new_rule_id = 'h0881d38-c3eb-4fee-9763-12de3338041d' + policy.firewall_rules.append(new_rule_id) + policy_dict['firewall_rules'].append(new_rule_id) + + body = {'firewall_rule_id': new_rule_id, + 'insert_before': policy.firewall_rules[1], + 'insert_after': policy.firewall_rules[0]} + + neutronclient.firewall_policy_insert_rule( + policy.id, body).AndReturn(policy_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.policy_insert_rule(self.request, + policy.id, **body) + self.assertIn(new_rule_id, ret_val.firewall_rules) + + @test.create_stubs({neutronclient: ('firewall_policy_remove_rule',)}) + def test_policy_remove_rule(self): + policy = self.fw_policies.first() + policy_dict = self.api_fw_policies.first() + + remove_rule_id = policy.firewall_rules[0] + policy_dict['firewall_rules'].remove(remove_rule_id) + + body = {'firewall_rule_id': remove_rule_id} + + neutronclient.firewall_policy_remove_rule( + policy.id, body).AndReturn(policy_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.policy_remove_rule(self.request, + policy.id, **body) + self.assertNotIn(remove_rule_id, ret_val.firewall_rules) + + @test.create_stubs({neutronclient: ('create_firewall', )}) + def test_firewall_create(self): + firewall = self.firewalls.first() + firewall_dict = self.api_firewalls.first() + + form_data = {'name': firewall.name, + 'description': firewall.description, + 'firewall_policy_id': firewall.firewall_policy_id, + 'admin_state_up': firewall.admin_state_up + } + + form_dict = {'firewall': form_data} + ret_dict = {'firewall': firewall_dict} + neutronclient.create_firewall(form_dict).AndReturn(ret_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.firewall_create(self.request, **form_data) + self.assertIsInstance(ret_val, api_fwaas.Firewall) + self.assertEqual(firewall.name, ret_val.name) + self.assertTrue(ret_val.id) + + def _assert_firewall_return_value(self, ret_val, exp_firewall): + self.assertIsInstance(ret_val, api_fwaas.Firewall) + self.assertEqual(exp_firewall.name, ret_val.name) + self.assertTrue(ret_val.id) + self.assertEqual(exp_firewall.firewall_policy_id, ret_val.policy.id) + self.assertEqual(exp_firewall.policy.name, ret_val.policy.name) + + # TODO(absubram) : Add API tests for firewall_create with routers, + # add router to firewall and remove router from fw. + + @test.create_stubs({neutronclient: ('list_firewalls', + 'list_firewall_policies')}) + def test_firewall_list(self): + exp_firewalls = self.firewalls.list() + firewalls_dict = {'firewalls': self.api_firewalls.list()} + policies_dict = {'firewall_policies': self.api_fw_policies.list()} + + neutronclient.list_firewalls().AndReturn(firewalls_dict) + neutronclient.list_firewall_policies().AndReturn(policies_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.firewall_list(self.request) + for (v, d) in zip(ret_val, exp_firewalls): + self._assert_firewall_return_value(v, d) + + @test.create_stubs({neutronclient: ('list_firewalls', + 'list_firewall_policies')}) + def test_firewall_list_for_tenant(self): + tenant_id = self.request.user.project_id + exp_firewalls = self.firewalls.list() + firewalls_dict = {'firewalls': self.api_firewalls.list()} + policies_dict = {'firewall_policies': self.api_fw_policies.list()} + + neutronclient.list_firewalls(tenant_id=tenant_id) \ + .AndReturn(firewalls_dict) + neutronclient.list_firewall_policies().AndReturn(policies_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.firewall_list_for_tenant(self.request, tenant_id) + for (v, d) in zip(ret_val, exp_firewalls): + self._assert_firewall_return_value(v, d) + + @test.create_stubs({neutronclient: ('show_firewall', + 'show_firewall_policy')}) + def test_firewall_get(self): + exp_firewall = self.firewalls.first() + ret_dict = {'firewall': self.api_firewalls.first()} + policy_dict = {'firewall_policy': self.api_fw_policies.first()} + + neutronclient.show_firewall(exp_firewall.id).AndReturn(ret_dict) + neutronclient.show_firewall_policy( + exp_firewall.firewall_policy_id).AndReturn(policy_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.firewall_get(self.request, exp_firewall.id) + self._assert_firewall_return_value(ret_val, exp_firewall) + + @test.create_stubs({neutronclient: ('update_firewall',)}) + def test_firewall_update(self): + firewall = self.firewalls.first() + firewall_dict = self.api_firewalls.first() + + firewall.name = 'new name' + firewall.description = 'new desc' + firewall.admin_state_up = False + + firewall_dict['name'] = 'new name' + firewall_dict['description'] = 'new desc' + firewall_dict['admin_state_up'] = False + + form_data = {'name': firewall.name, + 'description': firewall.description, + 'admin_state_up': firewall.admin_state_up + } + + form_dict = {'firewall': form_data} + ret_dict = {'firewall': firewall_dict} + + neutronclient.update_firewall( + firewall.id, form_dict).AndReturn(ret_dict) + self.mox.ReplayAll() + + ret_val = api_fwaas.firewall_update(self.request, + firewall.id, **form_data) + self.assertIsInstance(ret_val, api_fwaas.Firewall) + self.assertEqual(firewall.name, ret_val.name) + self.assertTrue(ret_val.id) diff --git a/neutron_fwaas_dashboard/test/helpers.py b/neutron_fwaas_dashboard/test/helpers.py new file mode 100644 index 0000000..39b8e40 --- /dev/null +++ b/neutron_fwaas_dashboard/test/helpers.py @@ -0,0 +1,36 @@ +# 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 openstack_dashboard.test import helpers + +from neutron_fwaas_dashboard.test.test_data import utils + + +create_stubs = helpers.create_stubs + + +class TestDataLoaderMixin(object): + def _setup_test_data(self): + super(TestDataLoaderMixin, self)._setup_test_data() + utils.load_data(self) + + +class TestCase(TestDataLoaderMixin, helpers.TestCase): + pass + + +class BaseAdminViewTests(TestDataLoaderMixin, helpers.TestCase): + pass + + +class APITestCase(TestDataLoaderMixin, helpers.APITestCase): + pass diff --git a/neutron_fwaas_dashboard/test/settings.py b/neutron_fwaas_dashboard/test/settings.py new file mode 100644 index 0000000..dc59b48 --- /dev/null +++ b/neutron_fwaas_dashboard/test/settings.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. + +# Default to Horizons test settings to avoid any missing keys +from horizon.test.settings import * # noqa +from openstack_dashboard.test.settings import * # noqa + +# pop these keys to avoid log warnings about deprecation +# update_dashboards will populate them anyway +HORIZON_CONFIG.pop('dashboards', None) +HORIZON_CONFIG.pop('default_dashboard', None) + +# Update the dashboards with neutron_fwaas_dashboard +import neutron_fwaas_dashboard.enabled +import openstack_dashboard.enabled +from openstack_dashboard.utils import settings + +settings.update_dashboards( + [ + openstack_dashboard.enabled, + neutron_fwaas_dashboard.enabled, + ], + HORIZON_CONFIG, + INSTALLED_APPS +) + +# Ensure any duplicate apps are removed after the update_dashboards call +INSTALLED_APPS = list(set(INSTALLED_APPS)) + +# -------------------- +# Test-only settings +# -------------------- +# TEST_GLOBAL_MOCKS_ON_PANELS: defines what and how methods should be +# mocked globally for unit tests and Selenium tests. +# 'method' is required. 'return_value' and 'side_effect' +# are optional and passed to mock.patch(). +TEST_GLOBAL_MOCKS_ON_PANELS['firewalls'] = { + 'method': ('neutron_fwaas_dashboard.dashboards.project.panel.' + 'Firewall.can_access'), + 'return_value': True, +} diff --git a/neutron_fwaas_dashboard/test/test_data/__init__.py b/neutron_fwaas_dashboard/test/test_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron_fwaas_dashboard/test/test_data/fwaas_data.py b/neutron_fwaas_dashboard/test/test_data/fwaas_data.py new file mode 100644 index 0000000..a2e208b --- /dev/null +++ b/neutron_fwaas_dashboard/test/test_data/fwaas_data.py @@ -0,0 +1,163 @@ +# Copyright 2012 Nebula, 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 copy + +from openstack_dashboard.test.test_data import utils + +from neutron_fwaas_dashboard.api import fwaas + + +def data(TEST): + # Data returned by openstack_dashboard.api.neutron wrapper. + TEST.firewalls = utils.TestDataContainer() + TEST.fw_policies = utils.TestDataContainer() + TEST.fw_rules = utils.TestDataContainer() + + # Data return by neutronclient. + TEST.api_firewalls = utils.TestDataContainer() + TEST.api_fw_policies = utils.TestDataContainer() + TEST.api_fw_rules = utils.TestDataContainer() + + # 1st rule (used by 1st policy) + rule1_dict = {'id': 'f0881d38-c3eb-4fee-9763-12de3338041d', + 'tenant_id': '1', + 'name': 'rule1', + 'description': 'rule1 description', + 'protocol': 'tcp', + 'action': 'allow', + 'source_ip_address': '1.2.3.0/24', + 'source_port': '80', + 'destination_ip_address': '4.5.6.7/32', + 'destination_port': '1:65535', + 'firewall_policy_id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'position': 1, + 'shared': True, + 'enabled': True, + 'ip_version': '4'} + TEST.api_fw_rules.add(rule1_dict) + + rule1 = fwaas.Rule(copy.deepcopy(rule1_dict)) + # NOTE: rule1['policy'] is set below + TEST.fw_rules.add(rule1) + + # 2nd rule (used by 2nd policy; no name) + rule2_dict = {'id': 'c6298a93-850f-4f64-b78a-959fd4f1e5df', + 'tenant_id': '1', + 'name': '', + 'description': '', + 'protocol': 'udp', + 'action': 'deny', + 'source_ip_address': '1.2.3.0/24', + 'source_port': '80', + 'destination_ip_address': '4.5.6.7/32', + 'destination_port': '1:65535', + 'firewall_policy_id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'position': 2, + 'shared': True, + 'enabled': True, + 'ip_version': '6'} + TEST.api_fw_rules.add(rule2_dict) + + rule2 = fwaas.Rule(copy.deepcopy(rule2_dict)) + # NOTE: rule2['policy'] is set below + TEST.fw_rules.add(rule2) + + # 3rd rule (not used by any policy) + rule3_dict = {'id': 'h0881d38-c3eb-4fee-9763-12de3338041d', + 'tenant_id': '1', + 'name': 'rule3', + 'description': 'rule3 description', + 'protocol': None, + 'action': 'allow', + 'source_ip_address': '1.2.3.0/24', + 'source_port': '80', + 'destination_ip_address': '4.5.6.7/32', + 'destination_port': '1:65535', + 'firewall_policy_id': None, + 'position': None, + 'shared': True, + 'enabled': True, + 'ip_version': '4'} + TEST.api_fw_rules.add(rule3_dict) + + rule3 = fwaas.Rule(copy.deepcopy(rule3_dict)) + # rule3 is not associated with any rules + rule3._apidict['policy'] = None + TEST.fw_rules.add(rule3) + + # 1st policy (associated with 2 rules) + policy1_dict = {'id': 'abcdef-c3eb-4fee-9763-12de3338041e', + 'tenant_id': '1', + 'name': 'policy1', + 'description': 'policy with two rules', + 'firewall_rules': [rule1_dict['id'], rule2_dict['id']], + 'audited': True, + 'shared': True} + TEST.api_fw_policies.add(policy1_dict) + + policy1 = fwaas.Policy(copy.deepcopy(policy1_dict)) + policy1._apidict['rules'] = [rule1, rule2] + TEST.fw_policies.add(policy1) + + # Reverse relations (rule -> policy) + rule1._apidict['policy'] = policy1 + rule2._apidict['policy'] = policy1 + + # 2nd policy (associated with no rules; no name) + policy2_dict = {'id': 'cf50b331-787a-4623-825e-da794c918d6a', + 'tenant_id': '1', + 'name': '', + 'description': '', + 'firewall_rules': [], + 'audited': False, + 'shared': False} + TEST.api_fw_policies.add(policy2_dict) + + policy2 = fwaas.Policy(copy.deepcopy(policy2_dict)) + policy2._apidict['rules'] = [] + TEST.fw_policies.add(policy2) + + # 1st firewall + fw1_dict = {'id': '8913dde8-4915-4b90-8d3e-b95eeedb0d49', + 'tenant_id': '1', + 'firewall_policy_id': + 'abcdef-c3eb-4fee-9763-12de3338041e', + 'name': 'firewall1', + 'router_ids': [TEST.routers.first().id], + 'description': 'firewall description', + 'status': 'PENDING_CREATE', + 'admin_state_up': True} + TEST.api_firewalls.add(fw1_dict) + + fw1 = fwaas.Firewall(copy.deepcopy(fw1_dict)) + fw1._apidict['policy'] = policy1 + fw1._apidict['routers'] = [TEST.routers.first()] + TEST.firewalls.add(fw1) + + # 2nd firewall (no name) + fw2_dict = {'id': '1aa75150-415f-458e-bae5-5a362a4fb1f7', + 'tenant_id': '1', + 'firewall_policy_id': + 'abcdef-c3eb-4fee-9763-12de3338041e', + 'name': '', + 'router_ids': [], + 'description': '', + 'status': 'PENDING_CREATE', + 'admin_state_up': True} + TEST.api_firewalls.add(fw2_dict) + + fw2 = fwaas.Firewall(copy.deepcopy(fw2_dict)) + fw2._apidict['policy'] = policy1 + TEST.firewalls.add(fw2) diff --git a/neutron_fwaas_dashboard/test/test_data/utils.py b/neutron_fwaas_dashboard/test/test_data/utils.py new file mode 100644 index 0000000..c6b1dfd --- /dev/null +++ b/neutron_fwaas_dashboard/test/test_data/utils.py @@ -0,0 +1,28 @@ +# 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 openstack_dashboard.test.test_data import utils + + +def load_data(load_onto=None): + from neutron_fwaas_dashboard.test.test_data import fwaas_data + + # The order of these loaders matters, some depend on others. + loaders = ( + fwaas_data.data, + ) + if load_onto: + for data_func in loaders: + data_func(load_onto) + return load_onto + else: + return utils.TestData(*loaders) diff --git a/neutron_fwaas_dashboard/version.py b/neutron_fwaas_dashboard/version.py new file mode 100644 index 0000000..4b1eff1 --- /dev/null +++ b/neutron_fwaas_dashboard/version.py @@ -0,0 +1,15 @@ +# 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 pbr.version + +version_info = pbr.version.VersionInfo('neutron_fwaas_dashboard') diff --git a/releasenotes/notes/.placeholder b/releasenotes/notes/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/notes/split-out-from-horizon-4807f953d5dc0799.yaml b/releasenotes/notes/split-out-from-horizon-4807f953d5dc0799.yaml new file mode 100644 index 0000000..7899f89 --- /dev/null +++ b/releasenotes/notes/split-out-from-horizon-4807f953d5dc0799.yaml @@ -0,0 +1,10 @@ +--- +prelude: > + Neutron FWaaS support in the OpenStack Dashboard is now split out into + a separate python package. +features: + - | + Neutron FWaaS support in the OpenStack Dashboard is now split out into a + separate package ``neutron-fwaas-dashboard``. You need to install + ``neutron-fwaas-dashboard`` after upgrading the OpenStack Dashboard + to Pike release and add ``enabled`` file for Neutron FWaaS dashboard. diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 0000000..5969fd1 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,196 @@ +# -*- 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. + +# Glance 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 = [ + 'oslosphinx', + 'reno.sphinxext', +] + +# 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 = u'Neutron FWaaS Dashboard Release Notes' +copyright = u'2016, OpenStack Foundation' + +# 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. +# +from neutron_fwaas_dashboard.version import version_info +# The short X.Y version. +# The full version, including alpha/beta/rc tags. +release = version_info.version_string_with_vcs() +# The short X.Y version. +version = version_info.version_string() + +# 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 = 'default' + +# 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 = [] + +# 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 not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# 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 = 'neutronfwaasdashboardReleaseNotesdoc' + +# -- Options for Internationalization output ------------------------------ +locale_dirs = ['locale/'] diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 0000000..1856a7c --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,8 @@ +===================================== +Neutron FWaaS Dashboard Release Notes +===================================== + +.. toctree:: + :maxdepth: 1 + + unreleased diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 0000000..875030f --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================ +Current Series Release Notes +============================ + +.. release-notes:: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7abeca5 --- /dev/null +++ b/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. + +pbr!=2.1.0,>=2.0.0 # Apache-2.0 +Django<1.10,>=1.8 # BSD +python-neutronclient>=5.1.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..915fc26 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,32 @@ +[metadata] +name = neutron-fwaas-dashboard +summary = Neutron FWaaS Dashboard +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://docs.openstack.org/developer/trove/ +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + +[files] +packages = + neutron_fwaas_dashboard + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all-files = 1 +warning-is-error = 1 + +[upload_sphinx] +upload-dir = doc/build/html diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d74ff58 --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +# 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 FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr>=2.0.0'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..e44ae05 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,18 @@ +# 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. + +# Hacking should appear first in case something else depends on pep8 +hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 + +coverage!=4.4,>=4.0 # Apache-2.0 +django-nose>=1.4.4 # BSD +mock>=2.0 # BSD +mox3!=0.19.0,>=0.7.0 # Apache-2.0 +python-subunit>=0.0.18 # Apache-2.0/BSD +sphinx!=1.6.1,>=1.5.1 # BSD +oslosphinx>=4.7.0 # Apache-2.0 +testrepository>=0.0.18 # Apache-2.0/BSD +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=1.4.0 # MIT +reno>=1.8.0 # Apache-2.0 diff --git a/tools/tox_install.sh b/tools/tox_install.sh new file mode 100755 index 0000000..7fd2a59 --- /dev/null +++ b/tools/tox_install.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# Client constraint file contains this client version pin that is in conflict +# with installing the client from source. We should remove the version pin in +# the constraints file before applying it for from-source installation. +# The script also has a secondary purpose to install certain special +# dependencies directly from git. + +# Wrapper for pip install that always uses constraints. +function pip_install() { + pip install -c"$localfile" -U "$@" +} + +# Grab the library from git using either zuul-cloner or pip. The former is +# there to a take advantage of the setup done by the gate infrastructure +# and honour any/all Depends-On headers in the commit message +function install_from_git() { + ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner + # GIT_HOST=git.openstack.org + GIT_HOST=github.com + PROJ=$1 + EGG=$2 + + edit-constraints "$localfile" -- "$EGG" + if [ -x "$ZUUL_CLONER" ]; then + SRC_DIR="$VIRTUAL_ENV/src" + mkdir -p "$SRC_DIR" + cd "$SRC_DIR" >/dev/null + ZUUL_CACHE_DIR=${ZUUL_CACHE_DIR:-/opt/git} $ZUUL_CLONER \ + --branch "$BRANCH_NAME" \ + "git://$GIT_HOST" "$PROJ" + pip_install -e "$PROJ/." + cd - >/dev/null + else + SRC_DIR="$VIRTUAL_ENV/src/$PROJ" + git clone --depth 1 --branch $BRANCH_NAME https://$GIT_HOST/$PROJ $SRC_DIR + pip_install -e $SRC_DIR + fi +} + + + +CONSTRAINTS_FILE="$1" +shift 1 + +# This script will either complete with a return code of 0 or the return code +# of whatever failed. +set -e + +# NOTE(tonyb): Place this in the tox environment's log dir so it will get +# published to logs.openstack.org for easy debugging. +mkdir -p "$VIRTUAL_ENV/log/" +localfile="$VIRTUAL_ENV/log/upper-constraints.txt" + +if [[ "$CONSTRAINTS_FILE" != http* ]]; then + CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE" +fi +# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep +curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile" + +pip_install openstack-requirements + +# This is the main purpose of the script: Allow local installation of +# the current repo. It is listed in constraints file and thus any +# install will be constrained and we need to unconstrain it. +edit-constraints "$localfile" -- "$CLIENT_NAME" + +declare -a passthrough_args +while [ $# -gt 0 ] ; do + case "$1" in + # If we have any special os: deps then process them + os:*) + declare -a pkg_spec + IFS=: pkg_spec=($1) + install_from_git "${pkg_spec[1]}" "${pkg_spec[2]}" + ;; + # Otherwise just pass the other deps through to the constrained pip install + *) + passthrough_args+=("$1") + ;; + esac + shift 1 +done + +# If *only* had special args then then isn't any need to run pip. +if [ -n "$passthrough_args" ] ; then + pip_install "${passthrough_args[@]}" +fi diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6e37d85 --- /dev/null +++ b/tox.ini @@ -0,0 +1,63 @@ +[tox] +envlist = py34,py27,pep8 +minversion = 2.3.2 +skipsdist = True + +[testenv] +usedevelop = True +install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +setenv = VIRTUAL_ENV={envdir} + # BRANCH_NAME=master + BRANCH_NAME=drop-vpnaas + CLIENT_NAME=neutron-fwaas-dashboard + NOSE_WITH_OPENSTACK=1 + NOSE_OPENSTACK_COLOR=1 + NOSE_OPENSTACK_RED=0.05 + NOSE_OPENSTACK_YELLOW=0.025 + NOSE_OPENSTACK_SHOW_ELAPSED=1 +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + os:amotoki/horizon:horizon + # os:openstack/horizon:horizon +commands = {[unit_tests]commands} + +[unit_tests] +commands = python manage.py test {posargs} --settings=neutron_fwaas_dashboard.test.settings + +[testenv:pep8] +commands = + flake8 {posargs} + {envpython} {toxinidir}/manage.py extract_messages --module neutron_fwaas_dashboard --verbosity 0 --check-only + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = + coverage erase + coverage run {toxinidir}/manage.py test neutron_fwaas_dashboard --settings=neutron_fwaas_dashboard.test.settings {posargs} + coverage xml --omit '.tox/cover/*' -o 'cover/coverage.xml' + coverage html --omit '.tox/cover/*' -d 'cover/htmlcov' + +[testenv:py27dj110] +basepython = python2.7 +commands = + pip install django>=1.10,<1.11 + {[unit_tests]commands} + +[testenv:docs] +commands = python setup.py build_sphinx + +[testenv:releasenotes] +commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + +[flake8] +exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,node_modules +# Enable the following hacking rules which are disabled by default +# H203 Use assertIs(Not)None to check for None +# H904 Delay string interpolations at logging calls +enable-extensions=H203,H904 +max-complexity = 20 + +[hacking] +local-check-factory = horizon.hacking.checks.factory