From a9f26b81e28359e6ccacf95a97557ad3005adc5f Mon Sep 17 00:00:00 2001 From: ZhouHeng Date: Tue, 8 Feb 2022 06:18:55 +0000 Subject: [PATCH] revive neutron-fwaas project This reverts commit caae7b6a6f5e8944dbe359b6472be2507bbf5e12. Reason for revert: Many users still need L3 firewalls and Inspur team wants to maintain this project. Neutron drivers team discussed the topic of the maintenance of neutron-fwaas, and agreed to include neutron-fwaas again to Neutron stadium[1]. Some updates have been made: Remove use "autonested_transaction" method, see more [2] Replace "neutron_lib.callbacks.registry.notify" with "registry.publish" Replace rootwrap execution with privsep context execution. Ensure db Models and migration scripts are sync, set table firewall_group_port_associations_v2's two columns nullable=False [1] https://meetings.opendev.org/meetings/neutron_drivers/2022/neutron_drivers.2022-01-28-14.00.log.html#l-14 [2] https://review.opendev.org/c/openstack/neutron-lib/+/761728 Change-Id: I14f551c199d9badcf25b9e65c954c012326d27cd --- .coveragerc | 7 + .gitignore | 37 + .mailmap | 11 + .pylintrc | 130 ++ .stestr.conf | 3 + .zuul.yaml | 77 + CONTRIBUTING.rst | 4 + HACKING.rst | 7 + LICENSE | 176 ++ README.rst | 45 +- TESTING.rst | 12 + babel.cfg | 2 + bindep.txt | 10 + devstack/README.rst | 30 + devstack/lib/l2_agent | 16 + devstack/lib/l3_agent | 16 + devstack/plugin.sh | 145 ++ devstack/settings | 12 + doc/requirements.txt | 7 + doc/source/_static/.placeholder | 0 doc/source/conf.py | 311 +++ doc/source/configuration/fwaas_driver.rst | 6 + doc/source/configuration/index.rst | 41 + doc/source/configuration/neutron_fwaas.rst | 6 + doc/source/configuration/policy-sample.rst | 16 + doc/source/configuration/policy.rst | 9 + .../configuration/samples/fwaas_driver.rst | 8 + .../configuration/samples/neutron_fwaas.rst | 8 + doc/source/contributor/contributing.rst | 28 + doc/source/contributor/devstack.rst | 1 + doc/source/contributor/fwaas_v2.rst | 7 + doc/source/contributor/index.rst | 17 + doc/source/contributor/modules.rst | 19 + doc/source/index.rst | 27 + doc/source/install/index.rst | 39 + etc/README.txt | 9 + etc/neutron/rootwrap.d/fwaas-privsep.filters | 7 + etc/oslo-config-generator/fwaas_driver.ini | 5 + etc/oslo-config-generator/neutron_fwaas.conf | 6 + etc/oslo-policy-generator/policy.conf | 3 + lower-constraints.txt | 147 ++ neutron_fwaas/__init__.py | 24 + neutron_fwaas/_i18n.py | 32 + neutron_fwaas/cmd/__init__.py | 0 neutron_fwaas/cmd/upgrade_checks/__init__.py | 0 neutron_fwaas/cmd/upgrade_checks/checks.py | 40 + neutron_fwaas/cmd/v1_to_v2_db_migration.py | 138 ++ neutron_fwaas/common/__init__.py | 0 neutron_fwaas/common/exceptions.py | 24 + neutron_fwaas/common/fwaas_constants.py | 42 + neutron_fwaas/common/resources.py | 17 + neutron_fwaas/db/__init__.py | 0 neutron_fwaas/db/firewall/__init__.py | 0 neutron_fwaas/db/firewall/firewall_db.py | 89 + neutron_fwaas/db/firewall/v2/__init__.py | 0 .../db/firewall/v2/firewall_db_v2.py | 1101 +++++++++++ neutron_fwaas/db/migration/__init__.py | 0 .../db/migration/alembic_migrations/README | 1 + .../migration/alembic_migrations/__init__.py | 0 .../db/migration/alembic_migrations/env.py | 86 + .../alembic_migrations/script.py.mako | 36 + .../4202e3047e47_add_index_tenant_id.py | 35 + .../540142f314f4_fwaas_router_insertion.py | 62 + .../versions/796c68dffbb_cisco_csr_fwaas.py | 45 + .../alembic_migrations/versions/CONTRACT_HEAD | 1 + .../alembic_migrations/versions/EXPAND_HEAD | 1 + .../versions/kilo_release.py | 29 + .../liberty/contract/67c8e8d61d5_initial.py | 38 + .../expand/4b47ea298795_add_reject_rule.py | 47 + .../liberty/expand/c40fbb377ad_initial.py | 34 + .../contract/458aa42b14b_fw_table_alter.py | 49 + .../f83a0b2964d0_rename_tenant_to_project.py | 143 ++ .../expand/d6a12e637e28_neutron_fwaas_v2_0.py | 115 ++ ...shared_attribute_for_firewall_resources.py | 37 + ...43_create_default_firewall_groups_table.py | 67 + ..._uniq_firewallgroupportassociation0port.py | 71 + .../versions/start_neutron_fwaas.py | 30 + neutron_fwaas/db/models/__init__.py | 0 neutron_fwaas/db/models/head.py | 17 + neutron_fwaas/extensions/__init__.py | 0 neutron_fwaas/extensions/firewall_v2.py | 302 +++ neutron_fwaas/opts.py | 36 + neutron_fwaas/policies/__init__.py | 25 + neutron_fwaas/policies/base.py | 17 + neutron_fwaas/policies/firewall_group.py | 113 ++ neutron_fwaas/policies/firewall_policy.py | 113 ++ neutron_fwaas/policies/firewall_rule.py | 136 ++ neutron_fwaas/privileged/__init__.py | 29 + .../privileged/netfilter_log/__init__.py | 0 .../netfilter_log/libnetfilter_log.py | 331 ++++ neutron_fwaas/privileged/netlink_constants.py | 86 + neutron_fwaas/privileged/netlink_lib.py | 314 +++ neutron_fwaas/privileged/tests/__init__.py | 0 .../privileged/tests/functional/__init__.py | 0 .../privileged/tests/functional/dummy.py | 29 + .../privileged/tests/functional/utils.py | 39 + neutron_fwaas/privileged/utils.py | 58 + neutron_fwaas/services/__init__.py | 0 neutron_fwaas/services/firewall/__init__.py | 0 .../services/firewall/fwaas_plugin_v2.py | 429 ++++ .../firewall/service_drivers/__init__.py | 0 .../service_drivers/agents/__init__.py | 0 .../firewall/service_drivers/agents/agents.py | 376 ++++ .../agents/drivers/__init__.py | 0 .../agents/drivers/conntrack_base.py | 55 + .../agents/drivers/fwaas_base.py | 120 ++ .../agents/drivers/fwaas_base_v2.py | 97 + .../agents/drivers/linux/__init__.py | 0 .../agents/drivers/linux/iptables_fwaas_v2.py | 550 ++++++ .../agents/drivers/linux/l2/__init__.py | 0 .../agents/drivers/linux/l2/driver_base.py | 63 + .../agents/drivers/linux/l2/noop/__init__.py | 0 .../drivers/linux/l2/noop/noop_driver.py | 41 + .../linux/l2/openvswitch_firewall/__init__.py | 19 + .../l2/openvswitch_firewall/constants.py | 63 + .../l2/openvswitch_firewall/exceptions.py | 26 + .../linux/l2/openvswitch_firewall/firewall.py | 1017 ++++++++++ .../linux/l2/openvswitch_firewall/rules.py | 208 ++ .../agents/drivers/linux/legacy_conntrack.py | 234 +++ .../agents/drivers/linux/netlink_conntrack.py | 144 ++ .../agents/firewall_agent_api.py | 94 + .../agents/firewall_service.py | 44 + .../service_drivers/agents/l2/__init__.py | 0 .../service_drivers/agents/l2/fwaas_v2.py | 493 +++++ .../agents/l3reference/__init__.py | 0 .../l3reference/firewall_l3_agent_v2.py | 552 ++++++ .../firewall/service_drivers/driver_api.py | 532 +++++ neutron_fwaas/services/logapi/__init__.py | 0 .../services/logapi/agents/__init__.py | 0 .../logapi/agents/drivers/__init__.py | 0 .../agents/drivers/iptables/__init__.py | 0 .../logapi/agents/drivers/iptables/driver.py | 71 + .../logapi/agents/drivers/iptables/log.py | 521 +++++ .../services/logapi/agents/l3/__init__.py | 0 .../services/logapi/agents/l3/fwg_log.py | 41 + .../services/logapi/common/__init__.py | 0 .../services/logapi/common/fwg_callback.py | 61 + .../services/logapi/common/log_db_api.py | 228 +++ .../services/logapi/common/port_callback.py | 40 + neutron_fwaas/services/logapi/constants.py | 21 + neutron_fwaas/services/logapi/exceptions.py | 34 + neutron_fwaas/services/logapi/fwg_validate.py | 125 ++ neutron_fwaas/services/logapi/rpc/__init__.py | 0 .../services/logapi/rpc/log_server.py | 30 + neutron_fwaas/tests/__init__.py | 0 neutron_fwaas/tests/base.py | 21 + neutron_fwaas/tests/contrib/README | 3 + neutron_fwaas/tests/contrib/filters.template | 20 + .../tests/contrib/functional-testing.filters | 10 + neutron_fwaas/tests/contrib/gate_hook.sh | 29 + .../tests/contrib/gate_hook_tempest.sh | 38 + .../tests/contrib/hooks/api_extensions-base | 1 + .../tests/contrib/hooks/api_extensions-legacy | 1 + .../tests/contrib/hooks/api_extensions-v1 | 1 + .../tests/contrib/hooks/api_extensions-v2 | 1 + .../tests/contrib/hooks/iptables_verify | 4 + neutron_fwaas/tests/contrib/post_test_hook.sh | 50 + neutron_fwaas/tests/fullstack/README | 1 + neutron_fwaas/tests/fullstack/__init__.py | 16 + neutron_fwaas/tests/fullstack/base.py | 67 + .../tests/fullstack/resources/__init__.py | 0 .../tests/fullstack/resources/client.py | 247 +++ .../tests/fullstack/resources/config.py | 290 +++ .../tests/fullstack/resources/environment.py | 362 ++++ .../tests/fullstack/resources/machine.py | 168 ++ .../tests/fullstack/resources/process.py | 235 +++ .../tests/fullstack/test_l3_agent.py | 183 ++ neutron_fwaas/tests/fullstack/utils.py | 24 + neutron_fwaas/tests/functional/__init__.py | 0 neutron_fwaas/tests/functional/db/__init__.py | 0 .../tests/functional/db/test_migrations.py | 100 + .../tests/functional/privileged/__init__.py | 0 .../tests/functional/privileged/test_dummy.py | 24 + .../functional/privileged/test_netlink_lib.py | 188 ++ .../tests/functional/privileged/test_utils.py | 53 + .../tests/functional/services/__init__.py | 0 .../functional/services/logapi/__init__.py | 0 .../services/logapi/agents/__init__.py | 0 .../logapi/agents/drivers/__init__.py | 0 .../agents/drivers/iptables/__init__.py | 0 .../agents/drivers/iptables/test_log.py | 568 ++++++ neutron_fwaas/tests/unit/__init__.py | 0 neutron_fwaas/tests/unit/cmd/__init__.py | 0 .../tests/unit/cmd/upgrade_checks/__init__.py | 0 .../unit/cmd/upgrade_checks/test_checks.py | 46 + neutron_fwaas/tests/unit/db/__init__.py | 0 .../tests/unit/db/firewall/__init__.py | 0 .../tests/unit/db/firewall/v2/__init__.py | 0 .../db/firewall/v2/test_firewall_db_v2.py | 1739 +++++++++++++++++ .../tests/unit/privileged/__init__.py | 0 .../unit/privileged/netfilter_log/__init__.py | 0 .../netfilter_log/test_libnetfilter_log.py | 137 ++ .../tests/unit/privileged/test_netlink_lib.py | 329 ++++ .../tests/unit/privileged/test_utils.py | 79 + neutron_fwaas/tests/unit/services/__init__.py | 0 .../tests/unit/services/firewall/__init__.py | 0 .../firewall/service_drivers/__init__.py | 0 .../service_drivers/agents/__init__.py | 0 .../agents/drivers/__init__.py | 0 .../agents/drivers/linux/__init__.py | 0 .../agents/drivers/linux/l2/__init__.py | 0 .../agents/drivers/linux/l2/noop/__init__.py | 0 .../drivers/linux/l2/noop/test_noop_driver.py | 44 + .../linux/l2/openvswitch_firewall/__init__.py | 0 .../l2/openvswitch_firewall/test_firewall.py | 699 +++++++ .../l2/openvswitch_firewall/test_rules.py | 339 ++++ .../drivers/linux/test_iptables_fwaas_v2.py | 520 +++++ .../drivers/linux/test_legacy_conntrack.py | 177 ++ .../drivers/linux/test_netlink_conntrack.py | 240 +++ .../service_drivers/agents/l2/__init__.py | 0 .../service_drivers/agents/l2/fake_data.py | 153 ++ .../agents/l2/test_fwaas_v2.py | 775 ++++++++ .../agents/l3reference/__init__.py | 0 .../l3reference/test_firewall_l3_agent_v2.py | 613 ++++++ .../service_drivers/agents/test_agents.py | 661 +++++++ .../agents/test_firewall_agent_api.py | 97 + .../agents/test_firewall_service.py | 61 + .../service_drivers/test_driver_api.py | 298 +++ .../services/firewall/test_fwaas_plugin_v2.py | 738 +++++++ .../tests/unit/services/logapi/__init__.py | 0 .../unit/services/logapi/agents/__init__.py | 0 .../logapi/agents/drivers/__init__.py | 0 .../agents/drivers/iptables/__init__.py | 0 .../agents/drivers/iptables/test_driver.py | 53 + .../agents/drivers/iptables/test_log.py | 341 ++++ .../services/logapi/agents/l3/__init__.py | 0 .../services/logapi/agents/l3/test_fwg_log.py | 51 + .../tests/unit/services/logapi/base.py | 38 + .../unit/services/logapi/common/__init__.py | 0 .../logapi/common/test_fwg_callback.py | 225 +++ .../services/logapi/common/test_log_db_api.py | 329 ++++ .../logapi/common/test_port_callback.py | 198 ++ .../unit/services/logapi/rpc/__init__.py | 0 .../services/logapi/rpc/test_log_server.py | 56 + .../unit/services/logapi/test_fwg_validate.py | 157 ++ neutron_fwaas/version.py | 17 + playbooks/configure_functional_job.yaml | 4 + releasenotes/notes/.placeholder | 0 ...r-future-consumption-ffd537c1f82e2e01.yaml | 13 + ...fault-firewall-group-7e9faf1afca1df85.yaml | 14 + .../notes/bug-1702242-c917c832ac2fa4e1.yaml | 11 + .../notes/bug-1746404-493a66faac333403.yaml | 10 + .../notes/bug-1799358-360c6ab27a32e0ac.yaml | 7 + ...co-fwaas-driver-move-8f46325d13c93543.yaml | 11 + ...e-between-sg-and-fwg-1f77a755539a9463.yaml | 16 + ...nfig-file-generation-265c5256668a26bf.yaml | 7 + ...s-as-stadium-project-934d6acb3e824249.yaml | 14 + .../drop-python-2-7-73d3113c69d724c1.yaml | 5 + .../notes/enable-quotas-a3d0a21743bb1985.yaml | 20 + .../notes/fwaas-config-9c780ccfb0e7887f.yaml | 4 + .../fwaas-v2-logging-79cbaa43ff17f47f.yaml | 22 + .../notes/fwaas_v2-374471c215af0ca0.yaml | 18 + ...fwaas-driver-removal-8915271e5d4288cf.yaml | 7 + .../ovs-firewall-driver-c347ea0a560b7e38.yaml | 16 + .../remove_fwaas_v1-15c6e19484f46d1b.yaml | 8 + ...if_port_is_supported-639d0df705eb67f9.yaml | 8 + ...fwaas-driver-removal-f7aa304a4544134a.yaml | 7 + ...fwaas-driver-removal-e38e6ecde5105084.yaml | 7 + releasenotes/source/_static/.placeholder | 0 releasenotes/source/_templates/.placeholder | 0 releasenotes/source/conf.py | 279 +++ releasenotes/source/index.rst | 16 + releasenotes/source/liberty.rst | 6 + .../locale/en_GB/LC_MESSAGES/releasenotes.po | 473 +++++ .../locale/fr/LC_MESSAGES/releasenotes.po | 66 + releasenotes/source/mitaka.rst | 6 + releasenotes/source/newton.rst | 6 + releasenotes/source/ocata.rst | 6 + releasenotes/source/pike.rst | 6 + releasenotes/source/queens.rst | 6 + releasenotes/source/rocky.rst | 6 + releasenotes/source/stein.rst | 6 + releasenotes/source/unreleased.rst | 5 + requirements.txt | 28 + roles/configure_functional_tests/README.rst | 24 + .../defaults/main.yaml | 7 + .../tasks/main.yaml | 21 + setup.cfg | 75 + setup.py | 29 + test-requirements.txt | 22 + tools/check_unit_test_structure.sh | 53 + tools/clean.sh | 5 + tools/configure_for_func_testing.sh | 281 +++ tools/configure_for_fwaas_func_testing.sh | 34 + tools/deploy_rootwrap.sh | 64 + tools/generate_config_file_samples.sh | 28 + tox.ini | 212 ++ 287 files changed, 25781 insertions(+), 8 deletions(-) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .mailmap create mode 100644 .pylintrc create mode 100644 .stestr.conf create mode 100644 .zuul.yaml create mode 100644 CONTRIBUTING.rst create mode 100644 HACKING.rst create mode 100644 LICENSE create mode 100644 TESTING.rst create mode 100644 babel.cfg create mode 100644 bindep.txt create mode 100644 devstack/README.rst create mode 100644 devstack/lib/l2_agent create mode 100644 devstack/lib/l3_agent create mode 100755 devstack/plugin.sh create mode 100644 devstack/settings create mode 100644 doc/requirements.txt create mode 100644 doc/source/_static/.placeholder create mode 100644 doc/source/conf.py create mode 100644 doc/source/configuration/fwaas_driver.rst create mode 100644 doc/source/configuration/index.rst create mode 100644 doc/source/configuration/neutron_fwaas.rst create mode 100644 doc/source/configuration/policy-sample.rst create mode 100644 doc/source/configuration/policy.rst create mode 100644 doc/source/configuration/samples/fwaas_driver.rst create mode 100644 doc/source/configuration/samples/neutron_fwaas.rst create mode 100644 doc/source/contributor/contributing.rst create mode 100644 doc/source/contributor/devstack.rst create mode 100644 doc/source/contributor/fwaas_v2.rst create mode 100644 doc/source/contributor/index.rst create mode 100644 doc/source/contributor/modules.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/install/index.rst create mode 100644 etc/README.txt create mode 100644 etc/neutron/rootwrap.d/fwaas-privsep.filters create mode 100644 etc/oslo-config-generator/fwaas_driver.ini create mode 100644 etc/oslo-config-generator/neutron_fwaas.conf create mode 100644 etc/oslo-policy-generator/policy.conf create mode 100644 lower-constraints.txt create mode 100644 neutron_fwaas/__init__.py create mode 100644 neutron_fwaas/_i18n.py create mode 100644 neutron_fwaas/cmd/__init__.py create mode 100644 neutron_fwaas/cmd/upgrade_checks/__init__.py create mode 100644 neutron_fwaas/cmd/upgrade_checks/checks.py create mode 100644 neutron_fwaas/cmd/v1_to_v2_db_migration.py create mode 100644 neutron_fwaas/common/__init__.py create mode 100644 neutron_fwaas/common/exceptions.py create mode 100644 neutron_fwaas/common/fwaas_constants.py create mode 100644 neutron_fwaas/common/resources.py create mode 100644 neutron_fwaas/db/__init__.py create mode 100644 neutron_fwaas/db/firewall/__init__.py create mode 100644 neutron_fwaas/db/firewall/firewall_db.py create mode 100644 neutron_fwaas/db/firewall/v2/__init__.py create mode 100644 neutron_fwaas/db/firewall/v2/firewall_db_v2.py create mode 100644 neutron_fwaas/db/migration/__init__.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/README create mode 100644 neutron_fwaas/db/migration/alembic_migrations/__init__.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/env.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/script.py.mako create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/4202e3047e47_add_index_tenant_id.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/540142f314f4_fwaas_router_insertion.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/796c68dffbb_cisco_csr_fwaas.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/CONTRACT_HEAD create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/EXPAND_HEAD create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/kilo_release.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/liberty/contract/67c8e8d61d5_initial.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/liberty/expand/4b47ea298795_add_reject_rule.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/liberty/expand/c40fbb377ad_initial.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/mitaka/contract/458aa42b14b_fw_table_alter.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/newton/contract/f83a0b2964d0_rename_tenant_to_project.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/newton/expand/d6a12e637e28_neutron_fwaas_v2_0.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/pike/contract/fd38cd995cc0_shared_attribute_for_firewall_resources.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/queens/expand/876782258a43_create_default_firewall_groups_table.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/queens/expand/f24e0d5e5bff_uniq_firewallgroupportassociation0port.py create mode 100644 neutron_fwaas/db/migration/alembic_migrations/versions/start_neutron_fwaas.py create mode 100644 neutron_fwaas/db/models/__init__.py create mode 100644 neutron_fwaas/db/models/head.py create mode 100644 neutron_fwaas/extensions/__init__.py create mode 100644 neutron_fwaas/extensions/firewall_v2.py create mode 100644 neutron_fwaas/opts.py create mode 100644 neutron_fwaas/policies/__init__.py create mode 100644 neutron_fwaas/policies/base.py create mode 100644 neutron_fwaas/policies/firewall_group.py create mode 100644 neutron_fwaas/policies/firewall_policy.py create mode 100644 neutron_fwaas/policies/firewall_rule.py create mode 100644 neutron_fwaas/privileged/__init__.py create mode 100644 neutron_fwaas/privileged/netfilter_log/__init__.py create mode 100644 neutron_fwaas/privileged/netfilter_log/libnetfilter_log.py create mode 100644 neutron_fwaas/privileged/netlink_constants.py create mode 100644 neutron_fwaas/privileged/netlink_lib.py create mode 100644 neutron_fwaas/privileged/tests/__init__.py create mode 100644 neutron_fwaas/privileged/tests/functional/__init__.py create mode 100644 neutron_fwaas/privileged/tests/functional/dummy.py create mode 100644 neutron_fwaas/privileged/tests/functional/utils.py create mode 100644 neutron_fwaas/privileged/utils.py create mode 100644 neutron_fwaas/services/__init__.py create mode 100644 neutron_fwaas/services/firewall/__init__.py create mode 100644 neutron_fwaas/services/firewall/fwaas_plugin_v2.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/__init__.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/__init__.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/agents.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/__init__.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/conntrack_base.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/fwaas_base.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/fwaas_base_v2.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/__init__.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/iptables_fwaas_v2.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/__init__.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/driver_base.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/noop/__init__.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/noop/noop_driver.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/__init__.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/constants.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/exceptions.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/firewall.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/rules.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/legacy_conntrack.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/netlink_conntrack.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/firewall_agent_api.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/firewall_service.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/l2/__init__.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/l2/fwaas_v2.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/l3reference/__init__.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/agents/l3reference/firewall_l3_agent_v2.py create mode 100644 neutron_fwaas/services/firewall/service_drivers/driver_api.py create mode 100644 neutron_fwaas/services/logapi/__init__.py create mode 100644 neutron_fwaas/services/logapi/agents/__init__.py create mode 100644 neutron_fwaas/services/logapi/agents/drivers/__init__.py create mode 100644 neutron_fwaas/services/logapi/agents/drivers/iptables/__init__.py create mode 100644 neutron_fwaas/services/logapi/agents/drivers/iptables/driver.py create mode 100644 neutron_fwaas/services/logapi/agents/drivers/iptables/log.py create mode 100644 neutron_fwaas/services/logapi/agents/l3/__init__.py create mode 100644 neutron_fwaas/services/logapi/agents/l3/fwg_log.py create mode 100644 neutron_fwaas/services/logapi/common/__init__.py create mode 100644 neutron_fwaas/services/logapi/common/fwg_callback.py create mode 100644 neutron_fwaas/services/logapi/common/log_db_api.py create mode 100644 neutron_fwaas/services/logapi/common/port_callback.py create mode 100644 neutron_fwaas/services/logapi/constants.py create mode 100644 neutron_fwaas/services/logapi/exceptions.py create mode 100644 neutron_fwaas/services/logapi/fwg_validate.py create mode 100644 neutron_fwaas/services/logapi/rpc/__init__.py create mode 100644 neutron_fwaas/services/logapi/rpc/log_server.py create mode 100644 neutron_fwaas/tests/__init__.py create mode 100644 neutron_fwaas/tests/base.py create mode 100644 neutron_fwaas/tests/contrib/README create mode 100644 neutron_fwaas/tests/contrib/filters.template create mode 100644 neutron_fwaas/tests/contrib/functional-testing.filters create mode 100644 neutron_fwaas/tests/contrib/gate_hook.sh create mode 100755 neutron_fwaas/tests/contrib/gate_hook_tempest.sh create mode 100644 neutron_fwaas/tests/contrib/hooks/api_extensions-base create mode 100644 neutron_fwaas/tests/contrib/hooks/api_extensions-legacy create mode 100644 neutron_fwaas/tests/contrib/hooks/api_extensions-v1 create mode 100644 neutron_fwaas/tests/contrib/hooks/api_extensions-v2 create mode 100644 neutron_fwaas/tests/contrib/hooks/iptables_verify create mode 100644 neutron_fwaas/tests/contrib/post_test_hook.sh create mode 100644 neutron_fwaas/tests/fullstack/README create mode 100644 neutron_fwaas/tests/fullstack/__init__.py create mode 100644 neutron_fwaas/tests/fullstack/base.py create mode 100644 neutron_fwaas/tests/fullstack/resources/__init__.py create mode 100644 neutron_fwaas/tests/fullstack/resources/client.py create mode 100644 neutron_fwaas/tests/fullstack/resources/config.py create mode 100644 neutron_fwaas/tests/fullstack/resources/environment.py create mode 100644 neutron_fwaas/tests/fullstack/resources/machine.py create mode 100644 neutron_fwaas/tests/fullstack/resources/process.py create mode 100644 neutron_fwaas/tests/fullstack/test_l3_agent.py create mode 100644 neutron_fwaas/tests/fullstack/utils.py create mode 100644 neutron_fwaas/tests/functional/__init__.py create mode 100644 neutron_fwaas/tests/functional/db/__init__.py create mode 100644 neutron_fwaas/tests/functional/db/test_migrations.py create mode 100644 neutron_fwaas/tests/functional/privileged/__init__.py create mode 100644 neutron_fwaas/tests/functional/privileged/test_dummy.py create mode 100644 neutron_fwaas/tests/functional/privileged/test_netlink_lib.py create mode 100644 neutron_fwaas/tests/functional/privileged/test_utils.py create mode 100644 neutron_fwaas/tests/functional/services/__init__.py create mode 100644 neutron_fwaas/tests/functional/services/logapi/__init__.py create mode 100644 neutron_fwaas/tests/functional/services/logapi/agents/__init__.py create mode 100644 neutron_fwaas/tests/functional/services/logapi/agents/drivers/__init__.py create mode 100644 neutron_fwaas/tests/functional/services/logapi/agents/drivers/iptables/__init__.py create mode 100644 neutron_fwaas/tests/functional/services/logapi/agents/drivers/iptables/test_log.py create mode 100644 neutron_fwaas/tests/unit/__init__.py create mode 100644 neutron_fwaas/tests/unit/cmd/__init__.py create mode 100644 neutron_fwaas/tests/unit/cmd/upgrade_checks/__init__.py create mode 100644 neutron_fwaas/tests/unit/cmd/upgrade_checks/test_checks.py create mode 100644 neutron_fwaas/tests/unit/db/__init__.py create mode 100644 neutron_fwaas/tests/unit/db/firewall/__init__.py create mode 100644 neutron_fwaas/tests/unit/db/firewall/v2/__init__.py create mode 100644 neutron_fwaas/tests/unit/db/firewall/v2/test_firewall_db_v2.py create mode 100644 neutron_fwaas/tests/unit/privileged/__init__.py create mode 100644 neutron_fwaas/tests/unit/privileged/netfilter_log/__init__.py create mode 100644 neutron_fwaas/tests/unit/privileged/netfilter_log/test_libnetfilter_log.py create mode 100644 neutron_fwaas/tests/unit/privileged/test_netlink_lib.py create mode 100644 neutron_fwaas/tests/unit/privileged/test_utils.py create mode 100644 neutron_fwaas/tests/unit/services/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/noop/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/noop/test_noop_driver.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/test_firewall.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/test_rules.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_iptables_fwaas_v2.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_legacy_conntrack.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_netlink_conntrack.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/fake_data.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/test_fwaas_v2.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l3reference/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l3reference/test_firewall_l3_agent_v2.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_agents.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_firewall_agent_api.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_firewall_service.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/service_drivers/test_driver_api.py create mode 100644 neutron_fwaas/tests/unit/services/firewall/test_fwaas_plugin_v2.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/agents/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/agents/drivers/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/test_driver.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/test_log.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/agents/l3/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/agents/l3/test_fwg_log.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/base.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/common/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/common/test_fwg_callback.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/common/test_log_db_api.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/common/test_port_callback.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/rpc/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/rpc/test_log_server.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/test_fwg_validate.py create mode 100644 neutron_fwaas/version.py create mode 100644 playbooks/configure_functional_job.yaml create mode 100644 releasenotes/notes/.placeholder create mode 100644 releasenotes/notes/adding-new-tables-for-future-consumption-ffd537c1f82e2e01.yaml create mode 100644 releasenotes/notes/auto-association-default-firewall-group-7e9faf1afca1df85.yaml create mode 100644 releasenotes/notes/bug-1702242-c917c832ac2fa4e1.yaml create mode 100644 releasenotes/notes/bug-1746404-493a66faac333403.yaml create mode 100644 releasenotes/notes/bug-1799358-360c6ab27a32e0ac.yaml create mode 100644 releasenotes/notes/cisco-fwaas-driver-move-8f46325d13c93543.yaml create mode 100644 releasenotes/notes/coexistence-between-sg-and-fwg-1f77a755539a9463.yaml create mode 100644 releasenotes/notes/config-file-generation-265c5256668a26bf.yaml create mode 100644 releasenotes/notes/deprecate-neutron-fwaas-as-stadium-project-934d6acb3e824249.yaml create mode 100644 releasenotes/notes/drop-python-2-7-73d3113c69d724c1.yaml create mode 100644 releasenotes/notes/enable-quotas-a3d0a21743bb1985.yaml create mode 100644 releasenotes/notes/fwaas-config-9c780ccfb0e7887f.yaml create mode 100644 releasenotes/notes/fwaas-v2-logging-79cbaa43ff17f47f.yaml create mode 100644 releasenotes/notes/fwaas_v2-374471c215af0ca0.yaml create mode 100644 releasenotes/notes/mcafee-fwaas-driver-removal-8915271e5d4288cf.yaml create mode 100644 releasenotes/notes/ovs-firewall-driver-c347ea0a560b7e38.yaml create mode 100644 releasenotes/notes/remove_fwaas_v1-15c6e19484f46d1b.yaml create mode 100644 releasenotes/notes/validation_if_port_is_supported-639d0df705eb67f9.yaml create mode 100644 releasenotes/notes/varmour-fwaas-driver-removal-f7aa304a4544134a.yaml create mode 100644 releasenotes/notes/vyatta-fwaas-driver-removal-e38e6ecde5105084.yaml create mode 100644 releasenotes/source/_static/.placeholder create mode 100644 releasenotes/source/_templates/.placeholder create mode 100644 releasenotes/source/conf.py create mode 100644 releasenotes/source/index.rst create mode 100644 releasenotes/source/liberty.rst create mode 100644 releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po create mode 100644 releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po create mode 100644 releasenotes/source/mitaka.rst create mode 100644 releasenotes/source/newton.rst create mode 100644 releasenotes/source/ocata.rst create mode 100644 releasenotes/source/pike.rst create mode 100644 releasenotes/source/queens.rst create mode 100644 releasenotes/source/rocky.rst create mode 100644 releasenotes/source/stein.rst create mode 100644 releasenotes/source/unreleased.rst create mode 100644 requirements.txt create mode 100644 roles/configure_functional_tests/README.rst create mode 100644 roles/configure_functional_tests/defaults/main.yaml create mode 100644 roles/configure_functional_tests/tasks/main.yaml create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test-requirements.txt create mode 100755 tools/check_unit_test_structure.sh create mode 100755 tools/clean.sh create mode 100755 tools/configure_for_func_testing.sh create mode 100755 tools/configure_for_fwaas_func_testing.sh create mode 100755 tools/deploy_rootwrap.sh create mode 100755 tools/generate_config_file_samples.sh create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..158720083 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = neutron_fwaas +omit = neutron_fwaas/tests/* + +[report] +ignore_errors = True diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..15da71cf2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +AUTHORS +build/* +build-stamp +ChangeLog +cover/ +covhtml/ +dist/ +doc/build +doc/source/_static/config_samples/*.sample +doc/source/_static/*.policy.yaml.sample +doc/source/contributor/api/ +etc/*.sample +*.DS_Store +*.pyc +neutron.egg-info/ +neutron_fwaas.egg-info/ +neutron/vcsversion.py +neutron/versioninfo +pbr*.egg/ +run_tests.err.log +run_tests.log +setuptools*.egg/ +subunit.log +*.mo +*.sw? +*~ +/.* +!/.coveragerc +!/.gitignore +!/.gitreview +!/.mailmap +!/.pylintrc +!/.zuul.yaml +!/.stestr.conf + +# Files created by releasenotes build +releasenotes/build diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..f3e7e5e1a --- /dev/null +++ b/.mailmap @@ -0,0 +1,11 @@ +# Format is: +# +# +lawrancejing +Jiajun Liu +Zhongyue Luo +Kun Huang +Zhenguo Niu +Isaku Yamahata +Isaku Yamahata +Morgan Fainberg diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 000000000..0aa449044 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,130 @@ +# The format of this file isn't really documented; just use --generate-rcfile +[MASTER] +# Add to the black list. It should be a base name, not a +# path. You may set this option multiple times. +# +ignore=.git,tests + +[MESSAGES CONTROL] +# NOTE(gus): This is a long list. A number of these are important and +# should be re-enabled once the offending code is fixed (or marked +# with a local disable) +disable= +# "F" Fatal errors that prevent further processing + import-error, +# "I" Informational noise + locally-disabled, +# "E" Error for important programming issues (likely bugs) + access-member-before-definition, + bad-super-call, + maybe-no-member, + no-member, + no-method-argument, + no-self-argument, + not-callable, + no-value-for-parameter, + super-on-old-class, + too-few-format-args, +# "W" Warnings for stylistic problems or minor programming issues + abstract-method, + anomalous-backslash-in-string, + anomalous-unicode-escape-in-string, + arguments-differ, + attribute-defined-outside-init, + bad-builtin, + bad-indentation, + broad-except, + dangerous-default-value, + deprecated-lambda, + duplicate-key, + expression-not-assigned, + fixme, + global-statement, + global-variable-not-assigned, + logging-not-lazy, + no-init, + non-parent-init-called, + pointless-string-statement, + protected-access, + redefined-builtin, + redefined-outer-name, + redefine-in-handler, + signature-differs, + star-args, + super-init-not-called, + unnecessary-lambda, + unnecessary-pass, + unpacking-non-sequence, + unreachable, + unused-argument, + unused-import, + unused-variable, +# TODO(dougwig) - disable nonstandard-exception while we have neutron_lib shims + nonstandard-exception, +# "C" Coding convention violations + bad-continuation, + invalid-name, + missing-docstring, + old-style-class, + superfluous-parens, +# "R" Refactor recommendations + abstract-class-little-used, + abstract-class-not-used, + duplicate-code, + interface-not-implemented, + no-self-use, + too-few-public-methods, + too-many-ancestors, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-lines, + too-many-locals, + too-many-public-methods, + too-many-return-statements, + too-many-statements + +[BASIC] +# Variable names can be 1 to 31 characters long, with lowercase and underscores +variable-rgx=[a-z_][a-z0-9_]{0,30}$ + +# Argument names can be 2 to 31 characters long, with lowercase and underscores +argument-rgx=[a-z_][a-z0-9_]{1,30}$ + +# Method names should be at least 3 characters long +# and be lowercased with underscores +method-rgx=([a-z_][a-z0-9_]{2,}|setUp|tearDown)$ + +# Module names matching neutron-* are ok (files in bin/) +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+)|(neutron-[a-z0-9_-]+))$ + +# Don't require docstrings on tests. +no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$ + +[FORMAT] +# Maximum number of characters on a single line. +max-line-length=79 + +[VARIABLES] +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +# _ is used by our localization +additional-builtins=_ + +[CLASSES] +# List of interface methods to ignore, separated by a comma. +ignore-iface-methods= + +[IMPORTS] +# Deprecated modules which should not be used, separated by a comma +deprecated-modules= +# should use oslo_serialization.jsonutils + json + +[TYPECHECK] +# List of module names for which member attributes should not be checked +ignored-modules=six.moves,_MovedItems + +[REPORTS] +# Tells whether to display a full report or only the messages +reports=no diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 000000000..bd1baede4 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=${OS_TEST_PATH:-./neutron_fwaas/tests/unit} +top_dir=./ diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 000000000..7967ce463 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,77 @@ +- project: + templates: + - check-requirements + - openstack-cover-jobs-neutron + - openstack-lower-constraints-jobs-neutron + - openstack-python3-ussuri-jobs-neutron + - periodic-stable-jobs-neutron + - publish-openstack-docs-pti + - release-notes-jobs-python3 + check: + jobs: + - neutron-fwaas-functional + - neutron-fwaas-v2-dsvm-tempest-multinode: + voting: false + gate: + jobs: + - neutron-fwaas-functional + experimental: + jobs: + - neutron-fwaas-fullstack + +- job: + name: neutron-fwaas-functional + parent: neutron-functional + timeout: 2400 + pre-run: playbooks/configure_functional_job.yaml + vars: + project_name: neutron-fwaas + devstack_services: + INSTALL_OVN: false + +- job: + name: neutron-fwaas-fullstack + parent: neutron-fullstack + vars: + project_name: neutron-fwaas + +- job: + name: neutron-fwaas-v2-dsvm-tempest-multinode + parent: neutron-ovs-tempest-multinode-full + roles: + - zuul: openstack/devstack + required-projects: + - openstack/devstack-gate + - openstack/neutron + - openstack/neutron-fwaas + - openstack/neutron-tempest-plugin + - openstack/tempest + vars: + tox_envlist: all-plugin + tempest_test_regex: ^neutron_tempest_plugin\.fwaas + devstack_plugins: + neutron: https://opendev.org/openstack/neutron.git + neutron-fwaas: https://opendev.org/openstack/neutron-fwaas.git + neutron-tempest-plugin: https://opendev.org/openstack/neutron-tempest-plugin.git + devstack_services: + q-fwaas-v2: true + devstack_localrc: + NETWORK_API_EXTENSIONS: "agent,binding,dhcp_agent_scheduler,external-net,ext-gw-mode,extra_dhcp_opts,quotas,router,security-group,subnet_allocation,network-ip-availability,auto-allocated-topology,timestamp_core,tag,service-type,rbac-policies,standard-attr-description,pagination,sorting,project-id,fwaas_v2" + Q_AGENT: openvswitch + Q_ML2_TENANT_NETWORK_TYPE: vxlan + Q_ML2_PLUGIN_MECHANISM_DRIVERS: openvswitch + group-vars: + subnode: + devstack_services: + q-agt: true + devstack_localrc: + USE_PYTHON3: true + devstack_local_conf: + post-config: + # NOTE(slaweq): We can get rid of this hardcoded absolute path when + # devstack-tempest job will be switched to use lib/neutron instead of + # lib/neutron-legacy + "/$NEUTRON_CORE_PLUGIN_CONF": + ovs: + tunnel_bridge: br-tun + bridge_mappings: public:br-ex diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..7d446b44a --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,4 @@ +Please see the Neutron CONTRIBUTING.rst file for how to contribute to +neutron-fwaas: + +`Neutron CONTRIBUTING.rst `_ diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 000000000..6ad86a5ca --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,7 @@ +Neutron FWaaS Style Commandments +================================ + +Please see the Neutron HACKING.rst file for style commandments for +neutron-fwaas: + +`Neutron HACKING.rst `_ diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..68c771a09 --- /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 index 4ee2c5f13..11837ca65 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,39 @@ -This project is no longer maintained. +======================== +Team and repository tags +======================== -The contents of this repository are still available in the Git -source code management system. To see the contents of this -repository before it reached its end of life, please check out the -previous commit with "git checkout HEAD^1". +.. image:: https://governance.openstack.org/tc/badges/neutron-fwaas.svg + :target: https://governance.openstack.org/tc/reference/tags/index.html + +.. Change things from this point on + +.. warning:: + Due to lack of maintainers this project is now deprecated in the Neutron + stadium and will be removed from stadium in ``W`` cycle. + If You want to step in and be maintainer of this project to keep it in the + Neutron stadium, please contact the ``neutron team`` via + openstack-discuss@lists.openstack.org or IRC channel #openstack-neutron + @freenode. + +Welcome! +======== + +This package contains the code for the Neutron Firewall as a Service +(FWaaS) service. This package requires Neutron to run. + +External Resources: +=================== + +The homepage for Neutron is: https://launchpad.net/neutron. Use this +site for asking for help, and filing bugs. We use a single Launchpad +page for all Neutron projects. + +Code is available on git.openstack.org at: +. + +Please refer to Neutron documentation for more information: +`Neutron README.rst `_ + +Get release notes: +`Neutron FWaaS Release Notes `_ -For any further questions, please email -openstack-discuss@lists.openstack.org or join #openstack-dev on -OFTC. diff --git a/TESTING.rst b/TESTING.rst new file mode 100644 index 000000000..9a9fc2f33 --- /dev/null +++ b/TESTING.rst @@ -0,0 +1,12 @@ +Testing Neutron FWaaS +===================== + +Please see the TESTING.rst file for the Neutron project itself. This will have +the latest up to date instructions for how to test Neutron, and will +be applicable to neutron-fwaas as well: + +`Neutron TESTING.rst `_ + +For instructions on how to use FWaaS with devstack, look at: + +`Neutron-FWaaS DevStack `_ diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 000000000..15cd6cb76 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,2 @@ +[python: **.py] + diff --git a/bindep.txt b/bindep.txt new file mode 100644 index 000000000..86fab166e --- /dev/null +++ b/bindep.txt @@ -0,0 +1,10 @@ +# This file contains runtime (non-python) dependencies +# More info at: http://docs.openstack.org/infra/bindep/readme.html + +# MySQL and PostgreSQL databases since some jobs are set up in +# OpenStack infra that need these like +libpq-dev [test] + +# Packages required e.g. in functional tests +libnetfilter-log1 [platform:dpkg platform:suse] +libnetfilter-log [platform:rpm !platform:suse] diff --git a/devstack/README.rst b/devstack/README.rst new file mode 100644 index 000000000..3d76f196b --- /dev/null +++ b/devstack/README.rst @@ -0,0 +1,30 @@ +========================= +neutron-fwaas in DevStack +========================= + +This is setup as a DevStack plugin. For more information on DevStack plugins, +see the `DevStack Plugins documentation +`_. + +Please note that the old 'q-fwaas' keyword still exists, You can specify +enable_service q-fwaas or enable_service q-fwaas-v2 in local.conf + +How to run FWaaS V2 in DevStack +=============================== + +Add the following to the localrc section of your local.conf to configure +FWaaS v2. + +.. code-block:: ini + + [[local|localrc]] + enable_plugin neutron-fwaas https://git.openstack.org/openstack/neutron-fwaas + +To check a specific patchset that is currently under development, use a form +like the below example, which is checking out change 214350 patch set 14 for +testing. + +.. code-block:: ini + + [[local|localrc]] + enable_plugin neutron-fwaas https://review.openstack.org/p/openstack/neutron-fwaas refs/changes/50/214350/14 diff --git a/devstack/lib/l2_agent b/devstack/lib/l2_agent new file mode 100644 index 000000000..1f1ca9d02 --- /dev/null +++ b/devstack/lib/l2_agent @@ -0,0 +1,16 @@ +# This file was shamelessly stolen from the neutron repository here: +# https://opendev.org/openstack/neutron/src/branch/master/devstack/lib/l2_agent + +function plugin_agent_add_l2_agent_extension { + local l2_agent_extension=$1 + if [[ -z "$L2_AGENT_EXTENSIONS" ]]; then + L2_AGENT_EXTENSIONS=$l2_agent_extension + elif [[ ! ,${L2_AGENT_EXTENSIONS}, =~ ,${l2_agent_extension}, ]]; then + L2_AGENT_EXTENSIONS+=",$l2_agent_extension" + fi +} + + +function configure_l2_agent { + iniset /$Q_PLUGIN_CONF_FILE agent extensions "$L2_AGENT_EXTENSIONS" +} diff --git a/devstack/lib/l3_agent b/devstack/lib/l3_agent new file mode 100644 index 000000000..d3541d315 --- /dev/null +++ b/devstack/lib/l3_agent @@ -0,0 +1,16 @@ +# This file is completely based on one in the neutron repository here: +# https://opendev.org/openstack/neutron/src/branch/master/devstack/lib/l2_agent + +function plugin_agent_add_l3_agent_extension { + local l3_agent_extension=$1 + if [[ -z "$L3_AGENT_EXTENSIONS" ]]; then + L3_AGENT_EXTENSIONS=$l3_agent_extension + elif [[ ! ,${L3_AGENT_EXTENSIONS}, =~ ,${l3_agent_extension}, ]]; then + L3_AGENT_EXTENSIONS+=",$l3_agent_extension" + fi +} + + +function configure_l3_agent { + iniset $Q_L3_CONF_FILE agent extensions "$L3_AGENT_EXTENSIONS" +} diff --git a/devstack/plugin.sh b/devstack/plugin.sh new file mode 100755 index 000000000..138659d85 --- /dev/null +++ b/devstack/plugin.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +# 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. + +# Dependencies: +# +# ``functions`` file +# ``DEST`` must be defined + +# Save trace setting +XTRACE=$(set +o | grep xtrace) +set +o xtrace + +# Source in L2 and L3 agent extension management +LIBDIR=$DEST/neutron-fwaas/devstack/lib +source $LIBDIR/l2_agent +source $LIBDIR/l3_agent + +function install_fwaas() { + # Install the service. + : + setup_develop $DEST/neutron-fwaas + if is_ubuntu; then + install_package libnetfilter-log1 + else + # EPEL + install_package libnetfilter_log + fi +} + +function configure_fwaas_v2() { + # Add conf file + cp $NEUTRON_FWAAS_DIR/etc/neutron_fwaas.conf.sample $NEUTRON_FWAAS_CONF + neutron_server_config_add $NEUTRON_FWAAS_CONF + inicomment $NEUTRON_FWAAS_CONF service_providers service_provider + iniadd $NEUTRON_FWAAS_CONF service_providers service_provider $NEUTRON_FWAAS_SERVICE_PROVIDERV2 + + neutron_fwaas_configure_driver fwaas_v2 + if is_service_enabled q-l3; then + iniset_multiline $Q_L3_CONF_FILE fwaas agent_version v2 + iniset_multiline $Q_L3_CONF_FILE fwaas driver $FWAAS_DRIVER_V2 + fi + if is_service_enabled q-agt; then + # TODO(hoangcx) we can remove the slashes below once neutron-legacy has gone + iniset /$NEUTRON_CORE_PLUGIN_CONF fwaas firewall_l2_driver $FW_L2_DRIVER + iniset /$NEUTRON_CORE_PLUGIN_CONF agent extensions fwaas_v2 + fi +} + +function configure_l3_log_fwaas_v2(){ + if is_service_enabled q-l3; then + iniadd $Q_L3_CONF_FILE agent extensions fwaas_v2_log + fi +} + +function neutron_fwaas_generate_config_files { + (cd $NEUTRON_FWAAS_DIR && exec ./tools/generate_config_file_samples.sh) +} + +function init_fwaas() { + # Initialize and start the service. + : + # Using sudo to gain the root privilege to be able to copy file to rootwrap.d + sudo cp $DEST/neutron-fwaas/etc/neutron/rootwrap.d/fwaas-privsep.filters /etc/neutron/rootwrap.d/fwaas-privsep.filters +} + +function shutdown_fwaas() { + # Shut the service down. + : +} + +function cleanup_fwaas() { + # Cleanup the service. + : + if is_ubuntu; then + uninstall_package libnetfilter-log1 + else + # EPEL + uninstall_package libnetfilter_log + fi +} + +function neutron_fwaas_configure_common { + neutron_service_plugin_class_add $FWAAS_PLUGIN_V2 +} + +function neutron_fwaas_configure_driver { + if is_service_enabled q-l3; then + plugin_agent_add_l3_agent_extension $1 + configure_l3_agent + iniset_multiline $Q_L3_CONF_FILE fwaas enabled True + fi +} + +# check for service enabled +if is_service_enabled q-svc neutron-api && is_service_enabled q-fwaas q-fwaas-v2 neutron-fwaas-v2; then + + if [[ "$1" == "stack" && "$2" == "install" ]]; then + # Perform installation of service source + echo_summary "Installing neutron-fwaas" + install_fwaas + + elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then + # Configure after the other layer 1 and 2 services have been configured + neutron_fwaas_configure_common + neutron_fwaas_generate_config_files + echo_summary "Configuring neutron-fwaas for FWaaS v2" + configure_fwaas_v2 + if is_service_enabled q-log neutron-log; then + echo_summary "Configuring FwaaS V2 packet log for l3 extension" + configure_l3_log_fwaas_v2 + fi + + elif [[ "$1" == "stack" && "$2" == "extra" ]]; then + # Initialize and start the neutron-fwaas service + echo_summary "Initializing neutron-fwaas" + init_fwaas + fi + + if [[ "$1" == "unstack" ]]; then + # Shut down neutron-fwaas services + # no-op + shutdown_fwaas + fi + + if [[ "$1" == "clean" ]]; then + # Remove state and transient data + # Remember clean.sh first calls unstack.sh + # no-op + cleanup_fwaas + fi +fi + +# Restore xtrace +$XTRACE diff --git a/devstack/settings b/devstack/settings new file mode 100644 index 000000000..e3b3b58fa --- /dev/null +++ b/devstack/settings @@ -0,0 +1,12 @@ +FWAAS_DRIVER_V2=${FWAAS_DRIVER_V2:-iptables_v2} +FW_L2_DRIVER=${FW_L2_DRIVER:-noop} +FWAAS_PLUGIN_V2=${FWAAS_PLUGIN:-firewall_v2} + +NEUTRON_FWAAS_DIR=$DEST/neutron-fwaas +NEUTRON_FWAAS_CONF_FILE=neutron_fwaas.conf + +NEUTRON_FWAAS_CONF=$NEUTRON_CONF_DIR/$NEUTRON_FWAAS_CONF_FILE + +NEUTRON_FWAAS_SERVICE_PROVIDERV2=${NEUTRON_FWAAS_SERVICE_PROVIDERV2:-FIREWALL_V2:fwaas_db:neutron_fwaas.services.firewall.service_drivers.agents.agents.FirewallAgentDriver:default} + +enable_service q-fwaas-v2 diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 000000000..9cf0708d1 --- /dev/null +++ b/doc/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. +sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD +sphinxcontrib-apidoc>=0.2.0 # BSD +openstackdocstheme>=1.18.1 # Apache-2.0 +reno>=2.5.0 # Apache-2.0 diff --git a/doc/source/_static/.placeholder b/doc/source/_static/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 000000000..2f8d1f6ff --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,311 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 OpenStack Foundation. +# +# 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. + +# +# Keystone documentation build configuration file, created by +# sphinx-quickstart on Tue May 18 13:50:15 2010. +# +# This file is execfile()'d with the current directory set to it's 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. + +import os +import sys + +# 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. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) +sys.path.insert(0, ROOT_DIR) + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinxcontrib.apidoc', + 'sphinx.ext.coverage', + 'sphinx.ext.ifconfig', + 'sphinx.ext.graphviz', + 'sphinx.ext.todo', + 'oslo_config.sphinxext', + 'oslo_config.sphinxconfiggen', + 'oslo_policy.sphinxext', + 'oslo_policy.sphinxpolicygen', + 'openstackdocstheme',] + +try: + import openstackdocstheme + extensions.append('openstackdocstheme') +except ImportError: + openstackdocstheme = None + +todo_include_todos = True + +# sphinxcontrib.apidoc options +apidoc_module_dir = '../../neutron_fwaas' +apidoc_output_dir = 'contributor/api' +# TODO(hoangcx): remove 'services/logapi/*' and +# 'services/firewall/fwaas_plugin_v2.py' after the next neutron release +# (current release is Rocky-3) + +# NOTE(longkb): Due to libnetfilter_log library is not installed in sphinx-docs +# gate, so we would like to ignore 'privileged/netfilter_log/*'. + +apidoc_excluded_paths = [ + 'db/migration/alembic_migrations/*', + 'privileged/netfilter_log/*', + 'services/firewall/fwaas_plugin_v2.py', + 'services/logapi/*', + 'setup.py', + 'tests/*', + 'tests'] +apidoc_separate_modules = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = [] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Neutron FWaaS' +copyright = u'2011-present, 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. +# +# Version info +from neutron_fwaas.version import version_info as neutron_fwaas_version +release = neutron_fwaas_version.release_string() +# The short X.Y version. +version = neutron_fwaas_version.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 documents that shouldn't be included in the build. +# unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = [] + +# The reST default role (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 = True + +# 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.'] + +# -- Options for man page output -------------------------------------------- + +# Grouping the document tree for man pages. +# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' + +#man_pages = [ +# ('man/neutron-server', 'neutron-server', u'Neutron Server', +# [u'OpenStack'], 1) +#] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +if openstackdocstheme is not None: + html_theme = 'openstackdocs' +else: + 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 = ['_theme'] + +# 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 = ['_static'] + +# 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' +html_last_updated_fmt = '%Y-%m-%d %H:%M' + +# 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_use_modindex = 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, 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 = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +#htmlhelp_basename = 'neutrondoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, +# documentclass [howto/manual]). +latex_documents = [ + ('index', 'doc-neutron-fwaas.tex', + u'Neutron Firewall-as-s-Service Documentation', + u'Neutron development team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True + +# Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 +latex_use_xindy = False + +latex_domain_indices = False + +latex_elements = { + 'makeindex': '', + 'printindex': '', + 'preamble': r'\setcounter{tocdepth}{3}', +} + +# -- Options for openstackdocstheme ------------------------------------------- +repository_name = 'openstack/neutron-fwaas' +bug_project = 'neutron' +bug_tag = 'doc' + +# -- Options for oslo_config.sphinxconfiggen --------------------------------- + +_config_generator_config_files = [ + 'fwaas_driver.ini', + 'neutron_fwaas.conf', +] + + +def _get_config_generator_config_definition(conf): + config_file_path = '../../etc/oslo-config-generator/%s' % conf + # oslo_config.sphinxconfiggen appends '.conf.sample' to the filename, + # strip file extentension (.conf or .ini). + output_file_path = '_static/config_samples/%s' % conf.rsplit('.', 1)[0] + return (config_file_path, output_file_path) + + +config_generator_config_file = [ + _get_config_generator_config_definition(conf) + for conf in _config_generator_config_files +] + +# -- Options for oslo_policy.sphinxpolicygen --------------------------------- + +policy_generator_config_file = '../../etc/oslo-policy-generator/policy.conf' +sample_policy_basename = '_static/neutron-fwaas' diff --git a/doc/source/configuration/fwaas_driver.rst b/doc/source/configuration/fwaas_driver.rst new file mode 100644 index 000000000..015740d3f --- /dev/null +++ b/doc/source/configuration/fwaas_driver.rst @@ -0,0 +1,6 @@ +================ +fwaas_driver.ini +================ + +.. show-options:: + :config-file: etc/oslo-config-generator/fwaas_driver.ini diff --git a/doc/source/configuration/index.rst b/doc/source/configuration/index.rst new file mode 100644 index 000000000..988ea7e7d --- /dev/null +++ b/doc/source/configuration/index.rst @@ -0,0 +1,41 @@ +.. _configuring: + +================================= +Neutron FWaaS Configuration Guide +================================= + +This section provides a list of all possible options for each +configuration file. + +Configuration +------------- + +Neutron FWaaS uses the following configuration files for its various services. + +.. toctree:: + :maxdepth: 1 + + neutron_fwaas + fwaas_driver + +The following are sample configuration files for Neutron FWaaS and utilities. +These are generated from code and reflect the current state of code +in the neutron-fwaas repository. + +.. toctree:: + :glob: + :maxdepth: 1 + + samples/* + +Policy +------ + +Neutron FWaaS, like most OpenStack projects, uses a policy language to restrict +permissions on REST API actions. + +.. toctree:: + :maxdepth: 1 + + Policy Reference + Sample Policy File diff --git a/doc/source/configuration/neutron_fwaas.rst b/doc/source/configuration/neutron_fwaas.rst new file mode 100644 index 000000000..e390f0f1b --- /dev/null +++ b/doc/source/configuration/neutron_fwaas.rst @@ -0,0 +1,6 @@ +================== +neutron_fwaas.conf +================== + +.. show-options:: + :config-file: etc/oslo-config-generator/neutron_fwaas.conf diff --git a/doc/source/configuration/policy-sample.rst b/doc/source/configuration/policy-sample.rst new file mode 100644 index 000000000..d5c9cd537 --- /dev/null +++ b/doc/source/configuration/policy-sample.rst @@ -0,0 +1,16 @@ +================================ +Sample Neutron FWaaS Policy File +================================ + +The following is a sample neutron-fwaas policy file for adaptation and use. + +The sample policy can also be viewed in :download:`file form +`. + +.. important:: + + The sample policy file is auto-generated from neutron-fwaas when this + documentation is built. You must ensure your version of neutron-fwaas + matches the version of this documentation. + +.. literalinclude:: /_static/neutron-fwaas.policy.yaml.sample diff --git a/doc/source/configuration/policy.rst b/doc/source/configuration/policy.rst new file mode 100644 index 000000000..697aa96d7 --- /dev/null +++ b/doc/source/configuration/policy.rst @@ -0,0 +1,9 @@ +====================== +neutron-fwaas policies +====================== + +The following is an overview of all available policies in neutron-fwaas. +For a sample configuration file, refer to :doc:`/configuration/policy-sample`. + +.. show-policy:: + :config-file: etc/oslo-policy-generator/policy.conf diff --git a/doc/source/configuration/samples/fwaas_driver.rst b/doc/source/configuration/samples/fwaas_driver.rst new file mode 100644 index 000000000..2314f4ce4 --- /dev/null +++ b/doc/source/configuration/samples/fwaas_driver.rst @@ -0,0 +1,8 @@ +======================= +Sample fwaas_driver.ini +======================= + +This sample configuration can also be viewed in `the raw format +<../../_static/config_samples/fwaas_driver.conf.sample>`_. + +.. literalinclude:: ../../_static/config_samples/fwaas_driver.conf.sample diff --git a/doc/source/configuration/samples/neutron_fwaas.rst b/doc/source/configuration/samples/neutron_fwaas.rst new file mode 100644 index 000000000..1e1fd0ced --- /dev/null +++ b/doc/source/configuration/samples/neutron_fwaas.rst @@ -0,0 +1,8 @@ +========================= +Sample neutron_fwaas.conf +========================= + +This sample configuration can also be viewed in `the raw format +<../../_static/config_samples/neutron_fwaas.conf.sample>`_. + +.. literalinclude:: ../../_static/config_samples/neutron_fwaas.conf.sample diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst new file mode 100644 index 000000000..a163a5e62 --- /dev/null +++ b/doc/source/contributor/contributing.rst @@ -0,0 +1,28 @@ +============================= +Contributing to neutron-fwaas +============================= + +If you would like to contribute to the development of OpenStack, you must +follow the steps documented at: +https://docs.openstack.org/infra/manual/developers.html + +Once those steps have been completed, changes to OpenStack should be submitted +for review via the Gerrit tool, following the workflow documented at: +https://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad in the 'neutron' project: +https://bugs.launchpad.net/neutron + +To get in touch with the neutron-fwaas community, +look at the following resources: + +- Join the #openstack-fwaas IRC channel on Freenode. This is where the + FireWall-as-a-Service team is available for discussion. +- Join the `FireWall-as-a-Service weekly IRC meeting + `_ + where the status of new initiatives and bugs is discussed. + +These are a great places to get recommendations on where to start contributing +to neutron-fwaas. diff --git a/doc/source/contributor/devstack.rst b/doc/source/contributor/devstack.rst new file mode 100644 index 000000000..da85f63d0 --- /dev/null +++ b/doc/source/contributor/devstack.rst @@ -0,0 +1 @@ +.. include:: ../../../devstack/README.rst diff --git a/doc/source/contributor/fwaas_v2.rst b/doc/source/contributor/fwaas_v2.rst new file mode 100644 index 000000000..8ee0662fc --- /dev/null +++ b/doc/source/contributor/fwaas_v2.rst @@ -0,0 +1,7 @@ +FireWall as a Service V2 +======================== + +The `FireWall as a Service API V2 +`_ +specification lists the changes that together compose FWaaS V2. These changes +are not fully implemented. diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 000000000..ca3f7d137 --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,17 @@ +================= +Contributor Guide +================= + +.. toctree:: + :maxdepth: 2 + + contributing + fwaas_v2 + devstack + +.. API reference contains a lot of sections, toctree with maxdepth 1 is used. +.. toctree:: + :glob: + :maxdepth: 1 + + modules diff --git a/doc/source/contributor/modules.rst b/doc/source/contributor/modules.rst new file mode 100644 index 000000000..b1a5cbe52 --- /dev/null +++ b/doc/source/contributor/modules.rst @@ -0,0 +1,19 @@ +================ +Module Reference +================ + +.. The module reference is rendered in HTML version much much better. + PDF version is not good for reading due to page width, lack of TOC + in subsections and so on, so we skip the module reference in PDF version. + +.. only:: html + + .. toctree:: + :maxdepth: 1 + :glob: + + api/* + +.. only:: latex + + See the online version of this document for the module reference. diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 000000000..55cc5a123 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,27 @@ +=========================== +neutron-fwaas documentation +=========================== + +.. warning:: + Due to lack of maintainers this project is now deprecated in the Neutron + stadium and will be removed from stadium in ``W`` cycle. + If You want to step in and be maintainer of this project to keep it in the + Neutron stadium, please contact the ``neutron team`` via + openstack-discuss@lists.openstack.org or IRC channel #openstack-neutron + @freenode. + +.. toctree:: + :glob: + :maxdepth: 2 + + install/index + configuration/index + contributor/index + +.. only:: html + + .. rubric:: Indices and tables + + * :ref:`genindex` + * :ref:`modindex` + * :ref:`search` diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst new file mode 100644 index 000000000..69044d889 --- /dev/null +++ b/doc/source/install/index.rst @@ -0,0 +1,39 @@ +.. + 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. + + + Convention for heading levels in Neutron devref: + ======= Heading 0 (reserved for the title in a document) + ------- Heading 1 + ~~~~~~~ Heading 2 + +++++++ Heading 3 + ''''''' Heading 4 + (Avoid deeper levels because they do not render well.) + + +============ +Installation +============ + +At the command line:: + + $ pip install neutron-fwaas + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv neutron-fwaas + $ pip install neutron-fwaas + +For information on what to do with FWaaS once it is installed, please check the +Networking Guide `Firewall-as-a-Service (FWaaS) v2 scenario `_ or +the `Firewall-as-a-Service (FWaaS) v1 scenario `_. diff --git a/etc/README.txt b/etc/README.txt new file mode 100644 index 000000000..074e68141 --- /dev/null +++ b/etc/README.txt @@ -0,0 +1,9 @@ +To generate the sample neutron-fwaas configuration files, run the following +command from the top level of the neutron-fwaas directory: + +tox -e genconfig + +If a 'tox' environment is unavailable, then you can run the following script +instead to generate the configuration files: + +./tools/generate_config_file_samples.sh diff --git a/etc/neutron/rootwrap.d/fwaas-privsep.filters b/etc/neutron/rootwrap.d/fwaas-privsep.filters new file mode 100644 index 000000000..6b631417d --- /dev/null +++ b/etc/neutron/rootwrap.d/fwaas-privsep.filters @@ -0,0 +1,7 @@ +# neutron-fwaas privsep filters + +# This file should be owned by (and only-writeable by) the root user + +[Filters] + +privsep-rootwrap: PathFilter, privsep-helper, root, privsep-helper, --config-file, /etc/(?!\.\.).*, --privsep_context, neutron_fwaas.privileged.default diff --git a/etc/oslo-config-generator/fwaas_driver.ini b/etc/oslo-config-generator/fwaas_driver.ini new file mode 100644 index 000000000..aa5205db7 --- /dev/null +++ b/etc/oslo-config-generator/fwaas_driver.ini @@ -0,0 +1,5 @@ +[DEFAULT] +output_file = etc/fwaas_driver.ini.sample +wrap_width = 79 + +namespace = firewall.agent diff --git a/etc/oslo-config-generator/neutron_fwaas.conf b/etc/oslo-config-generator/neutron_fwaas.conf new file mode 100644 index 000000000..ba145acdd --- /dev/null +++ b/etc/oslo-config-generator/neutron_fwaas.conf @@ -0,0 +1,6 @@ +[DEFAULT] +output_file = etc/neutron_fwaas.conf.sample +wrap_width = 79 + +namespace = neutron.fwaas + diff --git a/etc/oslo-policy-generator/policy.conf b/etc/oslo-policy-generator/policy.conf new file mode 100644 index 000000000..8e4aef649 --- /dev/null +++ b/etc/oslo-policy-generator/policy.conf @@ -0,0 +1,3 @@ +[DEFAULT] +output_file = etc/policy.yaml.sample +namespace = neutron-fwaas diff --git a/lower-constraints.txt b/lower-constraints.txt new file mode 100644 index 000000000..5e39c9945 --- /dev/null +++ b/lower-constraints.txt @@ -0,0 +1,147 @@ +alabaster==0.7.10 +alembic==1.6.5 +amqp==2.1.1 +appdirs==1.4.3 +Babel==2.3.4 +beautifulsoup4==4.6.0 +cachetools==2.0.0 +cffi==1.7.0 +chardet==3.0.4 +cliff==2.8.0 +cmd2==0.8.0 +contextlib2==0.4.0 +coverage==4.0 +debtcollector==1.2.0 +decorator==4.1.0 +deprecation==1.0 +doc8==0.6.0 +docutils==0.11 +dogpile.cache==0.6.2 +dulwich==0.15.0 +eventlet==0.18.2 +extras==1.0.0 +fasteners==0.7.0 +fixtures==3.0.0 +flake8-import-order==0.12 +flake8==3.6.0 +future==0.16.0 +futurist==1.2.0 +greenlet==0.4.10 +hacking==3.0.1 +httplib2==0.9.1 +imagesize==0.7.1 +iso8601==0.1.11 +Jinja2==2.10 +jmespath==0.9.0 +jsonpatch==1.16 +jsonpointer==1.13 +jsonschema==2.6.0 +keystoneauth1==3.4.0 +keystonemiddleware==4.17.0 +kombu==4.0.0 +linecache2==1.0.0 +logutils==0.3.5 +Mako==1.0.7 +MarkupSafe==1.1.1 +mccabe==0.6.0 +mock==2.0.0 +monotonic==0.6 +mox3==0.20.0 +msgpack-python==0.4.0 +munch==2.1.0 +netaddr==0.7.18 +netifaces==0.10.4 +neutron-lib==1.26.0 +neutron==14.0.0.0b3 +openstackdocstheme==1.18.1 +openstacksdk==0.11.2 +os-client-config==1.28.0 +os-ken==0.3.0 +os-service-types==1.2.0 +os-xenapi==0.3.1 +osc-lib==1.8.0 +oslo.cache==1.26.0 +oslo.concurrency==3.26.0 +oslo.config==5.2.0 +oslo.context==2.19.2 +oslo.db==4.37.0 +oslo.i18n==3.15.3 +oslo.log==3.36.0 +oslo.messaging==5.29.0 +oslo.middleware==3.31.0 +oslo.policy==1.30.0 +oslo.privsep==1.32.0 +oslo.reports==1.18.0 +oslo.rootwrap==5.8.0 +oslo.serialization==2.18.0 +oslo.service==1.24.0 +oslo.utils==3.33.0 +oslo.versionedobjects==1.31.2 +oslotest==3.2.0 +osprofiler==1.4.0 +ovs==2.8.0 +ovsdbapp==0.9.1 +Paste==2.0.2 +PasteDeploy==1.5.0 +pbr==4.0.0 +pecan==1.3.2 +pep8==1.5.7 +pika-pool==0.1.3 +pika==0.10.0 +positional==1.2.1 +prettytable==0.7.2 +psutil==3.2.2 +psycopg2==2.7.3 +pycadf==1.1.0 +pycodestyle==2.4.0 +pycparser==2.18 +pyflakes==2.0.0 +Pygments==2.2.0 +pyinotify==0.9.6 +PyMySQL==0.7.6 +pyparsing==2.1.0 +pyperclip==1.5.27 +pyroute2==0.5.3 +python-dateutil==2.5.3 +python-designateclient==2.7.0 +python-editor==1.0.3 +python-keystoneclient==3.8.0 +python-mimeparse==1.6.0 +python-neutronclient==6.7.0 +python-novaclient==9.1.0 +python-subunit==1.0.0 +pytz==2013.6 +PyYAML==3.12 +pyzmq==14.3.1 +reno==2.5.0 +repoze.lru==0.7 +requests-mock==1.2.0 +requests==2.14.2 +requestsexceptions==1.2.0 +restructuredtext-lint==1.1.1 +rfc3986==0.3.1 +Routes==2.3.1 +simplejson==3.5.1 +six==1.10.0 +snowballstemmer==1.2.1 +sphinx==1.6.5 +sqlalchemy-migrate==0.11.0 +SQLAlchemy==1.4.23 +sqlparse==0.2.2 +statsd==3.2.1 +stestr==1.0.0 +stevedore==1.20.0 +Tempita==0.5.2 +tenacity==3.2.1 +testrepository==0.0.18 +testresources==2.0.0 +testscenarios==0.4 +testtools==2.2.0 +tinyrpc==0.6 +traceback2==1.4.0 +unittest2==1.1.0 +vine==1.1.4 +waitress==1.1.0 +WebOb==1.8.2 +WebTest==2.0.27 +wrapt==1.7.0 diff --git a/neutron_fwaas/__init__.py b/neutron_fwaas/__init__.py new file mode 100644 index 000000000..208ffbf15 --- /dev/null +++ b/neutron_fwaas/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2011 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. + +import gettext + +import six + + +if six.PY2: + gettext.install('neutron', unicode=1) +else: + gettext.install('neutron') diff --git a/neutron_fwaas/_i18n.py b/neutron_fwaas/_i18n.py new file mode 100644 index 000000000..e28f57522 --- /dev/null +++ b/neutron_fwaas/_i18n.py @@ -0,0 +1,32 @@ +# 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. + +import oslo_i18n + +DOMAIN = "neutron_fwaas" + +_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN) + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# The contextual translation function using the name "_C" +_C = _translators.contextual_form + +# The plural translation function using the name "_P" +_P = _translators.plural_form + + +def get_available_languages(): + return oslo_i18n.get_available_languages(DOMAIN) diff --git a/neutron_fwaas/cmd/__init__.py b/neutron_fwaas/cmd/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/cmd/upgrade_checks/__init__.py b/neutron_fwaas/cmd/upgrade_checks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/cmd/upgrade_checks/checks.py b/neutron_fwaas/cmd/upgrade_checks/checks.py new file mode 100644 index 000000000..610b6968e --- /dev/null +++ b/neutron_fwaas/cmd/upgrade_checks/checks.py @@ -0,0 +1,40 @@ +# Copyright 2019 Red Hat 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 neutron_lib.utils import upgrade_checks as base_checks +from oslo_config import cfg +from oslo_upgradecheck import upgradecheck + +from neutron_fwaas._i18n import _ + + +class Checks(base_checks.BaseChecks): + + def get_checks(self): + return [ + (_("Check FWaaS v1"), self.fwaas_v1_check) + ] + + @staticmethod + def fwaas_v1_check(checker): + fwaas_v1_names = [ + 'firewall', + 'neutron_fwaas.services.firewall.fwaas_plugin:FirewallPlugin'] + for name in fwaas_v1_names: + if name in cfg.CONF.service_plugins: + return upgradecheck.Result( + upgradecheck.Code.FAILURE, + _("FWaaS v1 is removed. " + "FWaaS v2 should be used instead.")) + return upgradecheck.Result(upgradecheck.Code.SUCCESS) diff --git a/neutron_fwaas/cmd/v1_to_v2_db_migration.py b/neutron_fwaas/cmd/v1_to_v2_db_migration.py new file mode 100644 index 000000000..ea05f2bef --- /dev/null +++ b/neutron_fwaas/cmd/v1_to_v2_db_migration.py @@ -0,0 +1,138 @@ +# 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 neutron.common import config +from neutron.db import models_v2 +from oslo_config import cfg +from oslo_db.sqlalchemy import enginefacade +from oslo_log import log as logging + +from neutron_fwaas._i18n import _ +from neutron_fwaas.db.firewall import firewall_db as firewall_db_v1 +from neutron_fwaas.db.firewall.v2 import firewall_db_v2 + + +LOG = logging.getLogger(__name__) + + +def setup_conf(): + cli_opts = [ + cfg.StrOpt('neutron-db-connection', + required=True, + help=_('neutron database connection string')), + ] + conf = cfg.CONF + conf.register_cli_opts(cli_opts) + conf() + + +def migrate_fwaas_v1_to_v2(db_session): + # the entire migration process will be done under the same transaction to + # allow full rollback in case of error + with db_session.begin(subtransactions=True): + # Read all V1 policies + v1_policies = db_session.query(firewall_db_v1.FirewallPolicy) + + for v1_pol in v1_policies: + LOG.info("Migrating FWaaS V1 policy %s", v1_pol.id) + # read the rules of this policy + v1_rules = db_session.query(firewall_db_v1.FirewallRule).filter_by( + firewall_policy_id=v1_pol.id).all() + # Create the V2 policy + v2_pol = firewall_db_v2.FirewallPolicy( + id=v1_pol.id, + tenant_id=v1_pol.tenant_id, + name=v1_pol.name, + description=v1_pol.description, + shared=v1_pol.shared, + audited=v1_pol.audited, + rule_count=len(v1_rules)) + db_session.add(v2_pol) + + # Add the rules and associate them with the policy + for v1_rule in v1_rules: + LOG.info("Migrating FWaaS V1 rule %s", v1_rule.id) + v2_rule = firewall_db_v2.FirewallRuleV2( + id=v1_rule.id, + name=v1_rule.name, + description=v1_rule.description, + tenant_id=v1_rule.tenant_id, + shared=v1_rule.shared, + protocol=v1_rule.protocol, + ip_version=v1_rule.ip_version, + source_ip_address=v1_rule.source_ip_address, + destination_ip_address=v1_rule.destination_ip_address, + source_port_range_min=v1_rule.source_port_range_min, + source_port_range_max=v1_rule.source_port_range_max, + destination_port_range_min=( + v1_rule.destination_port_range_min), + destination_port_range_max=( + v1_rule.destination_port_range_max), + action=v1_rule.action, + enabled=v1_rule.enabled) + db_session.add(v2_rule) + v2_link = firewall_db_v2.FirewallPolicyRuleAssociation( + firewall_policy_id=v1_pol.id, + firewall_rule_id=v1_rule.id, + position=v1_rule.position) + db_session.add(v2_link) + + # Read all V1 firewalls + v1_fws = db_session.query(firewall_db_v1.Firewall) + for v1_fw in v1_fws: + LOG.info("Migrating FWaaS V1 firewall %s", v1_fw.id) + # create the V2 firewall group + v2_fw_group = firewall_db_v2.FirewallGroup( + id=v1_fw.id, + name=v1_fw.name, + description=v1_fw.description, + tenant_id=v1_fw.tenant_id, + shared=v1_fw.shared, + admin_state_up=v1_fw.admin_state_up, + status=v1_fw.status, + ingress_firewall_policy_id=v1_fw.firewall_policy_id, + egress_firewall_policy_id=v1_fw.firewall_policy_id) + db_session.add(v2_fw_group) + + # for every router in the V1 Firewall router association, add all + # its interface ports to the V2 FirewallGroupPortAssociation + v1_routers = db_session.query( + firewall_db_v1.FirewallRouterAssociation).filter_by( + fw_id=v1_fw.id) + for v1_router in v1_routers: + rtr_id = v1_router.router_id + LOG.info("Migrating FWaaS V1 %s router %s", v1_fw.id, rtr_id) + if_ports = db_session.query(models_v2.Port).filter_by( + device_id=rtr_id, + device_owner="network:router_interface") + for port in if_ports: + fw_port = firewall_db_v2.FirewallGroupPortAssociation( + firewall_group_id=v2_fw_group.id, + port_id=port.id) + db_session.add(fw_port) + + +def main(): + # Initialize the cli options + setup_conf() + config.setup_logging() + + # Get the neutron DB session + neutron_context_manager = enginefacade.transaction_context() + neutron_context_manager.configure( + connection=cfg.CONF.neutron_db_connection) + n_session_maker = neutron_context_manager.writer.get_sessionmaker() + n_session = n_session_maker(autocommit=True) + + # Run DB migration + migrate_fwaas_v1_to_v2(n_session) + LOG.info("DB migration done.") diff --git a/neutron_fwaas/common/__init__.py b/neutron_fwaas/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/common/exceptions.py b/neutron_fwaas/common/exceptions.py new file mode 100644 index 000000000..261a9d476 --- /dev/null +++ b/neutron_fwaas/common/exceptions.py @@ -0,0 +1,24 @@ +# Copyright 2018 Fujitsu Limited. +# 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. + +from neutron_lib import exceptions as n_exc + +from neutron_fwaas._i18n import _ + + +# TODO(annp): migrate to neutron-lib after Queen release +class FirewallGroupPortNotSupported(n_exc.Conflict): + message = _("Port %(port_id)s is not supported by firewall driver " + "'%(driver_name)s'.") diff --git a/neutron_fwaas/common/fwaas_constants.py b/neutron_fwaas/common/fwaas_constants.py new file mode 100644 index 000000000..c21006975 --- /dev/null +++ b/neutron_fwaas/common/fwaas_constants.py @@ -0,0 +1,42 @@ +# Copyright 2015 Cisco Systems, Inc +# 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. + +FIREWALL = 'FIREWALL' +FIREWALL_V2 = 'FIREWALL_V2' + +# Constants for "topics" +FIREWALL_PLUGIN = 'q-firewall-plugin' +FW_AGENT = 'firewall_agent' +FIREWALL_RULE_LIST = 'firewall_rule_list' + +# V2 Constants +DEFAULT_FWG = 'default' +DEFAULT_FWP_INGRESS = 'default ingress' +DEFAULT_FWP_EGRESS = 'default egress' + +# Firewall group events for agent-side +DELETE_FWG = 'delete_firewall_group' +UPDATE_FWG = 'update_firewall_group' +CREATE_FWG = 'create_firewall_group' + +# Port events for L2 agent extension +HANDLE_PORT = 'handle_port' +DELETE_PORT = 'delete_port' + +# Resource name + +FIREWALL_GROUP = 'firewall_group' +FIREWALL_RULE = 'firewall_rule' +FIREWALL_POLICY = 'firewall_policy' diff --git a/neutron_fwaas/common/resources.py b/neutron_fwaas/common/resources.py new file mode 100644 index 000000000..7f6fee431 --- /dev/null +++ b/neutron_fwaas/common/resources.py @@ -0,0 +1,17 @@ +# 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 neutron_fwaas.db.firewall.v2 import firewall_db_v2 + +FIREWALL_GROUP = firewall_db_v2.FirewallGroup +FIREWALL_POLICY = firewall_db_v2.FirewallPolicy +FIREWALL_RULE = firewall_db_v2.FirewallRuleV2 diff --git a/neutron_fwaas/db/__init__.py b/neutron_fwaas/db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/db/firewall/__init__.py b/neutron_fwaas/db/firewall/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/db/firewall/firewall_db.py b/neutron_fwaas/db/firewall/firewall_db.py new file mode 100644 index 000000000..f3d2018a5 --- /dev/null +++ b/neutron_fwaas/db/firewall/firewall_db.py @@ -0,0 +1,89 @@ +# Copyright 2013 Big Switch Networks, Inc. +# 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. + +from neutron_lib.db import model_base +import sqlalchemy as sa +from sqlalchemy.ext.orderinglist import ordering_list +from sqlalchemy import orm + + +# Note(annp): Keep firewall db v1 structure for migration +class FirewallRule(model_base.BASEV2, model_base.HasId, model_base.HasProject): + """Represents a Firewall rule.""" + __tablename__ = 'firewall_rules' + __table_args__ = ({'mysql_collate': 'utf8_bin'}) + name = sa.Column(sa.String(255)) + description = sa.Column(sa.String(1024)) + firewall_policy_id = sa.Column(sa.String(36), + sa.ForeignKey('firewall_policies.id'), + nullable=True) + shared = sa.Column(sa.Boolean) + protocol = sa.Column(sa.String(40)) + ip_version = sa.Column(sa.Integer, nullable=False) + source_ip_address = sa.Column(sa.String(46)) + destination_ip_address = sa.Column(sa.String(46)) + source_port_range_min = sa.Column(sa.Integer) + source_port_range_max = sa.Column(sa.Integer) + destination_port_range_min = sa.Column(sa.Integer) + destination_port_range_max = sa.Column(sa.Integer) + action = sa.Column(sa.Enum('allow', 'deny', 'reject', + name='firewallrules_action')) + enabled = sa.Column(sa.Boolean) + position = sa.Column(sa.Integer) + + +class Firewall(model_base.BASEV2, model_base.HasId, model_base.HasProject): + """Represents a Firewall resource.""" + __tablename__ = 'firewalls' + __table_args__ = ({'mysql_collate': 'utf8_bin'}) + name = sa.Column(sa.String(255)) + description = sa.Column(sa.String(1024)) + shared = sa.Column(sa.Boolean) + admin_state_up = sa.Column(sa.Boolean) + status = sa.Column(sa.String(16)) + firewall_policy_id = sa.Column(sa.String(36), + sa.ForeignKey('firewall_policies.id'), + nullable=True) + + +class FirewallPolicy(model_base.BASEV2, model_base.HasId, + model_base.HasProject): + """Represents a Firewall Policy resource.""" + __tablename__ = 'firewall_policies' + __table_args__ = ({'mysql_collate': 'utf8_bin'}) + name = sa.Column(sa.String(255)) + description = sa.Column(sa.String(1024)) + shared = sa.Column(sa.Boolean) + firewall_rules = orm.relationship( + FirewallRule, + backref=orm.backref('firewall_policies', cascade='all, delete'), + order_by='FirewallRule.position', + collection_class=ordering_list('position', count_from=1)) + audited = sa.Column(sa.Boolean) + firewalls = orm.relationship(Firewall, backref='firewall_policies') + + +class FirewallRouterAssociation(model_base.BASEV2): + + """Tracks FW Router Association""" + + __tablename__ = 'firewall_router_associations' + + fw_id = sa.Column(sa.String(36), + sa.ForeignKey('firewalls.id', ondelete="CASCADE"), + primary_key=True) + router_id = sa.Column(sa.String(36), + sa.ForeignKey('routers.id', ondelete="CASCADE"), + primary_key=True) diff --git a/neutron_fwaas/db/firewall/v2/__init__.py b/neutron_fwaas/db/firewall/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/db/firewall/v2/firewall_db_v2.py b/neutron_fwaas/db/firewall/v2/firewall_db_v2.py new file mode 100644 index 000000000..a3276251b --- /dev/null +++ b/neutron_fwaas/db/firewall/v2/firewall_db_v2.py @@ -0,0 +1,1101 @@ +# Copyright (c) 2016 +# 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. + +import copy + +import netaddr + +from neutron_lib import constants as nl_constants +from neutron_lib.db import api as db_api +from neutron_lib.db import constants as db_constants +from neutron_lib.db import model_base +from neutron_lib.db import model_query +from neutron_lib.db import utils as db_utils +from neutron_lib import exceptions +from neutron_lib.exceptions import firewall_v2 as f_exc +from oslo_config import cfg +from oslo_db import exception as db_exc +from oslo_log import log as logging +from oslo_utils import uuidutils +import sqlalchemy as sa +from sqlalchemy.ext.orderinglist import ordering_list +from sqlalchemy import or_ +from sqlalchemy import orm +from sqlalchemy.orm import exc + +from neutron_fwaas.common import fwaas_constants as const + + +LOG = logging.getLogger(__name__) + + +class FirewallDefaultParameterExists(exceptions.InUse): + """Default Firewall Parameter conflict exception + + Occurs when user creates/updates any existing firewall resource with + reserved parameter names. + """ + message = ("Operation cannot be performed since '%(name)s' " + "is a reserved name for %(resource_type)s.") + + +class FirewallDefaultObjectUpdateRestricted(FirewallDefaultParameterExists): + message = ("Operation cannot be performed on default object " + "'%(resource_id)s' of type %(resource_type)s.") + + +class HasName(object): + name = sa.Column(sa.String(db_constants.NAME_FIELD_SIZE)) + + +class HasDescription(object): + description = sa.Column( + sa.String(db_constants.LONG_DESCRIPTION_FIELD_SIZE)) + + +class FirewallRuleV2(model_base.BASEV2, model_base.HasId, HasName, + HasDescription, model_base.HasProject): + __tablename__ = "firewall_rules_v2" + shared = sa.Column(sa.Boolean) + protocol = sa.Column(sa.String(40)) + ip_version = sa.Column(sa.Integer) + source_ip_address = sa.Column(sa.String(46)) + destination_ip_address = sa.Column(sa.String(46)) + source_port_range_min = sa.Column(sa.Integer) + source_port_range_max = sa.Column(sa.Integer) + destination_port_range_min = sa.Column(sa.Integer) + destination_port_range_max = sa.Column(sa.Integer) + action = sa.Column(sa.Enum('allow', 'deny', 'reject', + name='firewallrules_action')) + enabled = sa.Column(sa.Boolean) + + +class FirewallGroup(model_base.BASEV2, model_base.HasId, HasName, + HasDescription, model_base.HasProject): + __tablename__ = 'firewall_groups_v2' + port_associations = orm.relationship( + 'FirewallGroupPortAssociation', + backref=orm.backref('firewall_group_port_associations_v2', + cascade='all, delete')) + name = sa.Column(sa.String(db_constants.NAME_FIELD_SIZE)) + description = sa.Column( + sa.String(db_constants.LONG_DESCRIPTION_FIELD_SIZE)) + ingress_firewall_policy_id = sa.Column( + sa.String(db_constants.UUID_FIELD_SIZE), + sa.ForeignKey('firewall_policies_v2.id')) + egress_firewall_policy_id = sa.Column( + sa.String(db_constants.UUID_FIELD_SIZE), + sa.ForeignKey('firewall_policies_v2.id')) + admin_state_up = sa.Column(sa.Boolean) + status = sa.Column(sa.String(db_constants.STATUS_FIELD_SIZE)) + shared = sa.Column(sa.Boolean) + + +class DefaultFirewallGroup(model_base.BASEV2, model_base.HasProjectPrimaryKey): + __tablename__ = "default_firewall_groups" + firewall_group_id = sa.Column(sa.String(db_constants.UUID_FIELD_SIZE), + sa.ForeignKey('firewall_groups_v2.id', + ondelete="CASCADE"), + nullable=False) + firewall_group = orm.relationship( + FirewallGroup, lazy='joined', + backref=orm.backref('default_firewall_group', cascade='all,delete'), + primaryjoin="FirewallGroup.id==DefaultFirewallGroup.firewall_group_id", + ) + + +class FirewallGroupPortAssociation(model_base.BASEV2): + __tablename__ = 'firewall_group_port_associations_v2' + firewall_group_id = sa.Column(sa.String(db_constants.UUID_FIELD_SIZE), + sa.ForeignKey('firewall_groups_v2.id', + ondelete="CASCADE"), + primary_key=True) + port_id = sa.Column(sa.String(db_constants.UUID_FIELD_SIZE), + sa.ForeignKey('ports.id', ondelete="CASCADE"), + unique=True, + primary_key=True) + + +class FirewallPolicyRuleAssociation(model_base.BASEV2): + + """Tracks FW Policy and Rule(s) Association""" + + __tablename__ = 'firewall_policy_rule_associations_v2' + + firewall_policy_id = sa.Column(sa.String(db_constants.UUID_FIELD_SIZE), + sa.ForeignKey('firewall_policies_v2.id', + ondelete="CASCADE"), + primary_key=True) + firewall_rule_id = sa.Column(sa.String(db_constants.UUID_FIELD_SIZE), + sa.ForeignKey('firewall_rules_v2.id', + ondelete="CASCADE"), + primary_key=True) + position = sa.Column(sa.Integer) + + +class FirewallPolicy(model_base.BASEV2, model_base.HasId, HasName, + HasDescription, model_base.HasProject): + __tablename__ = 'firewall_policies_v2' + name = sa.Column(sa.String(db_constants.NAME_FIELD_SIZE)) + description = sa.Column( + sa.String(db_constants.LONG_DESCRIPTION_FIELD_SIZE)) + rule_count = sa.Column(sa.Integer) + audited = sa.Column(sa.Boolean) + rule_associations = orm.relationship( + FirewallPolicyRuleAssociation, + backref=orm.backref('firewall_policies_v2', cascade='all, delete'), + order_by='FirewallPolicyRuleAssociation.position', + collection_class=ordering_list('position', count_from=1)) + shared = sa.Column(sa.Boolean) + + +def _list_firewall_groups_result_filter_hook(query, filters): + values = filters and filters.get('ports', []) + if values: + query = query.join(FirewallGroupPortAssociation) + query = query.filter(FirewallGroupPortAssociation.port_id.in_(values)) + + return query + + +def _list_firewall_policies_result_filter_hook(query, filters): + values = filters and filters.get('firewall_rules', []) + if values: + query = query.join(FirewallPolicyRuleAssociation) + query = query.filter( + FirewallPolicyRuleAssociation.firewall_rule_id.in_(values)) + + return query + + +class FirewallPluginDb(object): + + def __new__(cls, *args, **kwargs): + model_query.register_hook( + FirewallGroup, + "firewall_group_v2_filter_by_port_association", + query_hook=None, + filter_hook=None, + result_filters=_list_firewall_groups_result_filter_hook) + + model_query.register_hook( + FirewallPolicy, + "firewall_policy_v2_filter_by_firewall_rule_association", + query_hook=None, + filter_hook=None, + result_filters=_list_firewall_policies_result_filter_hook) + return super(FirewallPluginDb, cls).__new__(cls, *args, **kwargs) + + def _get_firewall_group(self, context, id): + try: + return model_query.get_by_id(context, FirewallGroup, id) + except exc.NoResultFound: + raise f_exc.FirewallGroupNotFound(firewall_id=id) + + def _get_firewall_policy(self, context, id): + try: + return model_query.get_by_id(context, FirewallPolicy, id) + except exc.NoResultFound: + raise f_exc.FirewallPolicyNotFound(firewall_policy_id=id) + + def _get_firewall_rule(self, context, id): + try: + return model_query.get_by_id(context, FirewallRuleV2, id) + except exc.NoResultFound: + raise f_exc.FirewallRuleNotFound(firewall_rule_id=id) + + def _validate_fwr_protocol_parameters(self, fwr): + protocol = fwr['protocol'] + source_port = fwr['source_port'] + dest_port = fwr['destination_port'] + + if protocol and protocol not in (nl_constants.PROTO_NAME_TCP, + nl_constants.PROTO_NAME_UDP): + if source_port or dest_port: + raise f_exc.FirewallRuleInvalidICMPParameter( + param="Source, destination port") + + if not protocol and (source_port or dest_port): + raise f_exc.FirewallRuleWithPortWithoutProtocolInvalid() + + def _validate_fwr_src_dst_ip_version(self, fwr, fwr_db=None): + src_version = dst_version = None + if fwr.get('source_ip_address', None): + src_version = netaddr.IPNetwork(fwr['source_ip_address']).version + if fwr.get('destination_ip_address', None): + dst_version = netaddr.IPNetwork( + fwr['destination_ip_address']).version + rule_ip_version = fwr.get('ip_version', None) + if not rule_ip_version and fwr_db: + rule_ip_version = fwr_db.ip_version + if ((src_version and src_version != rule_ip_version) or + (dst_version and dst_version != rule_ip_version)): + raise f_exc.FirewallIpAddressConflict() + + def _validate_fwr_port_range(self, min_port, max_port): + if int(min_port) > int(max_port): + port_range = '%s:%s' % (min_port, max_port) + raise f_exc.FirewallRuleInvalidPortValue(port=port_range) + + def _get_min_max_ports_from_range(self, port_range): + if not port_range: + return [None, None] + min_port, sep, max_port = port_range.partition(":") + if not max_port: + max_port = min_port + self._validate_fwr_port_range(min_port, max_port) + return [int(min_port), int(max_port)] + + def _get_port_range_from_min_max_ports(self, min_port, max_port): + if not min_port: + return None + if min_port == max_port: + return str(min_port) + self._validate_fwr_port_range(min_port, max_port) + return '%s:%s' % (min_port, max_port) + + def _make_firewall_rule_dict(self, firewall_rule, fields=None, + policies=None): + src_port_range = self._get_port_range_from_min_max_ports( + firewall_rule['source_port_range_min'], + firewall_rule['source_port_range_max']) + dst_port_range = self._get_port_range_from_min_max_ports( + firewall_rule['destination_port_range_min'], + firewall_rule['destination_port_range_max']) + res = {'id': firewall_rule['id'], + 'tenant_id': firewall_rule['tenant_id'], + 'name': firewall_rule['name'], + 'description': firewall_rule['description'], + 'protocol': firewall_rule['protocol'], + 'firewall_policy_id': policies, + 'ip_version': firewall_rule['ip_version'], + 'source_ip_address': firewall_rule['source_ip_address'], + 'destination_ip_address': + firewall_rule['destination_ip_address'], + 'source_port': src_port_range, + 'destination_port': dst_port_range, + 'action': firewall_rule['action'], + 'enabled': firewall_rule['enabled'], + 'shared': firewall_rule['shared']} + return db_utils.resource_fields(res, fields) + + def _make_firewall_policy_dict(self, firewall_policy, fields=None): + fw_rules = [ + rule_association.firewall_rule_id + for rule_association in firewall_policy['rule_associations']] + res = {'id': firewall_policy['id'], + 'tenant_id': firewall_policy['tenant_id'], + 'name': firewall_policy['name'], + 'description': firewall_policy['description'], + 'audited': firewall_policy['audited'], + 'firewall_rules': fw_rules, + 'shared': firewall_policy['shared']} + return db_utils.resource_fields(res, fields) + + def _make_firewall_group_dict(self, firewall_group_db, fields=None): + fwg_ports = [port_assoc.port_id for port_assoc in + firewall_group_db.port_associations] + res = {'id': firewall_group_db['id'], + 'tenant_id': firewall_group_db['tenant_id'], + 'name': firewall_group_db['name'], + 'description': firewall_group_db['description'], + 'ingress_firewall_policy_id': + firewall_group_db['ingress_firewall_policy_id'], + 'egress_firewall_policy_id': + firewall_group_db['egress_firewall_policy_id'], + 'admin_state_up': firewall_group_db['admin_state_up'], + 'ports': fwg_ports, + 'status': firewall_group_db['status'], + 'shared': firewall_group_db['shared']} + return db_utils.resource_fields(res, fields) + + def _get_policy_ordered_rules(self, context, policy_id): + query = (context.session.query(FirewallRuleV2) + .join(FirewallPolicyRuleAssociation) + .filter_by(firewall_policy_id=policy_id) + .order_by(FirewallPolicyRuleAssociation.position)) + return [self._make_firewall_rule_dict(rule) for rule in query] + + def make_firewall_group_dict_with_rules(self, context, firewall_group_id): + firewall_group = self.get_firewall_group(context, firewall_group_id) + ingress_policy_id = firewall_group['ingress_firewall_policy_id'] + if ingress_policy_id: + firewall_group['ingress_rule_list'] = ( + self._get_policy_ordered_rules(context, ingress_policy_id)) + else: + firewall_group['ingress_rule_list'] = [] + + egress_policy_id = firewall_group['egress_firewall_policy_id'] + if egress_policy_id: + firewall_group['egress_rule_list'] = ( + self._get_policy_ordered_rules(context, egress_policy_id)) + else: + firewall_group['egress_rule_list'] = [] + return firewall_group + + def _check_firewall_rule_conflict(self, fwr_db, fwp_db): + if not fwr_db['shared']: + if fwr_db['tenant_id'] != fwp_db['tenant_id']: + raise f_exc.FirewallRuleConflict( + firewall_rule_id=fwr_db['id'], + project_id=fwr_db['tenant_id']) + + def _process_rule_for_policy(self, context, firewall_policy_id, + firewall_rule_id, position, association_db): + with context.session.begin(subtransactions=True): + fwp_query = context.session.query( + FirewallPolicy).with_for_update() + fwp_db = fwp_query.filter_by(id=firewall_policy_id).one() + if position: + # Note that although position numbering starts at 1, + # internal ordering of the list starts at 0, so we compensate. + fwp_db.rule_associations.insert( + position - 1, + FirewallPolicyRuleAssociation( + firewall_rule_id=firewall_rule_id)) + else: + fwp_db.rule_associations.remove(association_db) + context.session.delete(association_db) + fwp_db.rule_associations.reorder() + fwp_db.audited = False + return self._make_firewall_policy_dict(fwp_db) + + def _get_policy_rule_association_query(self, context, firewall_policy_id, + firewall_rule_id): + fwpra_query = context.session.query(FirewallPolicyRuleAssociation) + return fwpra_query.filter_by(firewall_policy_id=firewall_policy_id, + firewall_rule_id=firewall_rule_id) + + def _ensure_rule_not_already_associated(self, context, firewall_policy_id, + firewall_rule_id): + """Checks that a rule is not already associated with a particular + policy. If it is the function will throw an exception. + """ + try: + self._get_policy_rule_association_query( + context, firewall_policy_id, firewall_rule_id).one() + raise f_exc.FirewallRuleAlreadyAssociated( + firewall_rule_id=firewall_rule_id, + firewall_policy_id=firewall_policy_id) + except exc.NoResultFound: + return + + def _get_policy_rule_association(self, context, firewall_policy_id, + firewall_rule_id): + """Returns the association between a firewall rule and a firewall + policy. Throws an exception if the assocition does not exist. + """ + try: + return self._get_policy_rule_association_query( + context, firewall_policy_id, firewall_rule_id).one() + except exc.NoResultFound: + raise f_exc.FirewallRuleNotAssociatedWithPolicy( + firewall_rule_id=firewall_rule_id, + firewall_policy_id=firewall_policy_id) + + def _create_default_firewall_rules(self, context, tenant_id): + # NOTE(xgerman) Maybe generating the final set of rules from a + # configuration file makes sense. Can be done some time later + + # 1. Firewall rule for ingress IPv4 packets (DROP by default) + in_fwr_v4 = { + 'description': 'default ingress rule for IPv4', + 'name': 'default ingress ipv4', + 'shared': cfg.CONF.default_fwg_rules.shared, + 'protocol': cfg.CONF.default_fwg_rules.protocol, + 'tenant_id': tenant_id, + 'ip_version': nl_constants.IP_VERSION_4, + 'action': cfg.CONF.default_fwg_rules.ingress_action, + 'enabled': cfg.CONF.default_fwg_rules.enabled, + 'source_port': cfg.CONF.default_fwg_rules.ingress_source_port, + 'source_ip_address': + cfg.CONF.default_fwg_rules.ingress_source_ipv4_address, + 'destination_port': + cfg.CONF.default_fwg_rules.ingress_destination_port, + 'destination_ip_address': + cfg.CONF.default_fwg_rules. + ingress_destination_ipv4_address, + } + + # 2. Firewall rule for ingress IPv6 packets (DROP by default) + in_fwr_v6 = copy.deepcopy(in_fwr_v4) + in_fwr_v6['description'] = 'default ingress rule for IPv6' + in_fwr_v6['name'] = 'default ingress ipv6' + in_fwr_v6['ip_version'] = nl_constants.IP_VERSION_6 + in_fwr_v6['source_ip_address'] = \ + cfg.CONF.default_fwg_rules.ingress_source_ipv6_address + in_fwr_v6['destination_ip_address'] = \ + cfg.CONF.default_fwg_rules.ingress_destination_ipv6_address + + # 3. Firewall rule for egress IPv4 packets (ALLOW by default) + eg_fwr_v4 = copy.deepcopy(in_fwr_v4) + eg_fwr_v4['description'] = 'default egress rule for IPv4' + eg_fwr_v4['name'] = 'default egress ipv4' + eg_fwr_v4['action'] = cfg.CONF.default_fwg_rules.egress_action + eg_fwr_v4['source_port'] = \ + cfg.CONF.default_fwg_rules.egress_source_port + eg_fwr_v4['source_ip_address'] = \ + cfg.CONF.default_fwg_rules.egress_source_ipv4_address + eg_fwr_v4['destination_port'] = \ + cfg.CONF.default_fwg_rules.egress_destination_port + eg_fwr_v4['destination_ip_address'] = \ + cfg.CONF.default_fwg_rules.egress_destination_ipv4_address + + # 4. Firewall rule for egress IPv6 packets (ALLOW by default) + eg_fwr_v6 = copy.deepcopy(in_fwr_v6) + eg_fwr_v6['description'] = 'default egress rule for IPv6' + eg_fwr_v6['name'] = 'default egress ipv6' + eg_fwr_v6['action'] = cfg.CONF.default_fwg_rules.egress_action + eg_fwr_v6['source_port'] = \ + cfg.CONF.default_fwg_rules.egress_source_port + eg_fwr_v6['source_ip_address'] = \ + cfg.CONF.default_fwg_rules.egress_source_ipv6_address + eg_fwr_v6['destination_port'] = \ + cfg.CONF.default_fwg_rules.egress_destination_port + eg_fwr_v6['destination_ip_address'] = \ + cfg.CONF.default_fwg_rules.egress_destination_ipv6_address + + return { + 'in_ipv4': self.create_firewall_rule(context, in_fwr_v4)['id'], + 'in_ipv6': self.create_firewall_rule(context, in_fwr_v6)['id'], + 'eg_ipv4': self.create_firewall_rule(context, eg_fwr_v4)['id'], + 'eg_ipv6': self.create_firewall_rule(context, eg_fwr_v6)['id'], + } + + def create_firewall_rule(self, context, firewall_rule): + fwr = firewall_rule + self._validate_fwr_protocol_parameters(fwr) + self._validate_fwr_src_dst_ip_version(fwr) + + src_port_min, src_port_max = self._get_min_max_ports_from_range( + fwr['source_port']) + dst_port_min, dst_port_max = self._get_min_max_ports_from_range( + fwr['destination_port']) + with context.session.begin(subtransactions=True): + fwr_db = FirewallRuleV2( + id=uuidutils.generate_uuid(), + tenant_id=fwr['tenant_id'], + name=fwr['name'], + description=fwr['description'], + protocol=fwr['protocol'], + ip_version=fwr['ip_version'], + source_ip_address=fwr['source_ip_address'], + destination_ip_address=fwr['destination_ip_address'], + source_port_range_min=src_port_min, + source_port_range_max=src_port_max, + destination_port_range_min=dst_port_min, + destination_port_range_max=dst_port_max, + action=fwr['action'], + enabled=fwr['enabled'], + shared=fwr['shared']) + context.session.add(fwr_db) + return self._make_firewall_rule_dict(fwr_db) + + def update_firewall_rule(self, context, id, firewall_rule): + fwr = firewall_rule + fwr_db = self._get_firewall_rule(context, id) + fwr_db_updated = self._make_firewall_rule_dict(fwr_db) + fwr_db_updated.update(fwr) + + self._validate_fwr_protocol_parameters(fwr_db_updated) + self._validate_fwr_src_dst_ip_version(fwr_db_updated) + if 'source_port' in fwr: + src_port_min, src_port_max = self._get_min_max_ports_from_range( + fwr['source_port']) + fwr['source_port_range_min'] = src_port_min + fwr['source_port_range_max'] = src_port_max + del fwr['source_port'] + if 'destination_port' in fwr: + dst_port_min, dst_port_max = self._get_min_max_ports_from_range( + fwr['destination_port']) + fwr['destination_port_range_min'] = dst_port_min + fwr['destination_port_range_max'] = dst_port_max + del fwr['destination_port'] + with context.session.begin(subtransactions=True): + fwr_db.update(fwr) + # if the rule on a policy, fix audited flag + fwp_ids = self.get_policies_with_rule(context, id) + for fwp_id in fwp_ids: + fwp_db = self._get_firewall_policy(context, fwp_id) + fwp_db['audited'] = False + return self._make_firewall_rule_dict(fwr_db) + + def delete_firewall_rule(self, context, id): + with context.session.begin(subtransactions=True): + fwr = self._get_firewall_rule(context, id) + # make sure rule is not associated with any policy + if self.get_policies_with_rule(context, id): + raise f_exc.FirewallRuleInUse(firewall_rule_id=id) + context.session.delete(fwr) + + def insert_rule(self, context, id, rule_info): + firewall_rule_id = rule_info['firewall_rule_id'] + # ensure rule is not already assigned to the policy + self._ensure_rule_not_already_associated(context, id, firewall_rule_id) + insert_before = True + ref_firewall_rule_id = None + if 'insert_before' in rule_info: + ref_firewall_rule_id = rule_info['insert_before'] + if not ref_firewall_rule_id and 'insert_after' in rule_info: + # If insert_before is set, we will ignore insert_after. + ref_firewall_rule_id = rule_info['insert_after'] + insert_before = False + with context.session.begin(subtransactions=True): + fwr_db = self._get_firewall_rule(context, firewall_rule_id) + fwp_db = self._get_firewall_policy(context, id) + self._check_firewall_rule_conflict(fwr_db, fwp_db) + if ref_firewall_rule_id: + # If reference_firewall_rule_id is set, the new rule + # is inserted depending on the value of insert_before. + # If insert_before is set, the new rule is inserted before + # reference_firewall_rule_id, and if it is not set the new + # rule is inserted after reference_firewall_rule_id. + fwpra_db = self._get_policy_rule_association( + context, id, ref_firewall_rule_id) + if insert_before: + position = fwpra_db.position + else: + position = fwpra_db.position + 1 + else: + # If reference_firewall_rule_id is not set, it is assumed + # that the new rule needs to be inserted at the top. + # insert_before field is ignored. + # So default insertion is always at the top. + # Also note that position numbering starts at 1. + position = 1 + return self._process_rule_for_policy(context, id, firewall_rule_id, + position, None) + + def remove_rule(self, context, id, rule_info): + firewall_rule_id = rule_info['firewall_rule_id'] + with context.session.begin(subtransactions=True): + self._get_firewall_rule(context, firewall_rule_id) + fwpra_db = self._get_policy_rule_association(context, id, + firewall_rule_id) + return self._process_rule_for_policy(context, id, firewall_rule_id, + None, fwpra_db) + + def get_firewall_rule(self, context, id, fields=None): + fwr = self._get_firewall_rule(context, id) + policies = self.get_policies_with_rule(context, id) or None + return self._make_firewall_rule_dict(fwr, fields, policies=policies) + + def get_firewall_rules(self, context, filters=None, fields=None): + return model_query.get_collection( + context, FirewallRuleV2, self._make_firewall_rule_dict, + filters=filters, fields=fields) + + def _get_rules_in_policy(self, context, fwpid): + """Gets rules in a firewall policy""" + with context.session.begin(subtransactions=True): + fw_pol_rule_qry = context.session.query( + FirewallPolicyRuleAssociation).filter_by( + firewall_policy_id=fwpid) + fwp_rules = [entry.firewall_rule_id for entry in fw_pol_rule_qry] + return fwp_rules + + def get_policies_with_rule(self, context, fwrid): + """Gets rules in a firewall policy""" + with context.session.begin(subtransactions=True): + fw_pol_rule_qry = context.session.query( + FirewallPolicyRuleAssociation).filter_by( + firewall_rule_id=fwrid) + fwps = [entry.firewall_policy_id for entry in fw_pol_rule_qry] + return fwps + + def _set_rules_in_policy_rule_assoc(self, context, fwp_db, fwp): + # Pull the rules and add it to policy - rule association table + # Set the position (this can be used in the making the dict) + # might be good to track the last position + rule_id_list = fwp['firewall_rules'] + if not rule_id_list: + return + position = 0 + with context.session.begin(subtransactions=True): + for rule_id in rule_id_list: + fw_pol_rul_db = FirewallPolicyRuleAssociation( + firewall_policy_id=fwp_db['id'], + firewall_rule_id=rule_id, + position=position) + context.session.add(fw_pol_rul_db) + position += 1 + + def _check_rules_for_policy_is_valid(self, context, fwp, fwp_db, + rule_id_list, filters): + rules_in_fwr_db = model_query.get_collection_query( + context, FirewallRuleV2, filters=filters) + rules_dict = dict((fwr_db['id'], fwr_db) for fwr_db in rules_in_fwr_db) + for fwrule_id in rule_id_list: + if fwrule_id not in rules_dict: + # Bail as soon as we find an invalid rule. + raise f_exc.FirewallRuleNotFound( + firewall_rule_id=fwrule_id) + if 'shared' in fwp: + if fwp['shared'] and not rules_dict[fwrule_id]['shared']: + raise f_exc.FirewallRuleSharingConflict( + firewall_rule_id=fwrule_id, + firewall_policy_id=fwp_db['id']) + elif fwp_db['shared'] and not rules_dict[fwrule_id]['shared']: + raise f_exc.FirewallRuleSharingConflict( + firewall_rule_id=fwrule_id, + firewall_policy_id=fwp_db['id']) + else: + # the policy is not shared, the rule and policy should be in + # the same project if the rule is not shared. + if not rules_dict[fwrule_id]['shared']: + if (rules_dict[fwrule_id]['tenant_id'] != fwp_db[ + 'tenant_id']): + raise f_exc.FirewallRuleConflict( + firewall_rule_id=fwrule_id, + project_id=rules_dict[fwrule_id]['tenant_id']) + + def _check_if_rules_shared_for_policy_shared(self, context, fwp_db, fwp): + if fwp['shared']: + rules_in_db = fwp_db.rule_associations + for entry in rules_in_db: + fwr_db = self._get_firewall_rule(context, + entry.firewall_rule_id) + if not fwr_db['shared']: + raise f_exc.FirewallPolicySharingConflict( + firewall_rule_id=fwr_db['id'], + firewall_policy_id=fwp_db['id']) + + def get_fwgs_with_policy(self, context, fwp_id): + with context.session.begin(subtransactions=True): + fwg_ing_pol_qry = context.session.query( + FirewallGroup).filter_by( + ingress_firewall_policy_id=fwp_id) + ing_fwg_ids = [entry.id for entry in fwg_ing_pol_qry] + fwg_eg_pol_qry = context.session.query( + FirewallGroup).filter_by( + egress_firewall_policy_id=fwp_id) + eg_fwg_ids = [entry.id for entry in fwg_eg_pol_qry] + return ing_fwg_ids, eg_fwg_ids + + def _check_fwgs_associated_with_policy_in_same_project(self, context, + fwp_id, + fwp_tenant_id): + with context.session.begin(subtransactions=True): + fwg_with_fwp_id_db = context.session.query(FirewallGroup).filter( + or_(FirewallGroup.ingress_firewall_policy_id == fwp_id, + FirewallGroup.egress_firewall_policy_id == fwp_id)) + for entry in fwg_with_fwp_id_db: + if entry.tenant_id != fwp_tenant_id: + raise f_exc.FirewallPolicyInUse( + firewall_policy_id=fwp_id) + + def _delete_all_rules_from_policy(self, context, fwp_db): + """Deletes all FirewallPolicyRuleAssociation objects + + fwp_db is an DB dict representing firewall policy. + Returns a dictionary with updated rule_associations. + """ + for rule_id in [rule_assoc.firewall_rule_id + for rule_assoc in fwp_db['rule_associations']]: + fwpra_db = self._get_policy_rule_association( + context, fwp_db['id'], rule_id) + fwp_db.rule_associations.remove(fwpra_db) + context.session.delete(fwpra_db) + fwp_db.rule_associations = [] + return fwp_db + + def _set_rules_for_policy(self, context, firewall_policy_db, fwp): + rule_id_list = fwp['firewall_rules'] + fwp_db = firewall_policy_db + with context.session.begin(subtransactions=True): + if not rule_id_list: + self._delete_all_rules_from_policy(context, fwp_db) + return + # We will first check if the new list of rules is valid + filters = {'firewall_rule_id': [r_id for r_id in rule_id_list]} + # Run a validation on the Firewall Rules table + self._check_rules_for_policy_is_valid(context, fwp, fwp_db, + rule_id_list, filters) + # new rules are valid, lets delete the old association + self._delete_all_rules_from_policy(context, fwp_db) + # and add in the new association + self._set_rules_in_policy_rule_assoc(context, fwp_db, fwp) + # we need care about the associations related with this policy + # and its rules only. + filters['firewall_policy_id'] = [fwp_db['id']] + rules_in_fpol_rul_db = model_query.get_collection_query( + context, + FirewallPolicyRuleAssociation, + filters=filters) + rules_dict = dict((fpol_rul_db['firewall_rule_id'], fpol_rul_db) + for fpol_rul_db in rules_in_fpol_rul_db) + fwp_db.rule_associations = [] + for fwrule_id in rule_id_list: + fwp_db.rule_associations.append(rules_dict[fwrule_id]) + fwp_db.rule_associations.reorder() + + def _create_default_firewall_policy(self, context, tenant_id, policy_type, + **kwargs): + fwrs = kwargs.get('firewall_rules', []) + description = kwargs.get('description', '') + name = (const.DEFAULT_FWP_INGRESS + if policy_type == 'ingress' else const.DEFAULT_FWP_EGRESS) + firewall_policy = { + 'name': name, + 'description': description, + 'audited': False, + 'shared': False, + 'firewall_rules': fwrs, + 'tenant_id': tenant_id, + } + return self._do_create_firewall_policy(context, firewall_policy) + + def _do_create_firewall_policy(self, context, firewall_policy): + fwp = firewall_policy + with context.session.begin(subtransactions=True): + fwp_db = FirewallPolicy( + id=uuidutils.generate_uuid(), + tenant_id=fwp['tenant_id'], + name=fwp['name'], + description=fwp['description'], + audited=fwp['audited'], + shared=fwp['shared']) + context.session.add(fwp_db) + self._set_rules_for_policy(context, fwp_db, fwp) + return self._make_firewall_policy_dict(fwp_db) + + def create_firewall_policy(self, context, firewall_policy): + self._ensure_not_default_resource(firewall_policy, 'firewall_policy') + return self._do_create_firewall_policy(context, firewall_policy) + + def update_firewall_policy(self, context, id, firewall_policy): + fwp = firewall_policy + with context.session.begin(subtransactions=True): + fwp_db = self._get_firewall_policy(context, id) + self._ensure_not_default_resource(fwp_db, 'firewall_policy', + action="update") + if not fwp.get('shared', True): + # an update is setting shared to False, make sure associated + # firewall groups are in the same project. + self._check_fwgs_associated_with_policy_in_same_project( + context, id, fwp_db['tenant_id']) + if 'shared' in fwp and 'firewall_rules' not in fwp: + self._check_if_rules_shared_for_policy_shared( + context, fwp_db, fwp) + if 'firewall_rules' in fwp: + self._set_rules_for_policy(context, fwp_db, fwp) + del fwp['firewall_rules'] + if 'audited' not in fwp: + fwp['audited'] = False + fwp_db.update(fwp) + return self._make_firewall_policy_dict(fwp_db) + + def delete_firewall_policy(self, context, id): + with context.session.begin(subtransactions=True): + fwp_db = self._get_firewall_policy(context, id) + # check if policy in use + qry = context.session.query(FirewallGroup) + if qry.filter_by(ingress_firewall_policy_id=id).first(): + raise f_exc.FirewallPolicyInUse(firewall_policy_id=id) + elif qry.filter_by(egress_firewall_policy_id=id).first(): + raise f_exc.FirewallPolicyInUse(firewall_policy_id=id) + else: + fwp_db = self._delete_all_rules_from_policy(context, fwp_db) + context.session.delete(fwp_db) + + def get_firewall_policy(self, context, id, fields=None): + fwp = self._get_firewall_policy(context, id) + return self._make_firewall_policy_dict(fwp, fields) + + def get_firewall_policies(self, context, filters=None, fields=None): + return model_query.get_collection( + context, FirewallPolicy, self._make_firewall_policy_dict, + filters=filters, fields=fields) + + def _set_ports_for_firewall_group(self, context, fwg_db, fwg): + port_id_list = fwg['ports'] + if not port_id_list: + return + + exc_ports = [] + for port_id in port_id_list: + try: + with context.session.begin(subtransactions=True): + fwg_port_db = FirewallGroupPortAssociation( + firewall_group_id=fwg_db['id'], + port_id=port_id) + context.session.add(fwg_port_db) + except db_exc.DBDuplicateEntry: + exc_ports.append(port_id) + if exc_ports: + raise f_exc.FirewallGroupPortInUse(port_ids=exc_ports) + + def get_ports_in_firewall_group(self, context, firewall_group_id): + """Get the Ports associated with the firewall group.""" + with context.session.begin(subtransactions=True): + fw_group_port_qry = context.session.query( + FirewallGroupPortAssociation) + fw_group_port_rows = fw_group_port_qry.filter_by( + firewall_group_id=firewall_group_id) + fw_ports = [entry.port_id for entry in fw_group_port_rows] + return fw_ports + + def _delete_ports_in_firewall_group(self, context, firewall_group_id): + """Delete the Ports associated with the firewall group.""" + with context.session.begin(subtransactions=True): + fw_group_port_qry = context.session.query( + FirewallGroupPortAssociation) + fw_group_port_qry.filter_by( + firewall_group_id=firewall_group_id).delete() + return + + def _get_default_fwg_id(self, context, tenant_id): + """Returns an id of default firewall group for given tenant or None""" + default_fwg = model_query.query_with_hooks( + context, FirewallGroup).filter_by( + project_id=tenant_id, name=const.DEFAULT_FWG).first() + if default_fwg: + return default_fwg.id + return None + + def get_fwg_attached_to_port(self, context, port_id): + """Return a firewall group ID that is attached to a given port""" + fwg_port = model_query.query_with_hooks( + context, FirewallGroupPortAssociation).\ + filter_by(port_id=port_id).first() + if fwg_port: + return fwg_port.firewall_group_id + return None + + def get_fwg_ports_in_tenant(self, context, tenant_id): + """Return a list of ports under a given tenant""" + try: + fwg_id = FirewallGroupPortAssociation.firewall_group_id + with context.session.begin(subtransactions=True): + port_qry = context.session.query( + FirewallGroupPortAssociation.port_id).join( + FirewallGroup, FirewallGroup.id == fwg_id).filter( + FirewallGroup.tenant_id == tenant_id).all() + return list({port for (port,) in port_qry}) + except exc.NoResultFound: + return [] + + def _ensure_default_firewall_group(self, context, tenant_id): + """Create a default firewall group if one doesn't exist for a tenant + + Returns the default firewall group id for a given tenant. + """ + exists = self._get_default_fwg_id(context, tenant_id) + if exists: + return exists + + try: + # NOTE(cby): default fwg not created => we try to create it! + with db_api.CONTEXT_WRITER.using(context): + + fwr_ids = self._create_default_firewall_rules( + context, tenant_id) + ingress_fwp = { + 'description': 'Ingress firewall policy', + 'firewall_rules': [fwr_ids['in_ipv4'], + fwr_ids['in_ipv6']], + } + egress_fwp = { + 'description': 'Egress firewall policy', + 'firewall_rules': [fwr_ids['eg_ipv4'], + fwr_ids['eg_ipv6']], + } + ingress_fwp_db = self._create_default_firewall_policy( + context, tenant_id, 'ingress', **ingress_fwp) + egress_fwp_db = self._create_default_firewall_policy( + context, tenant_id, 'egress', **egress_fwp) + + fwg = { + 'name': const.DEFAULT_FWG, + 'tenant_id': tenant_id, + 'ingress_firewall_policy_id': ingress_fwp_db['id'], + 'egress_firewall_policy_id': egress_fwp_db['id'], + 'ports': [], + 'shared': False, + 'status': nl_constants.INACTIVE, + 'admin_state_up': True, + 'description': 'Default firewall group', + } + fwg_db = self._create_firewall_group( + context, fwg, default_fwg=True) + context.session.add(DefaultFirewallGroup( + firewall_group_id=fwg_db['id'], + project_id=tenant_id)) + return fwg_db['id'] + + except db_exc.DBDuplicateEntry: + # NOTE(cby): default fwg created concurrently + LOG.debug("Default FWG was concurrently created") + return self._get_default_fwg_id(context, tenant_id) + + def _create_firewall_group(self, context, firewall_group, + default_fwg=False): + """Create a firewall group + + If default_fwg is True then a default firewall group is being created + for a given tenant. + """ + fwg = firewall_group + tenant_id = fwg['tenant_id'] + if firewall_group.get('status') is None: + fwg['status'] = nl_constants.CREATED + + if default_fwg: + # A default firewall group is being created. + default_fwg_id = self._get_default_fwg_id(context, tenant_id) + if default_fwg_id is not None: + # Default fwg for a given tenant exists, fetch it and return + return self.get_firewall_group(context, default_fwg_id) + else: + # An ordinary firewall group is being created BUT let's make sure + # that a default firewall group for given tenant exists + self._ensure_default_firewall_group(context, tenant_id) + + with context.session.begin(subtransactions=True): + fwg_db = FirewallGroup( + id=uuidutils.generate_uuid(), + tenant_id=tenant_id, + name=fwg['name'], + description=fwg['description'], + status=fwg['status'], + ingress_firewall_policy_id=fwg['ingress_firewall_policy_id'], + egress_firewall_policy_id=fwg['egress_firewall_policy_id'], + admin_state_up=fwg['admin_state_up'], + shared=fwg['shared']) + context.session.add(fwg_db) + self._set_ports_for_firewall_group(context, fwg_db, fwg) + return self._make_firewall_group_dict(fwg_db) + + def create_firewall_group(self, context, firewall_group): + self._ensure_not_default_resource(firewall_group, 'firewall_group') + return self._create_firewall_group(context, firewall_group) + + def update_firewall_group(self, context, id, firewall_group): + fwg = firewall_group + # make sure that no group can be updated to have name=default + self._ensure_not_default_resource(fwg, 'firewall_group') + with context.session.begin(subtransactions=True): + fwg_db = self.get_firewall_group(context, id) + if _is_default(fwg_db): + attrs = [ + 'name', 'description', 'admin_state_up', + 'ingress_firewall_policy_id', 'egress_firewall_policy_id' + ] + if context.is_admin: + attrs = ['name'] + for attr in attrs: + if attr in fwg: + raise FirewallDefaultObjectUpdateRestricted( + resource_type='Firewall Group', + resource_id=fwg_db['id']) + if 'ports' in fwg: + LOG.debug("Ports are updated in Firewall Group") + self._delete_ports_in_firewall_group(context, id) + self._set_ports_for_firewall_group(context, fwg_db, fwg) + del fwg['ports'] + # If fwg is empty, skip updating + if fwg: + count = context.session.query( + FirewallGroup).filter_by(id=id).update(fwg) + if not count: + raise f_exc.FirewallGroupNotFound(firewall_id=id) + return self.get_firewall_group(context, id) + + def update_firewall_group_status(self, context, id, status, not_in=None): + """Conditionally update firewall_group status. + Status transition is performed only if firewall is not in the specified + states as defined by 'not_in' list. + """ + # filter in_ wants iterable objects, None isn't. + not_in = not_in or [] + with context.session.begin(subtransactions=True): + return (context.session.query(FirewallGroup). + filter(FirewallGroup.id == id). + filter(~FirewallGroup.status.in_(not_in)). + update({'status': status}, synchronize_session=False)) + + def delete_firewall_group(self, context, id): + # Note: Plugin should ensure that it's okay to delete if the + # firewall is active + + with context.session.begin(subtransactions=True): + # if no such group exists -> don't raise an exception according to + # 80fe2ba1, return None + try: + fwg_db = self._get_firewall_group(context, id) + except f_exc.FirewallGroupNotFound: + return + + if _is_default(fwg_db): + if context.is_admin: + # Like Rules in Default SG, when the Default FWG is deleted + # its associated Rules and policies would also be deleted. + # Delete fwg first and then associated policies + context.session.query( + FirewallGroup).filter_by(id=id).delete() + fwp = [fwg_db['ingress_firewall_policy_id'], + fwg_db['egress_firewall_policy_id']] + for fwp_id in fwp: + self.delete_firewall_policy(context, fwp_id) + else: + # only admin can delete default fwg + raise f_exc.FirewallGroupCannotRemoveDefault() + else: + context.session.query( + FirewallGroup).filter_by(id=id).delete() + + def _ensure_not_default_resource(self, resource_dict, r_type, action=None): + """Checks that a resource is not default by checking its name + + A resource_dict can be either a dictionary in form {r_type : {}} or a + serialized object from db. + + Action is used to determine type of exception to be raised. + """ + resource = resource_dict.get(r_type) or resource_dict + if r_type == 'firewall_group': + if resource.get('name', '') == const.DEFAULT_FWG: + if action == "update": + raise FirewallDefaultObjectUpdateRestricted( + resource_type='Firewall Group', + resource_id=resource['id']) + raise FirewallDefaultParameterExists( + resource_type='Firewall Group', name=resource['name']) + elif r_type == 'firewall_policy': + if resource.get('name', '') in [const.DEFAULT_FWP_INGRESS, + const.DEFAULT_FWP_EGRESS]: + if action == "update": + raise FirewallDefaultObjectUpdateRestricted( + resource_type='Firewall Group', + resource_id=resource['id']) + raise FirewallDefaultParameterExists( + resource_type='Firewall Policy', name=resource['name']) + + def get_firewall_group(self, context, id, fields=None): + fw = self._get_firewall_group(context, id) + return self._make_firewall_group_dict(fw, fields) + + def get_firewall_groups(self, context, filters=None, fields=None): + if context.tenant_id: + tenant_id = filters.get('tenant_id') if filters else None + tenant_id = tenant_id[0] if tenant_id else context.tenant_id + self._ensure_default_firewall_group(context, tenant_id) + return model_query.get_collection( + context, FirewallGroup, self._make_firewall_group_dict, + filters=filters, fields=fields) + + +def _is_default(fwg_db): + return fwg_db['name'] == const.DEFAULT_FWG diff --git a/neutron_fwaas/db/migration/__init__.py b/neutron_fwaas/db/migration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/db/migration/alembic_migrations/README b/neutron_fwaas/db/migration/alembic_migrations/README new file mode 100644 index 000000000..98e4f9c44 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/neutron_fwaas/db/migration/alembic_migrations/__init__.py b/neutron_fwaas/db/migration/alembic_migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/db/migration/alembic_migrations/env.py b/neutron_fwaas/db/migration/alembic_migrations/env.py new file mode 100644 index 000000000..1ee1f6812 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/env.py @@ -0,0 +1,86 @@ +# Copyright 2014 OpenStack Foundation +# +# 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 logging import config as logging_config + +from alembic import context +from neutron_lib.db import model_base +from oslo_config import cfg +from oslo_db.sqlalchemy import session +import sqlalchemy as sa +from sqlalchemy import event + + +MYSQL_ENGINE = None +FWAAS_VERSION_TABLE = 'alembic_version_fwaas' +config = context.config +neutron_config = config.neutron_config +logging_config.fileConfig(config.config_file_name) +target_metadata = model_base.BASEV2.metadata + + +def set_mysql_engine(): + try: + mysql_engine = neutron_config.command.mysql_engine + except cfg.NoSuchOptError: + mysql_engine = None + + global MYSQL_ENGINE + MYSQL_ENGINE = (mysql_engine or + model_base.BASEV2.__table_args__['mysql_engine']) + + +def run_migrations_offline(): + set_mysql_engine() + + kwargs = dict() + if neutron_config.database.connection: + kwargs['url'] = neutron_config.database.connection + else: + kwargs['dialect_name'] = neutron_config.database.engine + kwargs['version_table'] = FWAAS_VERSION_TABLE + context.configure(**kwargs) + + with context.begin_transaction(): + context.run_migrations() + + +@event.listens_for(sa.Table, 'after_parent_attach') +def set_storage_engine(target, parent): + if MYSQL_ENGINE: + target.kwargs['mysql_engine'] = MYSQL_ENGINE + + +def run_migrations_online(): + set_mysql_engine() + engine = session.create_engine(neutron_config.database.connection) + + connection = engine.connect() + context.configure( + connection=connection, + target_metadata=target_metadata, + version_table=FWAAS_VERSION_TABLE + ) + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + engine.dispose() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/neutron_fwaas/db/migration/alembic_migrations/script.py.mako b/neutron_fwaas/db/migration/alembic_migrations/script.py.mako new file mode 100644 index 000000000..9e0b2ce21 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/script.py.mako @@ -0,0 +1,36 @@ +# Copyright ${create_date.year} +# +# 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. +# + +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision} +Create Date: ${create_date} + +""" + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +% if branch_labels: +branch_labels = ${repr(branch_labels)} +%endif + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +def upgrade(): + ${upgrades if upgrades else "pass"} diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/4202e3047e47_add_index_tenant_id.py b/neutron_fwaas/db/migration/alembic_migrations/versions/4202e3047e47_add_index_tenant_id.py new file mode 100644 index 000000000..67213a665 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/4202e3047e47_add_index_tenant_id.py @@ -0,0 +1,35 @@ +# Copyright 2015 OpenStack Foundation +# +# 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. +# + +"""add_index_tenant_id + +Revision ID: 4202e3047e47 +Revises: start_neutron_fwaas +Create Date: 2015-02-10 17:17:47.846764 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = '4202e3047e47' +down_revision = 'start_neutron_fwaas' + +TABLES = ['firewall_rules', 'firewalls', 'firewall_policies'] + + +def upgrade(): + for table in TABLES: + op.create_index(op.f('ix_%s_tenant_id' % table), + table, ['tenant_id'], unique=False) diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/540142f314f4_fwaas_router_insertion.py b/neutron_fwaas/db/migration/alembic_migrations/versions/540142f314f4_fwaas_router_insertion.py new file mode 100644 index 000000000..32e7d9ec2 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/540142f314f4_fwaas_router_insertion.py @@ -0,0 +1,62 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""FWaaS router insertion + +Revision ID: 540142f314f4 +Revises: 4202e3047e47 +Create Date: 2015-02-06 17:02:24.279337 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.engine import reflection + +# revision identifiers, used by Alembic. +revision = '540142f314f4' +down_revision = '4202e3047e47' + +SQL_STATEMENT = ( + "insert into firewall_router_associations " + "select " + "f.id as fw_id, r.id as router_id " + "from firewalls f, routers r " + "where " + "f.tenant_id=r.%s" +) + + +def upgrade(): + op.create_table('firewall_router_associations', + sa.Column('fw_id', sa.String(length=36), nullable=False), + sa.Column('router_id', sa.String(length=36), nullable=False), + sa.ForeignKeyConstraint(['fw_id'], ['firewalls.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['router_id'], ['routers.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('fw_id', 'router_id'), + ) + + # Depending on when neutron-fwaas is installed with neutron, this script + # may be run before or after the neutron core tables have had their + # tenant_id columns renamed to project_id. Account for both scenarios. + bind = op.get_bind() + insp = reflection.Inspector.from_engine(bind) + columns = insp.get_columns('routers') + if 'tenant_id' in [c['name'] for c in columns]: + op.execute(SQL_STATEMENT % 'tenant_id') + else: + op.execute(SQL_STATEMENT % 'project_id') diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/796c68dffbb_cisco_csr_fwaas.py b/neutron_fwaas/db/migration/alembic_migrations/versions/796c68dffbb_cisco_csr_fwaas.py new file mode 100644 index 000000000..b9f8f9703 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/796c68dffbb_cisco_csr_fwaas.py @@ -0,0 +1,45 @@ +# Copyright 2015 OpenStack Foundation +# +# 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. +# + +"""cisco_csr_fwaas + +Revision ID: 796c68dffbb +Revises: 540142f314f4 +Create Date: 2015-02-02 13:11:55.184112 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '796c68dffbb' +down_revision = '540142f314f4' + + +def upgrade(active_plugins=None, options=None): + + op.create_table('cisco_firewall_associations', + sa.Column('fw_id', sa.String(length=36), nullable=False), + sa.Column('port_id', sa.String(length=36), nullable=True), + sa.Column('direction', sa.String(length=16), nullable=True), + sa.Column('acl_id', sa.String(length=36), nullable=True), + sa.Column('router_id', sa.String(length=36), nullable=True), + sa.ForeignKeyConstraint(['fw_id'], ['firewalls.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['port_id'], ['ports.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('fw_id') + ) diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/CONTRACT_HEAD b/neutron_fwaas/db/migration/alembic_migrations/versions/CONTRACT_HEAD new file mode 100644 index 000000000..937996f64 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/CONTRACT_HEAD @@ -0,0 +1 @@ +fd38cd995cc0 diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron_fwaas/db/migration/alembic_migrations/versions/EXPAND_HEAD new file mode 100644 index 000000000..f323d45cd --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -0,0 +1 @@ +f24e0d5e5bff diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/kilo_release.py b/neutron_fwaas/db/migration/alembic_migrations/versions/kilo_release.py new file mode 100644 index 000000000..06ec04387 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/kilo_release.py @@ -0,0 +1,29 @@ +# 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. +# + +"""kilo + +Revision ID: kilo +Revises: 796c68dffbb +Create Date: 2015-04-16 00:00:00.000000 + +""" + +# revision identifiers, used by Alembic. +revision = 'kilo' +down_revision = '796c68dffbb' + + +def upgrade(): + """A no-op migration for marking the Kilo release.""" + pass diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/liberty/contract/67c8e8d61d5_initial.py b/neutron_fwaas/db/migration/alembic_migrations/versions/liberty/contract/67c8e8d61d5_initial.py new file mode 100644 index 000000000..1ea901f4c --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/liberty/contract/67c8e8d61d5_initial.py @@ -0,0 +1,38 @@ +# Copyright 2015 Red Hat 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. +# + +"""Initial Liberty no-op script. + +Revision ID: 67c8e8d61d5 +Revises: kilo +Create Date: 2015-07-28 22:18:13.330846 + +""" + +from neutron.db import migration +from neutron_lib.db import constants + + +# revision identifiers, used by Alembic. +revision = '67c8e8d61d5' +down_revision = 'kilo' +branch_labels = (constants.CONTRACT_BRANCH,) + +# milestone identifier, used by neutron-db-manage +neutron_milestone = [migration.LIBERTY] + + +def upgrade(): + pass diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/liberty/expand/4b47ea298795_add_reject_rule.py b/neutron_fwaas/db/migration/alembic_migrations/versions/liberty/expand/4b47ea298795_add_reject_rule.py new file mode 100644 index 000000000..8681d18ca --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/liberty/expand/4b47ea298795_add_reject_rule.py @@ -0,0 +1,47 @@ +# Copyright 2015 NEC Corporation. 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. + +"""add reject rule + +Revision ID: 4b47ea298795 +Revises: c40fbb377ad +Create Date: 2015-04-15 04:19:57.324584 + +""" + +import sqlalchemy as sa + +from neutron.db import migration + + +# revision identifiers, used by Alembic. +revision = '4b47ea298795' +down_revision = 'c40fbb377ad' + +# milestone identifier, used by neutron-db-manage +neutron_milestone = [migration.LIBERTY, migration.MITAKA] + + +new_action = sa.Enum('allow', 'deny', 'reject', name='firewallrules_action') + + +def upgrade(): + # NOTE: postgresql have a builtin ENUM type, so just altering the + # column won't works + # https://bitbucket.org/zzzeek/alembic/issues/270/altering-enum-type + # alter_enum that was already invented for such case in neutron + # https://github.com/openstack/neutron/blob/master/neutron/db/migration/__init__.py + + migration.alter_enum( + 'firewall_rules', 'action', enum_type=new_action, nullable=True) diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/liberty/expand/c40fbb377ad_initial.py b/neutron_fwaas/db/migration/alembic_migrations/versions/liberty/expand/c40fbb377ad_initial.py new file mode 100644 index 000000000..e0149a78f --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/liberty/expand/c40fbb377ad_initial.py @@ -0,0 +1,34 @@ +# Copyright 2015 Red Hat 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. +# + +"""Initial Liberty no-op script. + +Revision ID: c40fbb377ad +Revises: kilo +Create Date: 2015-07-28 22:18:13.321233 + +""" + +from neutron_lib.db import constants + + +# revision identifiers, used by Alembic. +revision = 'c40fbb377ad' +down_revision = 'kilo' +branch_labels = (constants.EXPAND_BRANCH,) + + +def upgrade(): + pass diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/mitaka/contract/458aa42b14b_fw_table_alter.py b/neutron_fwaas/db/migration/alembic_migrations/versions/mitaka/contract/458aa42b14b_fw_table_alter.py new file mode 100644 index 000000000..1f645e5b9 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/mitaka/contract/458aa42b14b_fw_table_alter.py @@ -0,0 +1,49 @@ +#Copyright 2015 OpenStack Foundation +# +# 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. +# + +"""fw_table_alter script to make column case sensitive + +Revision ID: 458aa42b14b +Revises: 67c8e8d61d5 +Create Date: 2015-09-16 11:47:43.061649 + +""" + +from alembic import op + +from neutron.db import migration + + +# revision identifiers, used by Alembic. +revision = '458aa42b14b' +down_revision = '67c8e8d61d5' + +# milestone identifier, used by neutron-db-manage +neutron_milestone = [migration.MITAKA] + + +FW_TAB_NAME = ['firewall_rules', 'firewall_policies', 'firewalls'] +SQL_STATEMENT_UPDATE_CMD = ( + "alter table %s " + "modify name varchar(255) " + "CHARACTER SET utf8 COLLATE utf8_bin" +) + + +def upgrade(): + context = op.get_context() + if context.bind.dialect.name == 'mysql': + for table in FW_TAB_NAME: + op.execute(SQL_STATEMENT_UPDATE_CMD % table) diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/newton/contract/f83a0b2964d0_rename_tenant_to_project.py b/neutron_fwaas/db/migration/alembic_migrations/versions/newton/contract/f83a0b2964d0_rename_tenant_to_project.py new file mode 100644 index 000000000..d3a00104e --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/newton/contract/f83a0b2964d0_rename_tenant_to_project.py @@ -0,0 +1,143 @@ +# Copyright 2016 +# +# 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. +# + +"""rename tenant to project + +Revision ID: f83a0b2964d0 +Revises: 458aa42b14b +Create Date: 2016-07-14 13:11:53.112622 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.engine import reflection + +from neutron.db import migration + + +# revision identifiers, used by Alembic. +revision = 'f83a0b2964d0' +down_revision = '458aa42b14b' + +# milestone identifier, used by neutron-db-manage +neutron_milestone = [migration.NEWTON] + +_INSPECTOR = None + + +def get_inspector(): + """Reuse inspector""" + + global _INSPECTOR + + if _INSPECTOR: + return _INSPECTOR + + else: + bind = op.get_bind() + _INSPECTOR = reflection.Inspector.from_engine(bind) + + return _INSPECTOR + + +def get_tables(): + """ + Returns hardcoded list of tables which have ``tenant_id`` column. + + The list is hardcoded to match the state of the schema when this + upgrade script is run. + """ + + tables = [ + 'firewalls', + 'firewall_policies', + 'firewall_rules', + ] + + return tables + + +def get_columns(table): + """Returns list of columns for given table.""" + inspector = get_inspector() + return inspector.get_columns(table) + + +def get_data(): + """Returns combined list of tuples: [(table, column)]. + + The list is built from tables with a tenant_id column. + """ + + output = [] + tables = get_tables() + for table in tables: + columns = get_columns(table) + + for column in columns: + if column['name'] == 'tenant_id': + output.append((table, column)) + + return output + + +def alter_column(table, column): + old_name = 'tenant_id' + new_name = 'project_id' + + op.alter_column( + table_name=table, + column_name=old_name, + new_column_name=new_name, + existing_type=column['type'], + existing_nullable=column['nullable'] + ) + + +def recreate_index(index, table_name): + old_name = index['name'] + new_name = old_name.replace('tenant', 'project') + + op.drop_index(op.f(old_name), table_name) + op.create_index(new_name, table_name, ['project_id']) + + +def upgrade(): + """Code reused from + + Change-Id: I87a8ef342ccea004731ba0192b23a8e79bc382dc + """ + + inspector = get_inspector() + + data = get_data() + for table, column in data: + alter_column(table, column) + + indexes = inspector.get_indexes(table) + for index in indexes: + if 'tenant_id' in index['name']: + recreate_index(index, table) + + +def contract_creation_exceptions(): + """Special migration for the blueprint to support Keystone V3. + We drop all tenant_id columns and create project_id columns instead. + """ + return { + sa.Column: ['.'.join([table, 'project_id']) for table in get_tables()], + sa.Index: get_tables() + } diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/newton/expand/d6a12e637e28_neutron_fwaas_v2_0.py b/neutron_fwaas/db/migration/alembic_migrations/versions/newton/expand/d6a12e637e28_neutron_fwaas_v2_0.py new file mode 100644 index 000000000..d505fa120 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/newton/expand/d6a12e637e28_neutron_fwaas_v2_0.py @@ -0,0 +1,115 @@ +# Copyright 2016 OpenStack Foundation +# +# 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 v2.0 + +Revision ID: d6a12e637e28 +Revises: 4b47ea298795 +Create Date: 2016-06-08 19:57:13.848855 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from neutron.db import migration + + +# revision identifiers, used by Alembic. +revision = 'd6a12e637e28' +down_revision = '4b47ea298795' + +# milestone identifier, used by neutron-db-manage +neutron_milestone = [migration.NEWTON] + + +def get_enum(): + engine = op.get_bind().engine + # In PostgreSQL types created separately, so if type was already created in + # 4b47ea298795_add_reject_rule it should be created one time. + # Use parameter create_type=False for that. + if engine.name == 'postgresql': + return postgresql.ENUM('allow', 'deny', 'reject', + name='firewallrules_action', + create_type=False) + else: + return sa.Enum('allow', 'deny', 'reject', + name='firewallrules_action') + + +def upgrade(): + + op.create_table( + 'firewall_policies_v2', + sa.Column('id', sa.String(length=36), primary_key=True), + sa.Column('name', sa.String(length=255)), + sa.Column('description', sa.String(length=1024)), + sa.Column('project_id', sa.String(length=255), index=True), + sa.Column('audited', sa.Boolean), + sa.Column('public', sa.Boolean), + sa.Column('rule_count', sa.Integer)) + + op.create_table( + 'firewall_rules_v2', + sa.Column('id', sa.String(length=36), primary_key=True), + sa.Column('name', sa.String(length=255)), + sa.Column('description', sa.String(length=1024)), + sa.Column('project_id', sa.String(length=255), index=True), + sa.Column('protocol', sa.String(length=40)), + sa.Column('ip_version', sa.Integer), + sa.Column('source_ip_address', sa.String(length=46)), + sa.Column('destination_ip_address', sa.String(length=46)), + sa.Column('source_port_range_min', sa.Integer), + sa.Column('source_port_range_max', sa.Integer), + sa.Column('destination_port_range_min', sa.Integer), + sa.Column('destination_port_range_max', sa.Integer), + sa.Column('action', get_enum()), + sa.Column('public', sa.Boolean), + sa.Column('enabled', sa.Boolean)) + + op.create_table( + 'firewall_groups_v2', + sa.Column('id', sa.String(length=36), primary_key=True), + sa.Column('name', sa.String(length=255)), + sa.Column('description', sa.String(length=1024)), + sa.Column('project_id', sa.String(length=255), index=True), + sa.Column('status', sa.String(length=16)), + sa.Column('admin_state_up', sa.Boolean), + sa.Column('public', sa.Boolean), + sa.Column('egress_firewall_policy_id', sa.String(length=36), + sa.ForeignKey('firewall_policies_v2.id')), + sa.Column('ingress_firewall_policy_id', sa.String(length=36), + sa.ForeignKey('firewall_policies_v2.id'))) + + op.create_table( + 'firewall_group_port_associations_v2', + sa.Column('firewall_group_id', sa.String(length=36), + sa.ForeignKey('firewall_groups_v2.id', ondelete='CASCADE'), + nullable=False), + sa.Column('port_id', sa.String(length=36), + sa.ForeignKey('ports.id', ondelete='CASCADE'), + nullable=False) + ) + + op.create_table( + 'firewall_policy_rule_associations_v2', + sa.Column('firewall_policy_id', sa.String(length=36), + sa.ForeignKey('firewall_policies_v2.id', ondelete='CASCADE'), + nullable=False, primary_key=True), + sa.Column('firewall_rule_id', sa.String(length=36), + sa.ForeignKey('firewall_rules_v2.id', ondelete='CASCADE'), + nullable=False, primary_key=True), + sa.Column('position', sa.Integer)) diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/pike/contract/fd38cd995cc0_shared_attribute_for_firewall_resources.py b/neutron_fwaas/db/migration/alembic_migrations/versions/pike/contract/fd38cd995cc0_shared_attribute_for_firewall_resources.py new file mode 100644 index 000000000..4077ced0b --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/pike/contract/fd38cd995cc0_shared_attribute_for_firewall_resources.py @@ -0,0 +1,37 @@ +# 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. +# + +"""change shared attribute for firewall resource + +Revision ID: fd38cd995cc0 +Revises: f83a0b2964d0 +Create Date: 2017-03-31 14:22:21.063392 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'fd38cd995cc0' +down_revision = 'f83a0b2964d0' +depends_on = ('d6a12e637e28',) + + +def upgrade(): + op.alter_column('firewall_rules_v2', 'public', new_column_name='shared', + existing_type=sa.Boolean) + op.alter_column('firewall_groups_v2', 'public', new_column_name='shared', + existing_type=sa.Boolean) + op.alter_column('firewall_policies_v2', 'public', new_column_name='shared', + existing_type=sa.Boolean) diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/queens/expand/876782258a43_create_default_firewall_groups_table.py b/neutron_fwaas/db/migration/alembic_migrations/versions/queens/expand/876782258a43_create_default_firewall_groups_table.py new file mode 100644 index 000000000..e8f4f7786 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/queens/expand/876782258a43_create_default_firewall_groups_table.py @@ -0,0 +1,67 @@ +# Copyright 2017 FUJITSU LIMITED +# +# 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. +# + +"""create_default_firewall_groups_table + +Revision ID: 876782258a43 +Revises: d6a12e637e28 +Create Date: 2017-01-26 23:47:42.795504 + +""" + +from alembic import op +from neutron_lib.db import constants as db_constants +from neutron_lib import exceptions +import sqlalchemy as sa + +from neutron_fwaas._i18n import _ +from neutron_fwaas.common import fwaas_constants as const +from neutron_fwaas.common import resources + +# revision identifiers, used by Alembic. +revision = '876782258a43' +down_revision = 'd6a12e637e28' + + +class DuplicateDefaultFirewallGroup(exceptions.Conflict): + message = _("Duplicate Firewall group found named '%s'. " + "Database cannot be upgraded. Please, remove all duplicates " + "before upgrading the database.") % const.DEFAULT_FWG + + +def upgrade(): + op.create_table( + 'default_firewall_groups', + sa.Column('project_id', + sa.String(length=db_constants.PROJECT_ID_FIELD_SIZE), + nullable=False), + sa.Column('firewall_group_id', + sa.String(length=db_constants.UUID_FIELD_SIZE), + nullable=False), + sa.PrimaryKeyConstraint('project_id'), + sa.ForeignKeyConstraint(['firewall_group_id'], + ['firewall_groups_v2.id'], ondelete="CASCADE")) + + +def check_sanity(connection): + # check for already existing firewall groups with name == DEFAULT_FWG + insp = sa.engine.reflection.Inspector.from_engine(connection) + if 'firewall_groups_v2' not in insp.get_table_names(): + return [] + session = sa.orm.Session(bind=connection.connect()) + default_fwg = session.query(resources.FIREWALL_GROUP.name).filter( + resources.FIREWALL_GROUP.name == const.DEFAULT_FWG).first() + if default_fwg: + raise DuplicateDefaultFirewallGroup() diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/queens/expand/f24e0d5e5bff_uniq_firewallgroupportassociation0port.py b/neutron_fwaas/db/migration/alembic_migrations/versions/queens/expand/f24e0d5e5bff_uniq_firewallgroupportassociation0port.py new file mode 100644 index 000000000..e4f084445 --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/queens/expand/f24e0d5e5bff_uniq_firewallgroupportassociation0port.py @@ -0,0 +1,71 @@ +# Copyright 2017 Fujitsu Limited +# +# 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. +# + +"""uniq_firewallgroupportassociation0port + +Revision ID: f24e0d5e5bff +Revises: 876782258a43 +Create Date: 2017-11-08 15:55:40.990272 + +""" + +from alembic import op +from neutron_lib import exceptions +import sqlalchemy as sa + +from neutron._i18n import _ + +# revision identifiers, used by Alembic. +revision = 'f24e0d5e5bff' +down_revision = '876782258a43' + + +fwg_port_association = sa.Table( + 'firewall_group_port_associations_v2', sa.MetaData(), + sa.Column('firewall_group_id', sa.String(36)), + sa.Column('port_id', sa.String(36))) + + +class DuplicatePortRecordinFirewallGroupPortAssociation(exceptions.Conflict): + message = _("Duplicate port(s) %(port_id)s records exist in" + "firewall_group_port_associations_v2 table. Database cannot" + "be upgraded. Please remove all duplicated records before" + "upgrading the database.") + + +def upgrade(): + op.create_unique_constraint( + 'uniq_firewallgroupportassociation0port_id', + 'firewall_group_port_associations_v2', + ['port_id']) + + +def check_sanity(connection): + duplicated_port_ids = ( + get_duplicate_port_records_in_fwg_port_association(connection)) + if duplicated_port_ids: + raise DuplicatePortRecordinFirewallGroupPortAssociation( + port_id=",".join(duplicated_port_ids)) + + +def get_duplicate_port_records_in_fwg_port_association(connection): + insp = sa.engine.reflection.Inspector.from_engine(connection) + if 'firewall_group_port_associations_v2' not in insp.get_table_names(): + return [] + session = sa.orm.Session(bind=connection.connect()) + query = (session.query(fwg_port_association.c.port_id) + .group_by(fwg_port_association.c.port_id) + .having(sa.func.count() > 1)).all() + return [q[0] for q in query] diff --git a/neutron_fwaas/db/migration/alembic_migrations/versions/start_neutron_fwaas.py b/neutron_fwaas/db/migration/alembic_migrations/versions/start_neutron_fwaas.py new file mode 100644 index 000000000..81f49b8dd --- /dev/null +++ b/neutron_fwaas/db/migration/alembic_migrations/versions/start_neutron_fwaas.py @@ -0,0 +1,30 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""start neutron-fwaas chain + +Revision ID: start_neutron_fwaas +Revises: None +Create Date: 2014-12-09 18:42:08.262632 + +""" + +# revision identifiers, used by Alembic. +revision = 'start_neutron_fwaas' +down_revision = None + + +def upgrade(): + pass diff --git a/neutron_fwaas/db/models/__init__.py b/neutron_fwaas/db/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/db/models/head.py b/neutron_fwaas/db/models/head.py new file mode 100644 index 000000000..a6251ab21 --- /dev/null +++ b/neutron_fwaas/db/models/head.py @@ -0,0 +1,17 @@ +# 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 neutron_lib.db import model_base + + +def get_metadata(): + return model_base.BASEV2.metadata diff --git a/neutron_fwaas/extensions/__init__.py b/neutron_fwaas/extensions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/extensions/firewall_v2.py b/neutron_fwaas/extensions/firewall_v2.py new file mode 100644 index 000000000..401fd2803 --- /dev/null +++ b/neutron_fwaas/extensions/firewall_v2.py @@ -0,0 +1,302 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc + +from debtcollector import moves +from neutron.api.v2 import resource_helper +from neutron_lib.api.definitions import constants as api_const +from neutron_lib.api.definitions import firewall_v2 +from neutron_lib.api import extensions +from neutron_lib.exceptions import firewall_v2 as f_exc +from neutron_lib.services import base as service_base +from oslo_config import cfg +import six + +from neutron_fwaas._i18n import _ +from neutron_fwaas.common import fwaas_constants + + +FirewallGroupNotFound = moves.moved_class( + f_exc.FirewallGroupNotFound, 'FirewallGroupNotFound', __name__) +FirewallGroupInUse = moves.moved_class( + f_exc.FirewallGroupInUse, 'FirewallGroupInUse', __name__) +FirewallGroupInPendingState = moves.moved_class( + f_exc.FirewallGroupInPendingState, 'FirewallGroupInPendingState', __name__) +FirewallGroupPortInvalid = moves.moved_class( + f_exc.FirewallGroupPortInvalid, 'FirewallGroupPortInvalid', __name__) +FirewallGroupPortInvalidProject = moves.moved_class( + f_exc.FirewallGroupPortInvalidProject, 'FirewallGroupPortInvalidProject', + __name__) +FirewallGroupPortInUse = moves.moved_class( + f_exc.FirewallGroupPortInUse, 'FirewallGroupPortInUse', __name__) +FirewallPolicyNotFound = moves.moved_class( + f_exc.FirewallPolicyNotFound, 'FirewallPolicyNotFound', __name__) +FirewallPolicyInUse = moves.moved_class( + f_exc.FirewallPolicyInUse, 'FirewallPolicyInUse', __name__) +FirewallPolicyConflict = moves.moved_class( + f_exc.FirewallPolicyConflict, 'FirewallPolicyConflict', __name__) +FirewallRuleSharingConflict = moves.moved_class( + f_exc.FirewallRuleSharingConflict, 'FirewallRuleSharingConflict', + __name__) +FirewallPolicySharingConflict = moves.moved_class( + f_exc.FirewallPolicySharingConflict, 'FirewallPolicySharingConflict', + __name__) +FirewallRuleNotFound = moves.moved_class( + f_exc.FirewallRuleNotFound, 'FirewallRuleNotFound', __name__) +FirewallRuleInUse = moves.moved_class( + f_exc.FirewallRuleInUse, 'FirewallRuleInUse', __name__) +FirewallRuleNotAssociatedWithPolicy = moves.moved_class( + f_exc.FirewallRuleNotAssociatedWithPolicy, + 'FirewallRuleNotAssociatedWithPolicy', + __name__) +FirewallRuleInvalidProtocol = moves.moved_class( + f_exc.FirewallRuleInvalidProtocol, 'FirewallRuleInvalidProtocol', + __name__) +FirewallRuleInvalidAction = moves.moved_class( + f_exc.FirewallRuleInvalidAction, 'FirewallRuleInvalidAction', + __name__) +FirewallRuleInvalidICMPParameter = moves.moved_class( + f_exc.FirewallRuleInvalidICMPParameter, + 'FirewallRuleInvalidICMPParameter', __name__) +FirewallRuleWithPortWithoutProtocolInvalid = moves.moved_class( + f_exc.FirewallRuleWithPortWithoutProtocolInvalid, + 'FirewallRuleWithPortWithoutProtocolInvalid', __name__) +FirewallRuleInvalidPortValue = moves.moved_class( + f_exc.FirewallRuleInvalidPortValue, 'FirewallRuleInvalidPortValue', + __name__) +FirewallRuleInfoMissing = moves.moved_class( + f_exc.FirewallRuleInfoMissing, 'FirewallRuleInfoMissing', __name__) +FirewallIpAddressConflict = moves.moved_class( + f_exc.FirewallIpAddressConflict, 'FirewallIpAddressConflict', __name__) +FirewallInternalDriverError = moves.moved_class( + f_exc.FirewallInternalDriverError, 'FirewallInternalDriverError', __name__) +FirewallRuleConflict = moves.moved_class( + f_exc.FirewallRuleConflict, 'FirewallRuleConflict', __name__) +FirewallRuleAlreadyAssociated = moves.moved_class( + f_exc.FirewallRuleAlreadyAssociated, 'FirewallRuleAlreadyAssociated', + __name__) + +default_fwg_rules_opts = [ + cfg.StrOpt('ingress_action', + default=api_const.FWAAS_DENY, + help=_('Firewall group rule action allow or ' + 'deny or reject for ingress. ' + 'Default is deny.')), + cfg.StrOpt('ingress_source_ipv4_address', + default=None, + help=_('IPv4 source address for ingress ' + '(address or address/netmask). ' + 'Default is None.')), + cfg.StrOpt('ingress_source_ipv6_address', + default=None, + help=_('IPv6 source address for ingress ' + '(address or address/netmask). ' + 'Default is None.')), + cfg.StrOpt('ingress_source_port', + default=None, + help=_('Source port number or range ' + '(min:max) for ingress. ' + 'Default is None.')), + cfg.StrOpt('ingress_destination_ipv4_address', + default=None, + help=_('IPv4 destination address for ingress ' + '(address or address/netmask). ' + 'Default is None.')), + cfg.StrOpt('ingress_destination_ipv6_address', + default=None, + help=_('IPv6 destination address for ingress ' + '(address or address/netmask). ' + 'Default is deny.')), + cfg.StrOpt('ingress_destination_port', + default=None, + help=_('Destination port number or range ' + '(min:max) for ingress. ' + 'Default is None.')), + cfg.StrOpt('egress_action', + default=api_const.FWAAS_ALLOW, + help=_('Firewall group rule action allow or ' + 'deny or reject for egress. ' + 'Default is allow.')), + cfg.StrOpt('egress_source_ipv4_address', + default=None, + help=_('IPv4 source address for egress ' + '(address or address/netmask). ' + 'Default is None.')), + cfg.StrOpt('egress_source_ipv6_address', + default=None, + help=_('IPv6 source address for egress ' + '(address or address/netmask). ' + 'Default is deny.')), + cfg.StrOpt('egress_source_port', + default=None, + help=_('Source port number or range ' + '(min:max) for egress. ' + 'Default is None.')), + cfg.StrOpt('egress_destination_ipv4_address', + default=None, + help=_('IPv4 destination address for egress ' + '(address or address/netmask). ' + 'Default is deny.')), + cfg.StrOpt('egress_destination_ipv6_address', + default=None, + help=_('IPv6 destination address for egress ' + '(address or address/netmask). ' + 'Default is deny.')), + cfg.StrOpt('egress_destination_port', + default=None, + help=_('Destination port number or range ' + '(min:max) for egress. ' + 'Default is None.')), + cfg.BoolOpt('shared', + default=False, + help=_('Firewall group rule shared. ' + 'Default is False.')), + cfg.StrOpt('protocol', + default=None, + help=_('Network protocols (tcp, udp, ...). ' + 'Default is None.')), + cfg.BoolOpt('enabled', + default=True, + help=_('Firewall group rule enabled. ' + 'Default is True.')), +] +firewall_quota_opts = [ + cfg.IntOpt('quota_firewall_group', + default=10, + help=_('Number of firewall groups allowed per tenant. ' + 'A negative value means unlimited.')), + cfg.IntOpt('quota_firewall_policy', + default=10, + help=_('Number of firewall policies allowed per tenant. ' + 'A negative value means unlimited.')), + cfg.IntOpt('quota_firewall_rule', + default=100, + help=_('Number of firewall rules allowed per tenant. ' + 'A negative value means unlimited.')), +] +cfg.CONF.register_opts(default_fwg_rules_opts, 'default_fwg_rules') +cfg.CONF.register_opts(firewall_quota_opts, 'QUOTAS') + + +# TODO(Reedip): Remove the convert_to functionality after bug1706061 is fixed. +def convert_to_string(value): + if value is not None: + return str(value) + return None + + +firewall_v2.RESOURCE_ATTRIBUTE_MAP[api_const.FIREWALL_RULES][ + 'source_port']['convert_to'] = convert_to_string +firewall_v2.RESOURCE_ATTRIBUTE_MAP[api_const.FIREWALL_RULES][ + 'destination_port']['convert_to'] = convert_to_string + + +class Firewall_v2(extensions.APIExtensionDescriptor): + api_definition = firewall_v2 + + @classmethod + def get_resources(cls): + special_mappings = {'firewall_policies': 'firewall_policy'} + plural_mappings = resource_helper.build_plural_mappings( + special_mappings, firewall_v2.RESOURCE_ATTRIBUTE_MAP) + return resource_helper.build_resource_info( + plural_mappings, firewall_v2.RESOURCE_ATTRIBUTE_MAP, + fwaas_constants.FIREWALL_V2, action_map=firewall_v2.ACTION_MAP, + register_quota=True) + + @classmethod + def get_plugin_interface(cls): + return Firewallv2PluginBase + + +@six.add_metaclass(abc.ABCMeta) +class Firewallv2PluginBase(service_base.ServicePluginBase): + + def get_plugin_type(self): + return fwaas_constants.FIREWALL_V2 + + def get_plugin_description(self): + return 'Firewall Service v2 Plugin' + + # Firewall Group + @abc.abstractmethod + def create_firewall_group(self, context, firewall_group): + pass + + @abc.abstractmethod + def delete_firewall_group(self, context, id): + pass + + @abc.abstractmethod + def get_firewall_group(self, context, id, fields=None): + pass + + @abc.abstractmethod + def get_firewall_groups(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def update_firewall_group(self, context, id, firewall_group): + pass + + # Firewall Policy + @abc.abstractmethod + def create_firewall_policy(self, context, firewall_policy): + pass + + @abc.abstractmethod + def delete_firewall_policy(self, context, id): + pass + + @abc.abstractmethod + def get_firewall_policy(self, context, id, fields=None): + pass + + @abc.abstractmethod + def get_firewall_policies(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def update_firewall_policy(self, context, id, firewall_policy): + pass + + # Firewall Rule + @abc.abstractmethod + def create_firewall_rule(self, context, firewall_rule): + pass + + @abc.abstractmethod + def delete_firewall_rule(self, context, id): + pass + + @abc.abstractmethod + def get_firewall_rule(self, context, id, fields=None): + pass + + @abc.abstractmethod + def get_firewall_rules(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def update_firewall_rule(self, context, id, firewall_rule): + pass + + @abc.abstractmethod + def insert_rule(self, context, id, rule_info): + pass + + @abc.abstractmethod + def remove_rule(self, context, id, rule_info): + pass diff --git a/neutron_fwaas/opts.py b/neutron_fwaas/opts.py new file mode 100644 index 000000000..4a375498a --- /dev/null +++ b/neutron_fwaas/opts.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. + +import neutron.conf.services.provider_configuration + +import neutron_fwaas.services.firewall.service_drivers.agents.\ + firewall_agent_api +import neutron_fwaas.extensions.firewall_v2 + + +def list_agent_opts(): + return [ + ('fwaas', + neutron_fwaas.services.firewall.service_drivers.agents. + firewall_agent_api.FWaaSOpts), + ] + + +def list_opts(): + return [ + ('quotas', + neutron_fwaas.extensions.firewall_v2.firewall_quota_opts), + ('service_providers', + neutron.conf.services.provider_configuration.serviceprovider_opts), + ('default_fwg_rules', + neutron_fwaas.extensions.firewall_v2.default_fwg_rules_opts), + ] diff --git a/neutron_fwaas/policies/__init__.py b/neutron_fwaas/policies/__init__.py new file mode 100644 index 000000000..0f156640e --- /dev/null +++ b/neutron_fwaas/policies/__init__.py @@ -0,0 +1,25 @@ +# 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 itertools + +from neutron_fwaas.policies import firewall_group +from neutron_fwaas.policies import firewall_policy +from neutron_fwaas.policies import firewall_rule + + +def list_rules(): + return itertools.chain( + firewall_group.list_rules(), + firewall_policy.list_rules(), + firewall_rule.list_rules(), + ) diff --git a/neutron_fwaas/policies/base.py b/neutron_fwaas/policies/base.py new file mode 100644 index 000000000..463ec829b --- /dev/null +++ b/neutron_fwaas/policies/base.py @@ -0,0 +1,17 @@ +# 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. + + +# TODO(amotoki): Define these in neutron or neutron-lib +RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner' +RULE_ADMIN_ONLY = 'rule:admin_only' +RULE_ANY = 'rule:regular_user' diff --git a/neutron_fwaas/policies/firewall_group.py b/neutron_fwaas/policies/firewall_group.py new file mode 100644 index 000000000..6e3a42b9a --- /dev/null +++ b/neutron_fwaas/policies/firewall_group.py @@ -0,0 +1,113 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from neutron_fwaas.policies import base + + +rules = [ + policy.RuleDefault( + 'shared_firewall_groups', + 'field:firewall_groups:shared=True', + 'Definition of shared firewall groups' + ), + + policy.DocumentedRuleDefault( + 'create_firewall_group', + base.RULE_ANY, + 'Create a firewall group', + [ + { + 'method': 'POST', + 'path': '/fwaas/firewall_groups', + }, + ] + ), + policy.DocumentedRuleDefault( + 'update_firewall_group', + base.RULE_ADMIN_OR_OWNER, + 'Update a firewall group', + [ + { + 'method': 'PUT', + 'path': '/fwaas/firewall_groups/{id}', + }, + ] + ), + policy.DocumentedRuleDefault( + 'delete_firewall_group', + base.RULE_ADMIN_OR_OWNER, + 'Delete a firewall group', + [ + { + 'method': 'DELETE', + 'path': '/fwaas/firewall_groups/{id}', + }, + ] + ), + + policy.DocumentedRuleDefault( + 'create_firewall_group:shared', + base.RULE_ADMIN_ONLY, + 'Create a shared firewall group', + [ + { + 'method': 'POST', + 'path': '/fwaas/firewall_groups', + }, + ] + ), + policy.DocumentedRuleDefault( + 'update_firewall_group:shared', + base.RULE_ADMIN_ONLY, + 'Update ``shared`` attribute of a firewall group', + [ + { + 'method': 'PUT', + 'path': '/fwaas/firewall_groups/{id}', + }, + ] + ), + # TODO(amotoki): Drop this rule as it has no effect. + policy.DocumentedRuleDefault( + 'delete_firewall_group:shared', + base.RULE_ADMIN_ONLY, + 'Delete a shared firewall group', + [ + { + 'method': 'DELETE', + 'path': '/fwaas/firewall_groups/{id}', + }, + ] + ), + + policy.DocumentedRuleDefault( + 'get_firewall_group', + 'rule:admin_or_owner or rule:shared_firewall_groups', + 'Get firewall groups', + [ + { + 'method': 'GET', + 'path': '/fwaas/firewall_groups', + }, + { + 'method': 'GET', + 'path': '/fwaas/firewall_groups/{id}', + }, + ] + ), +] + + +def list_rules(): + return rules diff --git a/neutron_fwaas/policies/firewall_policy.py b/neutron_fwaas/policies/firewall_policy.py new file mode 100644 index 000000000..03e37952d --- /dev/null +++ b/neutron_fwaas/policies/firewall_policy.py @@ -0,0 +1,113 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from neutron_fwaas.policies import base + + +rules = [ + policy.RuleDefault( + 'shared_firewall_policies', + 'field:firewall_policies:shared=True', + 'Definition of shared firewall policies' + ), + + policy.DocumentedRuleDefault( + 'create_firewall_policy', + base.RULE_ANY, + 'Create a firewall policy', + [ + { + 'method': 'POST', + 'path': '/fwaas/firewall_policies', + }, + ] + ), + policy.DocumentedRuleDefault( + 'update_firewall_policy', + base.RULE_ADMIN_OR_OWNER, + 'Update a firewall policy', + [ + { + 'method': 'PUT', + 'path': '/fwaas/firewall_policies/{id}', + }, + ] + ), + policy.DocumentedRuleDefault( + 'delete_firewall_policy', + base.RULE_ADMIN_OR_OWNER, + 'Delete a firewall policy', + [ + { + 'method': 'DELETE', + 'path': '/fwaas/firewall_policies/{id}', + }, + ] + ), + + policy.DocumentedRuleDefault( + 'create_firewall_policy:shared', + base.RULE_ADMIN_ONLY, + 'Create a shared firewall policy', + [ + { + 'method': 'POST', + 'path': '/fwaas/firewall_policies', + }, + ] + ), + policy.DocumentedRuleDefault( + 'update_firewall_policy:shared', + base.RULE_ADMIN_ONLY, + 'Update ``shared`` attribute of a firewall policy', + [ + { + 'method': 'PUT', + 'path': '/fwaas/firewall_policies/{id}', + }, + ] + ), + # TODO(amotoki): Drop this rule as it has no effect. + policy.DocumentedRuleDefault( + 'delete_firewall_policy:shared', + base.RULE_ADMIN_ONLY, + 'Delete a shread firewall policy', + [ + { + 'method': 'DELETE', + 'path': '/fwaas/firewall_policies/{id}', + }, + ] + ), + + policy.DocumentedRuleDefault( + 'get_firewall_policy', + 'rule:admin_or_owner or rule:shared_firewall_policies', + 'Get firewall policies', + [ + { + 'method': 'GET', + 'path': '/fwaas/firewall_policies', + }, + { + 'method': 'GET', + 'path': '/fwaas/firewall_policies/{id}', + }, + ] + ), +] + + +def list_rules(): + return rules diff --git a/neutron_fwaas/policies/firewall_rule.py b/neutron_fwaas/policies/firewall_rule.py new file mode 100644 index 000000000..eb0ce950e --- /dev/null +++ b/neutron_fwaas/policies/firewall_rule.py @@ -0,0 +1,136 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from neutron_fwaas.policies import base + + +rules = [ + policy.RuleDefault( + 'shared_firewall_rules', + 'field:firewall_rules:shared=True', + 'Definition of shared firewall rules' + ), + + policy.DocumentedRuleDefault( + 'create_firewall_rule', + base.RULE_ANY, + 'Create a firewall rule', + [ + { + 'method': 'POST', + 'path': '/fwaas/firewall_rules', + }, + ] + ), + policy.DocumentedRuleDefault( + 'update_firewall_rule', + base.RULE_ADMIN_OR_OWNER, + 'Update a firewall rule', + [ + { + 'method': 'PUT', + 'path': '/fwaas/firewall_rules/{id}', + }, + ] + ), + policy.DocumentedRuleDefault( + 'delete_firewall_rule', + base.RULE_ADMIN_OR_OWNER, + 'Delete a firewall rule', + [ + { + 'method': 'DELETE', + 'path': '/fwaas/firewall_rules/{id}', + }, + ] + ), + + policy.DocumentedRuleDefault( + 'create_firewall_rule:shared', + base.RULE_ADMIN_ONLY, + 'Create a shared firewall rule', + [ + { + 'method': 'POST', + 'path': '/fwaas/firewall_rules', + }, + ] + ), + policy.DocumentedRuleDefault( + 'update_firewall_rule:shared', + base.RULE_ADMIN_ONLY, + 'Update ``shared`` attribute of a firewall rule', + [ + { + 'method': 'PUT', + 'path': '/fwaas/firewall_rules/{id}', + }, + ] + ), + # TODO(amotoki): Drop this rule as it has no effect. + policy.DocumentedRuleDefault( + 'delete_firewall_rule:shared', + base.RULE_ADMIN_ONLY, + 'Delete a shread firewall rule', + [ + { + 'method': 'DELETE', + 'path': '/fwaas/firewall_rules/{id}', + }, + ] + ), + + policy.DocumentedRuleDefault( + 'get_firewall_rule', + 'rule:admin_or_owner or rule:shared_firewall_rules', + 'Get firewall rules', + [ + { + 'method': 'GET', + 'path': '/fwaas/firewall_rules', + }, + { + 'method': 'GET', + 'path': '/fwaas/firewall_rules/{id}', + }, + ] + ), + + policy.DocumentedRuleDefault( + 'insert_rule', + base.RULE_ADMIN_OR_OWNER, + 'Insert rule into a firewall policy', + [ + { + 'method': 'PUT', + 'path': '/fwaas/firewall_policies/{id}/insert_rule', + }, + ] + ), + policy.DocumentedRuleDefault( + 'remove_rule', + base.RULE_ADMIN_OR_OWNER, + 'Remove rule from a firewall policy', + [ + { + 'method': 'PUT', + 'path': '/fwaas/firewall_policies/{id}/remove_rule', + }, + ] + ), +] + + +def list_rules(): + return rules diff --git a/neutron_fwaas/privileged/__init__.py b/neutron_fwaas/privileged/__init__.py new file mode 100644 index 000000000..17831c1cc --- /dev/null +++ b/neutron_fwaas/privileged/__init__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2017 Thales Services SAS +# 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. + +from oslo_privsep import capabilities as c +from oslo_privsep import priv_context + +# It is expected that most (if not all) neutron-fwaas operations can be +# executed with these privileges. +default = priv_context.PrivContext( + __name__, + cfg_section='privsep', + pypath=__name__ + '.default', + # TODO(gus): CAP_SYS_ADMIN is required (only?) for manipulating + # network namespaces. SYS_ADMIN is a lot of scary powers, so + # consider breaking this out into a separate minimal context. + capabilities=[c.CAP_SYS_ADMIN, c.CAP_NET_ADMIN], +) diff --git a/neutron_fwaas/privileged/netfilter_log/__init__.py b/neutron_fwaas/privileged/netfilter_log/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/privileged/netfilter_log/libnetfilter_log.py b/neutron_fwaas/privileged/netfilter_log/libnetfilter_log.py new file mode 100644 index 000000000..fdd389e31 --- /dev/null +++ b/neutron_fwaas/privileged/netfilter_log/libnetfilter_log.py @@ -0,0 +1,331 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +import multiprocessing +import socket +import struct +import time + +import cffi +import eventlet +from eventlet.green import zmq +from neutron_lib.utils import runtime +from os_ken.lib import addrconv +from os_ken.lib.packet import arp +from os_ken.lib.packet import ether_types +from os_ken.lib.packet import ethernet +from os_ken.lib.packet import ipv4 +from os_ken.lib.packet import ipv6 +from oslo_log import log as logging +from oslo_serialization import jsonutils +from oslo_utils import encodeutils +from oslo_utils import excutils + +from neutron_fwaas._i18n import _ +from neutron_fwaas import privileged +from neutron_fwaas.privileged import utils as fwaas_utils + +LOG = logging.getLogger(__name__) + +# TODO(annp): consider to make a pub-sub pattern which allows other logging +# driver like snat log can consume libnetfilter_log + +NETFILTER_LOG = 'netfilter_log' +ADDR_IPC = "ipc:///var/run/nflog" +CDEF = ''' +typedef unsigned char u_int8_t; +typedef unsigned short int u_int16_t; +typedef unsigned int u_int32_t; + +struct nfulnl_msg_packet_hdr { + u_int16_t hw_protocol; // hw protocol (network order) + u_int8_t hook; // netfilter hook + u_int8_t _pad; +}; + +int nflog_fd(struct nflog_handle *h); +ssize_t recv(int sockfd, void *buf, size_t len, int flags); + +struct nflog_handle *nflog_open(void); +int nflog_close(struct nflog_handle *h); +int nflog_bind_pf(struct nflog_handle *h, u_int16_t pf); +int nflog_unbind_pf(struct nflog_handle *h, u_int16_t pf); +struct nflog_g_handle *nflog_bind_group(struct nflog_handle *h, u_int16_t num); +int nflog_unbind_group(struct nflog_g_handle *gh); + +static const u_int8_t NFULNL_COPY_PACKET; + +int nflog_set_mode(struct nflog_g_handle *gh, u_int8_t mode, unsigned int len); +int nflog_set_timeout(struct nflog_g_handle *gh, u_int32_t timeout); +int nflog_set_flags(struct nflog_g_handle *gh, u_int16_t flags); +int nflog_set_qthresh(struct nflog_g_handle *gh, u_int32_t qthresh); +int nflog_set_nlbufsiz(struct nflog_g_handle *gh, u_int32_t nlbufsiz); + +typedef int nflog_callback(struct nflog_g_handle *gh, +struct nfgenmsg *nfmsg, struct nflog_data *nfd, void *data); +int nflog_callback_register( +struct nflog_g_handle *gh, nflog_callback *cb, void *data); +int nflog_handle_packet(struct nflog_handle *h, char *buf, int len); + +struct nfulnl_msg_packet_hdr *nflog_get_msg_packet_hdr( +struct nflog_data *nfad); + +u_int16_t nflog_get_hwtype(struct nflog_data *nfad); +u_int16_t nflog_get_msg_packet_hwhdrlen(struct nflog_data *nfad); +char *nflog_get_msg_packet_hwhdr(struct nflog_data *nfad); +u_int32_t nflog_get_nfmark(struct nflog_data *nfad); +int nflog_get_timestamp(struct nflog_data *nfad, struct timeval *tv); +u_int32_t nflog_get_indev(struct nflog_data *nfad); +u_int32_t nflog_get_physindev(struct nflog_data *nfad); +u_int32_t nflog_get_outdev(struct nflog_data *nfad); +u_int32_t nflog_get_physoutdev(struct nflog_data *nfad); +struct nfulnl_msg_packet_hw *nflog_get_packet_hw(struct nflog_data *nfad); + +int nflog_get_payload(struct nflog_data *nfad, char **data); + +char *nflog_get_prefix(struct nflog_data *nfad); +''' + +ffi = None +libnflog = None + + +def init_library(): + """Load libnetfilter_log library""" + + global ffi + global libnflog + if not ffi: + ffi = cffi.FFI() + ffi.cdef(CDEF) + if not libnflog: + try: + libnflog = ffi.dlopen(NETFILTER_LOG) + except OSError: + msg = "Could not found libnetfilter-log" + raise Exception(msg) + + return ffi, libnflog + + +ffi, libnflog = init_library() + + +def _payload(nfa): + buf = ffi.new('char **') + pkt_len = libnflog.nflog_get_payload(nfa, buf) + if pkt_len <= 0: + return None + return ffi.buffer(buf[0], pkt_len)[:] + + +def decode(nfa): + """This function analyses nflog packet by using os-ken packet library.""" + + prefix = ffi.string(libnflog.nflog_get_prefix(nfa)) + packet_hdr = libnflog.nflog_get_msg_packet_hdr(nfa) + hw_proto = socket.ntohs(packet_hdr.hw_protocol) + + msg = '' + msg_packet_hwhdr = libnflog.nflog_get_msg_packet_hwhdr(nfa) + if msg_packet_hwhdr != ffi.NULL: + packet_hwhdr = ffi.string(msg_packet_hwhdr) + if len(packet_hwhdr) >= 12: + dst, src = struct.unpack_from('!6s6s', packet_hwhdr) + # Dump ethernet packet to get mac addresses + eth = ethernet.ethernet(addrconv.mac.bin_to_text(dst), + addrconv.mac.bin_to_text(src), + ethertype=hw_proto) + msg = str(eth) + + # Dump IP packet + pkt = _payload(nfa) + if hw_proto == ether_types.ETH_TYPE_IP: + ip_pkt, proto, data = ipv4.ipv4().parser(pkt) + msg += str(ip_pkt) + proto_pkt, a, b = proto.parser(data) + msg += str(proto_pkt) + elif hw_proto == ether_types.ETH_TYPE_IPV6: + ip_pkt, proto, data = ipv6.ipv6().parser(pkt) + proto_pkt, a, b = proto.parser(data) + msg += str(proto_pkt) + elif hw_proto == ether_types.ETH_TYPE_ARP: + ip_pkt, proto, data = arp.arp().parser(pkt) + msg += str(ip_pkt) + else: + msg += "Does not support hw_proto: " + str(hw_proto) + + return {'prefix': encodeutils.safe_decode(prefix), + 'msg': encodeutils.safe_decode(msg)} + + +class NFLogWrapper(object): + """A wrapper for libnetfilter_log api""" + + _instance = None + + def __init__(self): + self.nflog_g_hanldes = {} + + @classmethod + @runtime.synchronized("nflog-wrapper") + def _create_instance(cls): + if not cls.has_instance(): + cls._instance = cls() + + @classmethod + def has_instance(cls): + return cls._instance is not None + + @classmethod + def clear_instance(cls): + cls._instance = None + + @classmethod + def get_instance(cls): + # double checked locking + if not cls.has_instance(): + cls._create_instance() + return cls._instance + + def open(self): + self.nflog_handle = libnflog.nflog_open() + if not self.nflog_handle: + msg = _("Could not open nflog handle") + raise Exception(msg) + self._bind_pf() + + def close(self): + if self.nflog_handle: + libnflog.nflog_close(self.nflog_handle) + + def bind_group(self, group): + g_handle = libnflog.nflog_bind_group(self.nflog_handle, group) + if g_handle: + self.nflog_g_hanldes[group] = g_handle + self._set_mode(g_handle, 0x2, 0xffff) + self._set_callback(g_handle, self.cb) + + def _bind_pf(self): + for pf in (socket.AF_INET, socket.AF_INET6): + libnflog.nflog_unbind_pf(self.nflog_handle, pf) + libnflog.nflog_bind_pf(self.nflog_handle, pf) + + def unbind_group(self, group): + try: + g_handle = self.nflog_g_hanldes[group] + if g_handle: + libnflog.nflog_unbind_group(g_handle) + except Exception: + pass + + def _set_mode(self, g_handle, mode, len): + ret = libnflog.nflog_set_mode(g_handle, mode, len) + if ret != 0: + msg = _("Could not set mode for nflog") + raise Exception(msg) + + @ffi.callback("nflog_callback") + def cb(gh, nfmsg, nfa, data): + ev = decode(nfa) + msg = jsonutils.dumps(ev) + '\n' + ctx = zmq.Context(1) + pub = ctx.socket(zmq.XREQ) + pub.bind(ADDR_IPC) + pub.send(msg.encode('utf-8')) + pub.close() + return 0 + + def _set_callback(self, g_handle, cb): + + ret = libnflog.nflog_callback_register(g_handle, cb, ffi.NULL) + if ret != 0: + msg = _("Could not set callback for nflog") + raise Exception(msg) + + def run_loop(self): + fd = libnflog.nflog_fd(self.nflog_handle) + buff = ffi.new('char[]', 4096) + while True: + try: + pkt_len = libnflog.recv(fd, buff, 4096, 0) + except OSError as err: + # No buffer space available + if err.errno == 11: + continue + msg = _("Unknown exception") + raise Exception(msg) + if pkt_len > 0: + libnflog.nflog_handle_packet(self.nflog_handle, buff, pkt_len) + time.sleep(1.0) + + def start(self): + nflog_process = multiprocessing.Process(target=self.run_loop) + nflog_process.daemon = True + nflog_process.start() + return nflog_process.pid + + +@privileged.default.entrypoint +def run_nflog(namespace=None, group=0): + """Run a nflog process under a namespace + + This process will listen nflog packets, which are sent from kernel to + userspace. Then it decode these packets and send it to IPC address for log + application. + """ + + with fwaas_utils.in_namespace(namespace): + try: + handle = NFLogWrapper.get_instance() + handle.open() + handle.bind_group(group) + pid = handle.start() + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception("NFLOG thread died of an exception") + try: + handle.unbind_group(group) + handle.close() + except Exception: + pass + return pid + + +class NFLogApp(object): + """Log application for handling nflog packets""" + + callback = None + + def register_packet_handler(self, caller): + self.callback = caller + + def unregister_packet_handler(self): + self.callback = None + + def start(self): + def loop(): + while True: + if self.callback: + ctx = zmq.Context(1) + sub = ctx.socket(zmq.XREQ) + sub.connect(ADDR_IPC) + msg = sub.recv() + if len(msg): + self.callback(jsonutils.loads(msg)) + sub.close() + time.sleep(1.0) + # Spawn loop + eventlet.spawn_n(loop) diff --git a/neutron_fwaas/privileged/netlink_constants.py b/neutron_fwaas/privileged/netlink_constants.py new file mode 100644 index 000000000..376150fdc --- /dev/null +++ b/neutron_fwaas/privileged/netlink_constants.py @@ -0,0 +1,86 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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. +# +# Some parts are based on python-conntrack: +# Copyright (c) 2009-2011,2015 Andrew Grigorev +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import socket + + +CONNTRACK = 0 + +NFCT_O_PLAIN = 0 + +NFCT_OF_TIME_BIT = 1 +NFCT_OF_TIME = 1 << NFCT_OF_TIME_BIT + +NFCT_Q_DESTROY = 2 +NFCT_Q_FLUSH = 4 +NFCT_Q_DUMP = 5 +NFCT_T_DESTROY_BIT = 2 +NFCT_T_DESTROY = 1 << NFCT_T_DESTROY_BIT + +ATTR_IPV4_SRC = 0 +ATTR_IPV4_DST = 1 +ATTR_IPV6_SRC = 4 +ATTR_IPV6_DST = 5 +ATTR_PORT_SRC = 8 +ATTR_PORT_DST = 9 +ATTR_ICMP_TYPE = 12 +ATTR_ICMP_CODE = 13 +ATTR_ICMP_ID = 14 +ATTR_L3PROTO = 15 +ATTR_L4PROTO = 17 + +NFCT_T_NEW_BIT = 0 +NFCT_T_NEW = 1 << NFCT_T_NEW_BIT +NFCT_T_UPDATE_BIT = 1 +NFCT_T_UPDATE = 1 << NFCT_T_UPDATE_BIT +NFCT_T_DESTROY_BIT = 2 +NFCT_T_DESTROY = 1 << NFCT_T_DESTROY_BIT + +NFCT_T_ALL = NFCT_T_NEW | NFCT_T_UPDATE | NFCT_T_DESTROY + +NFCT_CB_CONTINUE = 1 +NFCT_CB_FAILURE = -1 + +NFNL_SUBSYS_CTNETLINK = 0 + +BUFFER = 1024 +# IPv6 address memory buffer +ADDR_BUFFER_6 = 16 +ADDR_BUFFER_4 = 4 + +IPVERSION_SOCKET = {4: socket.AF_INET, 6: socket.AF_INET6} +IPVERSION_BUFFER = {4: ADDR_BUFFER_4, 6: ADDR_BUFFER_6} diff --git a/neutron_fwaas/privileged/netlink_lib.py b/neutron_fwaas/privileged/netlink_lib.py new file mode 100644 index 000000000..7fac6d250 --- /dev/null +++ b/neutron_fwaas/privileged/netlink_lib.py @@ -0,0 +1,314 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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. +# +# Some parts are based on python-conntrack: +# Copyright (c) 2009-2011,2015 Andrew Grigorev +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import ctypes +from ctypes import util + +from oslo_log import log as logging + +from neutron_lib import constants + +from neutron_fwaas import privileged +from neutron_fwaas.privileged import netlink_constants as nl_constants +from neutron_fwaas.privileged import utils as fwaas_utils + +LOG = logging.getLogger(__name__) + +nfct_lib = util.find_library('netfilter_conntrack') +nfct = ctypes.CDLL(nfct_lib) +libc = ctypes.CDLL(util.find_library('libc.so.6')) + +# In unit tests the actual nfct library may not be installed, and since we +# don't make actual calls to it we don't want to add a hard dependency. +if nfct_lib: + # It's important that the types be defined properly on all of the functions + # we call from nfct, otherwise pointers can be truncated and cause + # segfaults. + nfct.nfct_set_attr.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_void_p] + nfct.nfct_set_attr_u8.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_uint8] + nfct.nfct_set_attr_u16.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_uint16] + nfct.nfct_snprintf.argtypes = [ctypes.c_char_p, + ctypes.c_uint, + ctypes.c_void_p, + ctypes.c_uint, + ctypes.c_uint, + ctypes.c_uint] + nfct.nfct_new.restype = ctypes.c_void_p + nfct.nfct_destroy.argtypes = [ctypes.c_void_p] + nfct.nfct_query.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_void_p] + nfct.nfct_callback_register.argtypes = [ctypes.c_void_p, + ctypes.c_int, + ctypes.c_void_p, + ctypes.c_void_p] + nfct.nfct_open.restype = ctypes.c_void_p + nfct.nfct_close.argtypes = [ctypes.c_void_p] + + +IP_VERSIONS = [constants.IP_VERSION_4, constants.IP_VERSION_6] +DATA_CALLBACK = None + +ATTR_POSITIONS = { + 'icmp': [('type', 6), ('code', 7), ('src', 4), ('dst', 5), ('id', 8)], + 'icmpv6': [('type', 6), ('code', 7), ('src', 4), ('dst', 5), ('id', 8)], + 'tcp': [('sport', 7), ('dport', 8), ('src', 5), ('dst', 6)], + 'udp': [('sport', 6), ('dport', 7), ('src', 4), ('dst', 5)] +} + +TARGET = {'src': {4: nl_constants.ATTR_IPV4_SRC, + 6: nl_constants.ATTR_IPV6_SRC}, + 'dst': {4: nl_constants.ATTR_IPV4_DST, + 6: nl_constants.ATTR_IPV6_DST}, + 'ipversion': {4: nl_constants.ATTR_L3PROTO, + 6: nl_constants.ATTR_L3PROTO}, + 'protocol': {4: nl_constants.ATTR_L4PROTO, + 6: nl_constants.ATTR_L4PROTO}, + 'code': {4: nl_constants.ATTR_ICMP_CODE, + 6: nl_constants.ATTR_ICMP_CODE}, + 'type': {4: nl_constants.ATTR_ICMP_TYPE, + 6: nl_constants.ATTR_ICMP_TYPE}, + 'id': {4: nl_constants.ATTR_ICMP_ID, + 6: nl_constants.ATTR_ICMP_ID}, + 'sport': {4: nl_constants.ATTR_PORT_SRC, + 6: nl_constants.ATTR_PORT_SRC}, + 'dport': {4: nl_constants.ATTR_PORT_DST, + 6: nl_constants.ATTR_PORT_DST}} + +NFCT_CALLBACK = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, + ctypes.c_void_p, ctypes.c_void_p) + + +class ConntrackOpenFailedExit(SystemExit): + """Raised if we fail to open a new conntrack or conntrack handler""" + + +class ConntrackManager(object): + def __init__(self, family_socket=None): + self.family_socket = family_socket + self.set_functions = { + 'src': {4: nfct.nfct_set_attr, + 6: nfct.nfct_set_attr}, + 'dst': {4: nfct.nfct_set_attr, + 6: nfct.nfct_set_attr}, + 'ipversion': {4: nfct.nfct_set_attr_u8, + 6: nfct.nfct_set_attr_u8}, + 'protocol': {4: nfct.nfct_set_attr_u8, + 6: nfct.nfct_set_attr_u8}, + 'type': {4: nfct.nfct_set_attr_u8, + 6: nfct.nfct_set_attr_u8}, + 'code': {4: nfct.nfct_set_attr_u8, + 6: nfct.nfct_set_attr_u8}, + 'id': {4: nfct.nfct_set_attr_u16, + 6: nfct.nfct_set_attr_u16}, + 'sport': {4: nfct.nfct_set_attr_u16, + 6: nfct.nfct_set_attr_u16}, + 'dport': {4: nfct.nfct_set_attr_u16, + 6: nfct.nfct_set_attr_u16}, } + + self.converters = {'src': bytes, + 'dst': bytes, + 'ipversion': nl_constants.IPVERSION_SOCKET.get, + 'protocol': constants.IP_PROTOCOL_MAP.get, + 'code': int, + 'type': int, + 'id': libc.htons, + 'sport': libc.htons, + 'dport': libc.htons, } + + def list_entries(self): + entries = [] + raw_entry = ctypes.create_string_buffer(nl_constants.BUFFER) + + @NFCT_CALLBACK + def callback(type_, conntrack, data): + nfct.nfct_snprintf(raw_entry, nl_constants.BUFFER, + conntrack, type_, + nl_constants.NFCT_O_PLAIN, + nl_constants.NFCT_OF_TIME) + entries.append(raw_entry.value.decode('utf-8')) + return nl_constants.NFCT_CB_CONTINUE + + self._callback_register(nl_constants.NFCT_T_ALL, + callback, DATA_CALLBACK) + + data_ref = self._get_ref(self.family_socket or + nl_constants.IPVERSION_SOCKET[4]) + self._query(nl_constants.NFCT_Q_DUMP, data_ref) + return entries + + def delete_entries(self, entries): + conntrack = nfct.nfct_new() + try: + for entry in entries: + self._set_attributes(conntrack, entry) + self._query(nl_constants.NFCT_Q_DESTROY, conntrack) + except Exception as e: + msg = "Failed to delete conntrack entries %s" % e + LOG.critical(msg) + raise ConntrackOpenFailedExit(msg) + finally: + nfct.nfct_destroy(conntrack) + + def flush_entries(self): + data_ref = self._get_ref(self.family_socket or + nl_constants.IPVERSION_SOCKET[4]) + self._query(nl_constants.NFCT_Q_FLUSH, data_ref) + + def _query(self, query_type, query_data): + result = nfct.nfct_query(self.conntrack_handler, query_type, + query_data) + if result == nl_constants.NFCT_CB_FAILURE: + LOG.warning("Netlink query failed") + + def _convert_text_to_binary(self, source, addr_family): + dest = ctypes.create_string_buffer( + nl_constants.IPVERSION_BUFFER[addr_family]) + libc.inet_pton(nl_constants.IPVERSION_SOCKET[addr_family], + source.encode('utf-8'), dest) + return dest.raw + + def _set_attributes(self, conntrack, entry): + ipversion = entry.get('ipversion', 4) + for attr, value in entry.items(): + set_function = self.set_functions[attr][ipversion] + target = TARGET[attr][ipversion] + converter = self.converters[attr] + if attr in ['src', 'dst']: + # convert src and dst of IPv4 and IPv6 into same format + value = self._convert_text_to_binary(value, ipversion) + set_function(conntrack, target, converter(value)) + + def _callback_register(self, message_type, callback_func, data): + nfct.nfct_callback_register(self.conntrack_handler, + message_type, callback_func, data) + + def _get_ref(self, data): + return ctypes.byref(ctypes.c_int(data)) + + def __enter__(self): + self.conntrack_handler = nfct.nfct_open( + nl_constants.CONNTRACK, + nl_constants.NFNL_SUBSYS_CTNETLINK) + if not self.conntrack_handler: + msg = "Failed to open new conntrack handler" + LOG.critical(msg) + raise ConntrackOpenFailedExit(msg) + return self + + def __exit__(self, *args): + nfct.nfct_close(self.conntrack_handler) + + +def _parse_entry(entry, ipversion): + """Parse entry from text to Python tuple + + :param entry: conntrack entry in text + :param ipversion: ipversion 4 or 6 + :return: conntrack entry in Python tuple + example: (4, 'tcp', '1', '2', '1.1.1.1', '2.2.2.2') + The attributes are ordered to be easy to compare with other entries + and compare with firewall rule + """ + protocol = entry[1] + parsed_entry = [ipversion, protocol] + for attr, position in ATTR_POSITIONS[protocol]: + val = entry[position].partition('=')[2] + parsed_entry.append(int(val) if attr in ['sport', 'dport', 'type', + 'code', 'id'] else val) + return tuple(parsed_entry) + + +@privileged.default.entrypoint +def flush_entries(namespace=None): + """Delete all conntrack entries + + :param namespace: namespace to delete conntrack entries + :return: None + """ + with fwaas_utils.in_namespace(namespace): + for ipversion in IP_VERSIONS: + with ConntrackManager(nl_constants.IPVERSION_SOCKET[ipversion]) \ + as conntrack: + conntrack.flush_entries() + + +@privileged.default.entrypoint +def list_entries(namespace=None): + """List and parse all conntrack entries + + :param namespace: namespace to get conntrack entries + :return: sorted list of conntrack entries in Python tuple + example: [(4, 'icmp', '8', '0', '1.1.1.1', '2.2.2.2', '1234'), + (4, 'tcp', '1', '2', '1.1.1.1', '2.2.2.2')] + """ + parsed_entries = [] + with fwaas_utils.in_namespace(namespace): + for ipversion in IP_VERSIONS: + with ConntrackManager(nl_constants.IPVERSION_SOCKET[ipversion]) \ + as conntrack: + raw_entries = conntrack.list_entries() + for raw_entry in raw_entries: + parsed_entry = _parse_entry(raw_entry.split(), ipversion) + parsed_entries.append(parsed_entry) + return sorted(parsed_entries) + + +@privileged.default.entrypoint +def delete_entries(entries, namespace=None): + """Delete selected entries + + :param entries: list of parsed (as tuple) entries to delete + :param namespace: namespace to delete conntrack entries + :return: None + """ + entry_args = [] + for entry in entries: + entry_arg = {'ipversion': entry[0], 'protocol': entry[1]} + for idx, attr in enumerate(ATTR_POSITIONS[entry_arg['protocol']]): + entry_arg[attr[0]] = entry[idx + 2] + entry_args.append(entry_arg) + + with fwaas_utils.in_namespace(namespace): + with ConntrackManager() as conntrack: + conntrack.delete_entries(entry_args) diff --git a/neutron_fwaas/privileged/tests/__init__.py b/neutron_fwaas/privileged/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/privileged/tests/functional/__init__.py b/neutron_fwaas/privileged/tests/functional/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/privileged/tests/functional/dummy.py b/neutron_fwaas/privileged/tests/functional/dummy.py new file mode 100644 index 000000000..fe0e85995 --- /dev/null +++ b/neutron_fwaas/privileged/tests/functional/dummy.py @@ -0,0 +1,29 @@ +# Copyright (c) 2017 Thales Services SAS +# 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. + +from oslo_utils import uuidutils +from pyroute2 import netns as pynetns + +from neutron_fwaas import privileged + + +# TODO(cby): move this method in neutron.tests.functional.privileged associated +# to a new privsep context. +@privileged.default.entrypoint +def dummy(): + """This method aim is to validate that we can use privsep in functests.""" + namespace = 'dummy-%s' % uuidutils.generate_uuid() + pynetns.create(namespace) + pynetns.remove(namespace) diff --git a/neutron_fwaas/privileged/tests/functional/utils.py b/neutron_fwaas/privileged/tests/functional/utils.py new file mode 100644 index 000000000..2e58a27d8 --- /dev/null +++ b/neutron_fwaas/privileged/tests/functional/utils.py @@ -0,0 +1,39 @@ +# Copyright (c) 2017 Thales Services SAS +# 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. + +import pyroute2 + +from neutron_fwaas import privileged +from neutron_fwaas.privileged import utils + + +def _get_ifname(link): + attr_dict = dict(link['attrs']) + return attr_dict['IFLA_IFNAME'] + + +def list_interface_names(): + iproute = pyroute2.IPRoute() + result = iproute.get_links() + return [_get_ifname(link) for link in result] + + +@privileged.default.entrypoint +def get_in_namespace_interfaces(namespace): + before = list_interface_names() + with utils.in_namespace(namespace): + inside = list_interface_names() + after = list_interface_names() + return before, inside, after diff --git a/neutron_fwaas/privileged/utils.py b/neutron_fwaas/privileged/utils.py new file mode 100644 index 000000000..f3edccbae --- /dev/null +++ b/neutron_fwaas/privileged/utils.py @@ -0,0 +1,58 @@ +# Copyright (c) 2017 Thales Services SAS +# 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. + +import contextlib +import os + +from oslo_log import log as logging +from pyroute2 import netns as pynetns + +from neutron_fwaas._i18n import _ + + +PROCESS_NETNS = '/proc/self/ns/net' + +LOG = logging.getLogger(__name__) + + +class BackInNamespaceExit(SystemExit): + """Raised if we fail to moved back process in its original namespace.""" + + +@contextlib.contextmanager +def in_namespace(namespace): + """Move current process in a specific namespace. + + This contextmanager moves current process in a specific namespace and + ensures to move it back in original namespace or kills it if we fail to + move back in original namespace. + """ + if not namespace: + yield + return + + org_netns_fd = os.open(PROCESS_NETNS, os.O_RDONLY) + pynetns.setns(namespace) + try: + yield + finally: + try: + # NOTE(cby): this code is not executed only if we fail to + # move in target namespace + pynetns.setns(org_netns_fd) + except Exception as e: + msg = _('Failed to move back in original netns: %s') % e + LOG.critical(msg) + raise BackInNamespaceExit(msg) diff --git a/neutron_fwaas/services/__init__.py b/neutron_fwaas/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/firewall/__init__.py b/neutron_fwaas/services/firewall/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/firewall/fwaas_plugin_v2.py b/neutron_fwaas/services/firewall/fwaas_plugin_v2.py new file mode 100644 index 000000000..0cb49a5b1 --- /dev/null +++ b/neutron_fwaas/services/firewall/fwaas_plugin_v2.py @@ -0,0 +1,429 @@ +# 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. + +from neutron.db import servicetype_db as st_db +from neutron import service +from neutron.services import provider_configuration as provider_conf +from neutron.services import service_base +from neutron_lib.api.definitions import firewall_v2 +from neutron_lib.api.definitions import portbindings as pb_def +from neutron_lib.api import validators +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib import constants as nl_constants +from neutron_lib.exceptions import firewall_v2 as f_exc +from neutron_lib.plugins import constants as plugin_const +from neutron_lib.plugins import directory +from oslo_log import helpers as log_helpers +from oslo_log import log as logging + +from neutron_fwaas.common import exceptions +from neutron_fwaas.common import fwaas_constants +from neutron_fwaas.extensions.firewall_v2 import Firewallv2PluginBase +from neutron_fwaas.services.firewall.service_drivers import driver_api +from neutron_fwaas.services.logapi.agents.drivers.iptables \ + import driver as logging_driver + +LOG = logging.getLogger(__name__) + + +@registry.has_registry_receivers +class FirewallPluginV2(Firewallv2PluginBase): + """Firewall v2 Neutron service plugin class""" + + supported_extension_aliases = [firewall_v2.ALIAS] + path_prefix = firewall_v2.API_PREFIX + + def __init__(self): + super(FirewallPluginV2, self).__init__() + """Do the initialization for the firewall service plugin here.""" + # Initialize the Firewall v2 service plugin + service_type_manager = st_db.ServiceTypeManager.get_instance() + service_type_manager.add_provider_configuration( + fwaas_constants.FIREWALL_V2, + provider_conf.ProviderConfiguration('neutron_fwaas')) + + # Load the default driver + drivers, default_provider = service_base.load_drivers( + fwaas_constants.FIREWALL_V2, self) + LOG.info("Firewall v2 Service Plugin using Service Driver: %s", + default_provider) + + if len(drivers) > 1: + LOG.warning("Multiple drivers configured for Firewall v2, " + "although running multiple drivers in parallel is " + "not yet supported") + + self.driver_name = default_provider + self.driver = drivers[default_provider] + + # start rpc listener if driver required + if isinstance(self.driver, driver_api.FirewallDriverRPCMixin): + rpc_worker = service.RpcWorker([self], worker_process_count=0) + self.add_worker(rpc_worker) + + log_plugin = directory.get_plugin(plugin_const.LOG_API) + logging_driver.register() + # If log_plugin was loaded before firewall plugin + if log_plugin: + # Register logging driver with LoggingServiceDriverManager again + log_plugin.driver_manager.register_driver(logging_driver.DRIVER) + + def start_rpc_listeners(self): + return self.driver.start_rpc_listener() + + @property + def _core_plugin(self): + return directory.get_plugin() + + def _ensure_update_firewall_group(self, context, fwg_id): + """Checks if the firewall group can be updated + + Raises FirewallGroupInPendingState if the firewall group is in pending + state. + :param context: neutron context + :param fwg_id: firewall group ID to check + :return: Firewall group dict + """ + fwg = self.get_firewall_group(context, fwg_id) + if fwg['status'] in [nl_constants.PENDING_CREATE, + nl_constants.PENDING_UPDATE, + nl_constants.PENDING_DELETE]: + raise f_exc.FirewallGroupInPendingState( + firewall_id=fwg_id, pending_state=fwg['status']) + return fwg + + def _ensure_update_firewall_policy(self, context, fwp_id): + """Checks if the firewall policy can be updated + + Fetch firewall group associated to the policy and checks if they can be + updated. + :param context: neutron context + :param fwp_id: firewall policy ID to check + """ + fwp = self.get_firewall_policy(context, fwp_id) + ing_fwg_ids, eg_fwg_ids = self._get_fwgs_with_policy(context, fwp) + for fwg_id in list(set(ing_fwg_ids + eg_fwg_ids)): + self._ensure_update_firewall_group(context, fwg_id) + + def _ensure_update_firewall_rule(self, context, fwr_id): + """Checks if the firewall rule can be updated + + Fetch firewall policy associated to the rule and checks if they can be + updated. + :param context: neutron context + :param fwr_id: firewall policy ID to check + """ + fwr = self.get_firewall_rule(context, fwr_id) + fwp_ids = self._get_policies_with_rule(context, fwr) + for fwp_id in fwp_ids: + self._ensure_update_firewall_policy(context, fwp_id) + + def _validate_firewall_policies_for_firewall_group(self, context, fwg): + """Validate firewall group and policy owner + + Check if the firewall policy is not shared, it have the same project + owner than the friewall group. + :param context: neutron context + :param fwg: firewall group to validate + """ + for policy_type in ['ingress_firewall_policy_id', + 'egress_firewall_policy_id']: + if fwg.get(policy_type): + fwp = self.get_firewall_policy(context, fwg[policy_type]) + if fwg['tenant_id'] != fwp['tenant_id'] and not fwp['shared']: + raise f_exc.FirewallPolicyConflict( + firewall_policy_id=fwg[policy_type]) + + def _validate_ports_for_firewall_group(self, context, tenant_id, + fwg_ports): + """Validate firewall group associated ports + + Check if the firewall group associated ports have the same project + owner and is router interface type or a compute layer 2 and supported + by the firewall driver + :param context: neutron context + :param tenant_id: firewall group project ID + :param fwg_ports: firewall group associated ports + """ + # TODO(sridar): elevated context and do we want to use public ? + for port_id in fwg_ports: + port = self._core_plugin.get_port(context, port_id) + + if port['tenant_id'] != tenant_id: + raise f_exc.FirewallGroupPortInvalidProject( + port_id=port_id, project_id=port['tenant_id']) + device_owner = port.get('device_owner', '') + if device_owner in nl_constants.ROUTER_INTERFACE_OWNERS: + if not self.driver.is_supported_l3_port(port): + raise exceptions.FirewallGroupPortNotSupported( + driver_name=self.driver_name, port_id=port_id) + elif device_owner.startswith( + nl_constants.DEVICE_OWNER_COMPUTE_PREFIX): + if not self._is_supported_l2_port(context, port_id): + raise exceptions.FirewallGroupPortNotSupported( + driver_name=self.driver_name, port_id=port_id) + else: + raise f_exc.FirewallGroupPortInvalid(port_id=port_id) + + def _is_supported_l2_port(self, context, port_id): + """Whether this l2 port is supported""" + + # Re-fetch to get up-to-date data from db + port = self._core_plugin.get_port(context, id=port_id) + + # Skip port binding is unbound or failed + if port[pb_def.VIF_TYPE] in [pb_def.VIF_TYPE_UNBOUND, + pb_def.VIF_TYPE_BINDING_FAILED]: + return False + + return self.driver.is_supported_l2_port(port) + + def _validate_if_firewall_group_on_ports(self, context, firewall_group, + id=None): + """Validate if ports are not associated with any firewall_group. + + If any of the ports in the list is already associated with + a firewall group, raise an exception else just return. + :param context: neutron context + :param fwg: firewall group to validate + """ + if 'ports' not in firewall_group or not firewall_group['ports']: + return + + filters = { + 'tenant_id': [firewall_group['tenant_id']], + 'ports': firewall_group['ports'], + } + ports_in_use = set() + for fwg in self.get_firewall_groups(context, filters=filters): + if id is not None and fwg['id'] == id: + continue + ports_in_use |= set(fwg.get('ports', [])) & \ + set(firewall_group['ports']) + if ports_in_use: + raise f_exc.FirewallGroupPortInUse(port_ids=list(ports_in_use)) + + def _get_fwgs_with_policy(self, context, firewall_policy): + """List firewall group IDs which use a firewall policy + + List all firewall group IDs which have the given firewall policy as + ingress or egress. + :param context: neutron context + :param firewall_policy: firewall policy to filter + """ + filters = { + 'tenant_id': [firewall_policy['tenant_id']], + 'ingress_firewall_policy_id': [firewall_policy['id']], + } + ingress_fwp_ids = [fwg['id'] + for fwg in self.get_firewall_groups( + context, filters=filters)] + + filters = { + 'tenant_id': [firewall_policy['tenant_id']], + 'egress_firewall_policy_id': [firewall_policy['id']], + } + egress_fwp_ids = [fwg['id'] + for fwg in self.get_firewall_groups( + context, filters=filters)] + + return ingress_fwp_ids, egress_fwp_ids + + def _get_policies_with_rule(self, context, firewall_rule): + filters = { + 'tenant_id': [firewall_rule['tenant_id']], + 'firewall_rules': [firewall_rule['id']], + } + return [fwp['id'] for fwp in self.get_firewall_policies( + context, filters=filters)] + + def _validate_insert_remove_rule_request(self, rule_info): + """Validate rule_info dict + + Check that all mandatory fields are present, otherwise raise + proper exception. + """ + if not rule_info or 'firewall_rule_id' not in rule_info: + raise f_exc.FirewallRuleInfoMissing() + # Validator doesn't return anything if the check passes + if validators.validate_uuid(rule_info['firewall_rule_id']): + raise f_exc.FirewallRuleNotFound( + firewall_rule_id=rule_info['firewall_rule_id']) + + @registry.receives(resources.PORT, [events.AFTER_UPDATE]) + def handle_update_port(self, resource, event, trigger, payload): + context = payload.context + original_port = payload.states[0] + updated_port = payload.states[1] + if not updated_port['device_owner'].startswith( + nl_constants.DEVICE_OWNER_COMPUTE_PREFIX): + return + + if (original_port[pb_def.VIF_TYPE] != pb_def.VIF_TYPE_UNBOUND): + # Checking newly vm port binding allows us to avoid call to DB + # when a port update_event like restart, setting name, etc... + # Moreover, that will help us in case of tenant admin wants to + # only attach security group to vm port. + return + + port_id = updated_port['id'] + # Check port is supported by firewall driver + if not self._is_supported_l2_port(context, port_id): + return + + project_id = updated_port['project_id'] + fwgs = self.get_firewall_groups( + context, + filters={ + 'tenant_id': [project_id], + 'name': [fwaas_constants.DEFAULT_FWG], + }, + fields=['id', 'ports'], + ) + if len(fwgs) != 1: + # Cannot found default Firewall Group, abandon + LOG.warning("Cannot found default firewall group of project %s", + project_id) + return + default_fwg = fwgs[0] + + # Add default firewall group to the port + port_ids = default_fwg.get('ports', []) + [port_id] + try: + self.update_firewall_group(context, default_fwg['id'], + {'firewall_group': {'ports': port_ids}}) + except f_exc.FirewallGroupPortInUse: + LOG.warning("Port %s has been already associated with default " + "firewall group %s and skip association", port_id, + default_fwg['id']) + + # Firewall Group + @log_helpers.log_method_call + def create_firewall_group(self, context, firewall_group): + firewall_group = firewall_group['firewall_group'] + ports = firewall_group.get('ports', []) + + self._validate_firewall_policies_for_firewall_group(context, + firewall_group) + # Validate ports owner type and project + self._validate_ports_for_firewall_group(context, + firewall_group['tenant_id'], + ports) + + self._validate_if_firewall_group_on_ports(context, firewall_group) + + return self.driver.create_firewall_group(context, firewall_group) + + @log_helpers.log_method_call + def delete_firewall_group(self, context, id): + # if no such group exists -> don't raise an exception according to + # 80fe2ba1, return None + try: + fwg = self.get_firewall_group(context, id) + except f_exc.FirewallGroupNotFound: + return + + if fwg['status'] == nl_constants.ACTIVE: + raise f_exc.FirewallGroupInUse(firewall_id=id) + + self.driver.delete_firewall_group(context, id) + + @log_helpers.log_method_call + def get_firewall_group(self, context, id, fields=None): + return self.driver.get_firewall_group(context, id, fields=fields) + + @log_helpers.log_method_call + def get_firewall_groups(self, context, filters=None, fields=None): + return self.driver.get_firewall_groups(context, filters, fields) + + @log_helpers.log_method_call + def update_firewall_group(self, context, id, firewall_group): + firewall_group = firewall_group['firewall_group'] + ports = firewall_group.get('ports', []) + + old_firewall_group = self._ensure_update_firewall_group(context, id) + firewall_group['tenant_id'] = old_firewall_group['tenant_id'] + + self._validate_firewall_policies_for_firewall_group(context, + firewall_group) + # Validate ports owner type and project + self._validate_ports_for_firewall_group(context, + firewall_group['tenant_id'], + ports) + self._validate_if_firewall_group_on_ports(context, firewall_group, + id=id) + + return self.driver.update_firewall_group(context, id, firewall_group) + + # Firewall Policy + @log_helpers.log_method_call + def create_firewall_policy(self, context, firewall_policy): + firewall_policy = firewall_policy['firewall_policy'] + return self.driver.create_firewall_policy(context, firewall_policy) + + @log_helpers.log_method_call + def delete_firewall_policy(self, context, id): + self.driver.delete_firewall_policy(context, id) + + @log_helpers.log_method_call + def get_firewall_policy(self, context, id, fields=None): + return self.driver.get_firewall_policy(context, id, fields) + + @log_helpers.log_method_call + def get_firewall_policies(self, context, filters=None, fields=None): + return self.driver.get_firewall_policies(context, filters, fields) + + @log_helpers.log_method_call + def update_firewall_policy(self, context, id, firewall_policy): + firewall_policy = firewall_policy['firewall_policy'] + self._ensure_update_firewall_policy(context, id) + return self.driver.update_firewall_policy(context, id, firewall_policy) + + # Firewall Rule + @log_helpers.log_method_call + def create_firewall_rule(self, context, firewall_rule): + firewall_rule = firewall_rule['firewall_rule'] + return self.driver.create_firewall_rule(context, firewall_rule) + + @log_helpers.log_method_call + def delete_firewall_rule(self, context, id): + self.driver.delete_firewall_rule(context, id) + + @log_helpers.log_method_call + def get_firewall_rule(self, context, id, fields=None): + return self.driver.get_firewall_rule(context, id, fields) + + @log_helpers.log_method_call + def get_firewall_rules(self, context, filters=None, fields=None): + return self.driver.get_firewall_rules(context, filters, fields) + + @log_helpers.log_method_call + def update_firewall_rule(self, context, id, firewall_rule): + firewall_rule = firewall_rule['firewall_rule'] + self._ensure_update_firewall_rule(context, id) + return self.driver.update_firewall_rule(context, id, firewall_rule) + + @log_helpers.log_method_call + def insert_rule(self, context, policy_id, rule_info): + self._ensure_update_firewall_policy(context, policy_id) + self._validate_insert_remove_rule_request(rule_info) + return self.driver.insert_rule(context, policy_id, rule_info) + + @log_helpers.log_method_call + def remove_rule(self, context, policy_id, rule_info): + self._ensure_update_firewall_policy(context, policy_id) + self._validate_insert_remove_rule_request(rule_info) + return self.driver.remove_rule(context, policy_id, rule_info) diff --git a/neutron_fwaas/services/firewall/service_drivers/__init__.py b/neutron_fwaas/services/firewall/service_drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/__init__.py b/neutron_fwaas/services/firewall/service_drivers/agents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/agents.py b/neutron_fwaas/services/firewall/service_drivers/agents/agents.py new file mode 100644 index 000000000..0643799f2 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/agents.py @@ -0,0 +1,376 @@ +# Copyright (c) 2013 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. + +from neutron_lib.api.definitions import portbindings as pb_def +from neutron_lib import constants as nl_constants +from neutron_lib import context as neutron_context +from neutron_lib.exceptions import firewall_v2 as f_exc +from neutron_lib import rpc as n_rpc +from oslo_config import cfg +from oslo_log import helpers as log_helpers +from oslo_log import log as logging +import oslo_messaging + +from neutron_fwaas.common import fwaas_constants as constants +from neutron_fwaas.services.firewall.service_drivers import driver_api + + +LOG = logging.getLogger(__name__) + + +class FirewallAgentCallbacks(object): + target = oslo_messaging.Target(version='1.0') + + def __init__(self, firewall_db): + self.firewall_db = firewall_db + + @log_helpers.log_method_call + def set_firewall_group_status(self, context, fwg_id, status, **kwargs): + """Agent uses this to set a firewall_group's status.""" + # Sanitize status first + if status in (nl_constants.ACTIVE, nl_constants.DOWN, + nl_constants.INACTIVE): + to_update = status + else: + to_update = nl_constants.ERROR + # ignore changing status if firewall_group expects to be deleted + # That case means that while some pending operation has been + # performed on the backend, neutron server received delete request + # and changed firewall status to PENDING_DELETE + updated = self.firewall_db.update_firewall_group_status( + context, fwg_id, to_update, not_in=(nl_constants.PENDING_DELETE,)) + if updated: + LOG.debug("firewall %s status set: %s", fwg_id, to_update) + return updated and to_update != nl_constants.ERROR + + @log_helpers.log_method_call + def firewall_group_deleted(self, context, fwg_id, **kwargs): + """Agent uses this to indicate firewall is deleted.""" + try: + fwg = self.firewall_db.get_firewall_group(context, fwg_id) + # allow to delete firewalls in ERROR state + if fwg['status'] in (nl_constants.PENDING_DELETE, + nl_constants.ERROR): + self.firewall_db.delete_firewall_group(context, fwg_id) + return True + LOG.warning('Firewall %(fwg)s unexpectedly deleted by agent, ' + 'status was %(status)s', + {'fwg': fwg_id, 'status': fwg['status']}) + fwg['status'] = nl_constants.ERROR + self.firewall_db.update_firewall_group(context, fwg_id, fwg) + return False + except f_exc.FirewallGroupNotFound: + LOG.info('Firewall group %s already deleted', fwg_id) + return True + + @log_helpers.log_method_call + def get_firewall_groups_for_project(self, context, **kwargs): + """Gets all firewall_groups and rules on a project.""" + fwg_list = [] + for fwg in self.firewall_db.get_firewall_groups(context): + fwg_with_rules =\ + self.firewall_db.make_firewall_group_dict_with_rules( + context, fwg['id']) + if fwg['status'] == nl_constants.PENDING_DELETE: + fwg_with_rules['add-port-ids'] = [] + fwg_with_rules['del-port-ids'] = ( + self.firewall_db.get_ports_in_firewall_group( + context, fwg['id'])) + else: + fwg_with_rules['add-port-ids'] = ( + self.firewall_db.get_ports_in_firewall_group( + context, fwg['id'])) + fwg_with_rules['del-port-ids'] = [] + fwg_list.append(fwg_with_rules) + return fwg_list + + @log_helpers.log_method_call + def get_projects_with_firewall_groups(self, context, **kwargs): + """Get all projects that have firewall_groups.""" + ctx = neutron_context.get_admin_context() + fwg_list = self.firewall_db.get_firewall_groups(ctx) + fwg_project_list = list(set(fwg['tenant_id'] for fwg in fwg_list)) + return fwg_project_list + + @log_helpers.log_method_call + def get_firewall_group_for_port(self, context, **kwargs): + """Get firewall_group is associated with a port.""" + ctx = context.elevated() + # Only one Firewall Group can be associated to a port at a time + fwg_port_binding = self.firewall_db.get_firewall_groups( + ctx, filters={'ports': [kwargs.get('port_id')]}) + if len(fwg_port_binding) != 1: + return + fwg = fwg_port_binding[0] + + fwg['ingress_rule_list'] = [] + for rule_id in self.firewall_db.get_firewall_policy( + context, fwg['ingress_firewall_policy_id'], + fields=['firewall_rules'])['firewall_rules']: + fwg['ingress_rule_list'].append( + self.firewall_db.get_firewall_rule(context, rule_id)) + fwg['egress_rule_list'] = [] + for rule_id in self.firewall_db.get_firewall_policy( + context, fwg['egress_firewall_policy_id'], + fields=['firewall_rules'])['firewall_rules']: + fwg['egress_rule_list'].append( + self.firewall_db.get_firewall_rule(context, rule_id)) + return fwg + + +class FirewallAgentApi(object): + """Plugin side of plugin to agent RPC API""" + + def __init__(self, topic, host): + self.host = host + target = oslo_messaging.Target(topic=topic, version='1.0') + self.client = n_rpc.get_client(target) + + def create_firewall_group(self, context, firewall_group): + cctxt = self.client.prepare(fanout=True) + cctxt.cast(context, 'create_firewall_group', + firewall_group=firewall_group, host=self.host) + + def update_firewall_group(self, context, firewall_group): + cctxt = self.client.prepare(fanout=True) + cctxt.cast(context, 'update_firewall_group', + firewall_group=firewall_group, host=self.host) + + def delete_firewall_group(self, context, firewall_group): + cctxt = self.client.prepare(fanout=True) + cctxt.cast(context, 'delete_firewall_group', + firewall_group=firewall_group, host=self.host) + + +class FirewallAgentDriver(driver_api.FirewallDriverDB, + driver_api.FirewallDriverRPCMixin): + """Firewall driver to implement agent messages and callback methods + + Implement RPC Firewall v2 API and callback methods for agents based on + Neutron DB model. + """ + + def __init__(self, service_plugin): + super(FirewallAgentDriver, self).__init__(service_plugin) + self.agent_rpc = FirewallAgentApi(constants.FW_AGENT, cfg.CONF.host) + + def is_supported_l2_port(self, port): + if port[pb_def.VIF_TYPE] == pb_def.VIF_TYPE_OVS: + if not port['port_security_enabled']: + return True + + # TODO(annp): remove these lines after we fully support for hybrid + # port + if not port[pb_def.VIF_DETAILS][pb_def.OVS_HYBRID_PLUG]: + return True + LOG.warning("Doesn't support hybrid port at the moment") + else: + LOG.warning("Doesn't support vif type %s", port[pb_def.VIF_TYPE]) + return False + + def is_supported_l3_port(self, port): + return True + + def start_rpc_listener(self): + self.endpoints = [FirewallAgentCallbacks(self.firewall_db)] + self.rpc_connection = n_rpc.Connection() + self.rpc_connection.create_consumer(constants.FIREWALL_PLUGIN, + self.endpoints, fanout=False) + return self.rpc_connection.consume_in_threads() + + def _rpc_update_firewall_group(self, context, fwg_id): + fw_ports = self.firewall_db.get_ports_in_firewall_group( + context, fwg_id) + if not fw_ports: + return + status_update = {"status": nl_constants.PENDING_UPDATE} + self.update_firewall_group(context, fwg_id, status_update) + fwg_with_rules = self.firewall_db.make_firewall_group_dict_with_rules( + context, fwg_id) + # this is triggered on an update to fwg rule or policy, no + # change in associated ports. + fwg_with_rules['add-port-ids'] = fw_ports + fwg_with_rules['del-port-ids'] = [] + fwg_with_rules['port_details'] = self._get_fwg_port_details( + context, fwg_with_rules['add-port-ids']) + self.agent_rpc.update_firewall_group(context, fwg_with_rules) + + def _rpc_update_firewall_policy(self, context, firewall_policy_id): + firewall_policy = self.get_firewall_policy(context, firewall_policy_id) + if firewall_policy: + ing_fwg_ids, eg_fwg_ids = self.firewall_db.get_fwgs_with_policy( + context, firewall_policy_id) + for fwg_id in list(set(ing_fwg_ids + eg_fwg_ids)): + self._rpc_update_firewall_group(context, fwg_id) + + def _get_fwg_port_details(self, context, fwg_ports): + """Returns a dictionary list of port details. """ + port_details = {} + for port_id in fwg_ports: + port_db = self._core_plugin.get_port(context, port_id) + # Add more parameters below based on requirement. + device_owner = port_db['device_owner'] + port_details[port_id] = { + 'device_owner': device_owner, + 'device': port_db['id'], + 'network_id': port_db['network_id'], + 'fixed_ips': port_db['fixed_ips'], + 'allowed_address_pairs': + port_db.get('allowed_address_pairs', []), + 'port_security_enabled': + port_db.get('port_security_enabled', True), + 'id': port_db['id'], + 'status': port_db['status'], + } + if device_owner.startswith( + nl_constants.DEVICE_OWNER_COMPUTE_PREFIX): + port_details[port_id].update( + {'host': port_db[pb_def.HOST_ID]}) + return port_details + + def create_firewall_group_precommit(self, context, firewall_group): + ports = firewall_group['ports'] + + if (not ports or (not firewall_group['ingress_firewall_policy_id'] and + not firewall_group['egress_firewall_policy_id'])): + # no messaging to agent needed and fw needs to go to INACTIVE state + # as no associated ports and/or no policy configured. + status = nl_constants.INACTIVE + else: + status = (nl_constants.CREATED if cfg.CONF.router_distributed + else nl_constants.PENDING_CREATE) + firewall_group['status'] = status + + def create_firewall_group_postcommit(self, context, firewall_group): + if firewall_group['status'] != nl_constants.INACTIVE: + fwg_with_rules =\ + self.firewall_db.make_firewall_group_dict_with_rules( + context, firewall_group['id']) + fwg_with_rules['add-port-ids'] = firewall_group['ports'] + fwg_with_rules['del-ports-id'] = [] + fwg_with_rules['port_details'] = self._get_fwg_port_details( + context, firewall_group['ports']) + self.agent_rpc.create_firewall_group(context, fwg_with_rules) + + def delete_firewall_group_precommit(self, context, firewall_group): + if firewall_group['status'] == nl_constants.ACTIVE: + raise f_exc.FirewallGroupInUse(firewall_id=firewall_group['id']) + elif firewall_group['status'] != nl_constants.INACTIVE: + # Firewall group is in inconsistent state, remove it + return + if not firewall_group['ports']: + # No associated port, can safety remove it + return + + # Need to prevent agent to delete the firewall group before delete it + self.firewall_db.update_firewall_group_status( + context, firewall_group['id'], nl_constants.PENDING_DELETE) + firewall_group['status'] = nl_constants.PENDING_DELETE + + fwg_with_rules = self.firewall_db.make_firewall_group_dict_with_rules( + context, firewall_group['id']) + fwg_with_rules['del-port-ids'] = firewall_group['ports'] + fwg_with_rules['add-port-ids'] = [] + # Reflect state change in fwg_with_rules + fwg_with_rules['status'] = nl_constants.PENDING_DELETE + fwg_with_rules['port_details'] = self._get_fwg_port_details( + context, fwg_with_rules['del-port-ids']) + self.agent_rpc.delete_firewall_group(context, fwg_with_rules) + + def _need_pending_update(self, old_firewall_group, new_firewall_group): + port_updated = (set(new_firewall_group['ports']) != + set(old_firewall_group['ports'])) + policies_updated = ( + new_firewall_group['ingress_firewall_policy_id'] != + old_firewall_group['ingress_firewall_policy_id'] or + new_firewall_group['egress_firewall_policy_id'] != + old_firewall_group['egress_firewall_policy_id'] + ) + if (port_updated and + (new_firewall_group['ingress_firewall_policy_id'] or + new_firewall_group['egress_firewall_policy_id'])): + return True + if policies_updated and new_firewall_group['ports']: + return True + return False + + def update_firewall_group_precommit(self, context, old_firewall_group, + new_firewall_group): + if self._need_pending_update(old_firewall_group, new_firewall_group): + new_firewall_group['status'] = nl_constants.PENDING_UPDATE + + def update_firewall_group_postcommit(self, context, old_firewall_group, + new_firewall_group): + if not self._need_pending_update(old_firewall_group, + new_firewall_group): + return + + fwg_with_rules = self.firewall_db.make_firewall_group_dict_with_rules( + context, new_firewall_group['id']) + + # determine ports to add fw to and del from + fwg_with_rules['add-port-ids'] = list( + set(new_firewall_group['ports']) - set(old_firewall_group['ports']) + ) + fwg_with_rules['del-port-ids'] = list( + set(old_firewall_group['ports']) - set(new_firewall_group['ports']) + ) + + # last-port drives agent to ack with status to set state to INACTIVE + # Set last-port to True if there are no ports in the new group and + # the old group had the same number of ports that need to be deleted. + fwg_with_rules['last-port'] = (len(old_firewall_group['ports']) == len( + fwg_with_rules['del-port-ids']) and + not(new_firewall_group['ports'])) + + LOG.debug("update_firewall_group %s: Add Ports: %s, Del Ports: %s", + new_firewall_group['id'], + fwg_with_rules['add-port-ids'], + fwg_with_rules['del-port-ids']) + + fwg_with_rules['port_details'] = self._get_fwg_port_details( + context, fwg_with_rules['del-port-ids']) + fwg_with_rules['port_details'].update(self._get_fwg_port_details( + context, fwg_with_rules['add-port-ids'])) + + if (new_firewall_group['name'] == constants.DEFAULT_FWG and + len(fwg_with_rules['add-port-ids']) == 1 and + not fwg_with_rules['del-port-ids']): + port_id = fwg_with_rules['add-port-ids'][0] + if (fwg_with_rules['port_details'][port_id].get('status') != + nl_constants.ACTIVE): + # If port not yet active, just associate to default firewall + # group. When agent will set it to UP, it'll found FG + # association and enforce default policies + return + # Warn agents Firewall Group port list updated + self.agent_rpc.update_firewall_group(context, fwg_with_rules) + + def update_firewall_policy_postcommit(self, context, old_firewall_policy, + new_firewall_group): + self._rpc_update_firewall_policy(context, new_firewall_group['id']) + + def update_firewall_rule_postcommit(self, context, old_firewall_rule, + new_firewall_rule): + firewall_policy_ids = self.firewall_db.get_policies_with_rule( + context, new_firewall_rule['id']) + for firewall_policy_id in firewall_policy_ids: + self._rpc_update_firewall_policy(context, firewall_policy_id) + + def insert_rule_postcommit(self, context, policy_id, rule_info): + self._rpc_update_firewall_policy(context, policy_id) + + def remove_rule_postcommit(self, context, policy_id, rule_info): + self._rpc_update_firewall_policy(context, policy_id) diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/__init__.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/conntrack_base.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/conntrack_base.py new file mode 100644 index 000000000..536869632 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/conntrack_base.py @@ -0,0 +1,55 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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. + +import abc + +import six + +from neutron_lib.utils import runtime +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import excutils + +LOG = logging.getLogger(__name__) + + +def load_and_init_conntrack_driver(*args, **kwargs): + driver = cfg.CONF.fwaas.conntrack_driver + try: + conntrack_driver_cls = runtime.load_class_by_alias_or_classname( + 'neutron.agent.l3.firewall_drivers', driver) + except ImportError: + with excutils.save_and_reraise_exception(): + LOG.exception("Driver '%s' not found.", driver) + conntrack_driver = conntrack_driver_cls() + conntrack_driver.initialize(*args, **kwargs) + return conntrack_driver + + +@six.add_metaclass(abc.ABCMeta) +class ConntrackDriverBase(object): + """Base Driver for Conntrack""" + + @abc.abstractmethod + def initialize(self, *args, **kwargs): + """Initialize the driver""" + + @abc.abstractmethod + def delete_entries(self, rules, namespace): + """Delete conntrack entries specified by list of rules""" + + @abc.abstractmethod + def flush_entries(self, namespace): + """Delete all conntrack entries within namespace""" diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/fwaas_base.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/fwaas_base.py new file mode 100644 index 000000000..da8cf3006 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/fwaas_base.py @@ -0,0 +1,120 @@ +# Copyright 2013 Dell Inc. +# 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. + +import abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class FwaasDriverBase(object): + """Firewall as a Service Driver base class. + + Using FwaasDriver Class, an instance of L3 perimeter Firewall + can be created. The firewall co-exists with the L3 agent. + + One instance is created for each tenant. One firewall policy + is associated with each tenant (in the Havana release). + + The Firewall can be visualized as having two zones (in Havana + release), trusted and untrusted. + + All the 'internal' interfaces of Neutron Router is treated as trusted. The + interface connected to 'external network' is treated as untrusted. + + The policy is applied on traffic ingressing/egressing interfaces on + the trusted zone. This implies that policy will be applied for traffic + passing from + + - trusted to untrusted zones + - untrusted to trusted zones + - trusted to trusted zones + + Policy WILL NOT be applied for traffic from untrusted to untrusted zones. + This is not a problem in Havana release as there is only one interface + connected to external network. + + Since the policy is applied on the internal interfaces, the traffic + will be not be NATed to floating IP. For incoming traffic, the + traffic will get NATed to internal IP address before it hits + the firewall rules. So, while writing the rules, care should be + taken if using rules based on floating IP. + + The firewall rule addition/deletion/insertion/update are done by the + management console. When the policy is sent to the driver, the complete + policy is sent and the whole policy has to be applied atomically. The + firewall rules will not get updated individually. This is to avoid problems + related to out-of-order notifications or inconsistent behaviour by partial + application of rules. Argument agent_mode indicates the l3 agent in DVR or + DVR_SNAT or LEGACY mode. + """ + # TODO(Margaret): Remove the first 3 methods and make the second three + # @abc.abstractmethod + def create_firewall(self, agent_mode, apply_list, firewall): + """Create the Firewall with default (drop all) policy. + + The default policy will be applied on all the interfaces of + trusted zone. + """ + pass + + def delete_firewall(self, agent_mode, apply_list, firewall): + """Delete firewall. + + Removes all policies created by this instance and frees up + all the resources. + """ + pass + + def update_firewall(self, agent_mode, apply_list, firewall): + """Apply the policy on all trusted interfaces. + + Remove previous policy and apply the new policy on all trusted + interfaces. + """ + pass + + def create_firewall_group(self, agent_mode, apply_list, firewall): + """Create the Firewall with default (drop all) policy. + + The default policy will be applied on all the interfaces of + trusted zone. + """ + pass + + def delete_firewall_group(self, agent_mode, apply_list, firewall): + """Delete firewall. + + Removes all policies created by this instance and frees up + all the resources. + """ + pass + + def update_firewall_group(self, agent_mode, apply_list, firewall): + """Apply the policy on all trusted interfaces. + + Remove previous policy and apply the new policy on all trusted + interfaces. + """ + pass + + @abc.abstractmethod + def apply_default_policy(self, agent_mode, apply_list, firewall): + """Apply the default policy on all trusted interfaces. + + Remove current policy and apply the default policy on all trusted + interfaces. + """ + pass diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/fwaas_base_v2.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/fwaas_base_v2.py new file mode 100644 index 000000000..b50538b64 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/fwaas_base_v2.py @@ -0,0 +1,97 @@ +# Copyright (c) 2016 +# 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. + +import abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class FwaasDriverBase(object): + """Firewall as a Service Driver base class. + + Using FwaasDriver Class, an instance of L3 perimeter Firewall + can be created. The firewall co-exists with the L3 agent. + + One instance is created for each tenant. One firewall policy + is associated with each tenant (in the Havana release). + + The Firewall can be visualized as having two zones (in Havana + release), trusted and untrusted. + + All the 'internal' interfaces of Neutron Router is treated as trusted. The + interface connected to 'external network' is treated as untrusted. + + The policy is applied on traffic ingressing/egressing interfaces on + the trusted zone. This implies that policy will be applied for traffic + passing from + + - trusted to untrusted zones + - untrusted to trusted zones + - trusted to trusted zones + + Policy WILL NOT be applied for traffic from untrusted to untrusted zones. + This is not a problem in Havana release as there is only one interface + connected to external network. + + Since the policy is applied on the internal interfaces, the traffic + will be not be NATed to floating IP. For incoming traffic, the + traffic will get NATed to internal IP address before it hits + the firewall rules. So, while writing the rules, care should be + taken if using rules based on floating IP. + + The firewall rule addition/deletion/insertion/update are done by the + management console. When the policy is sent to the driver, the complete + policy is sent and the whole policy has to be applied atomically. The + firewall rules will not get updated individually. This is to avoid problems + related to out-of-order notifications or inconsistent behaviour by partial + application of rules. Argument agent_mode indicates the l3 agent in DVR or + DVR_SNAT or LEGACY mode. + """ + @abc.abstractmethod + def create_firewall_group(self, agent_mode, apply_list, firewall): + """Create the Firewall with default (drop all) policy. + + The default policy will be applied on all the interfaces of + trusted zone. + """ + pass + + @abc.abstractmethod + def delete_firewall_group(self, agent_mode, apply_list, firewall): + """Delete firewall. + + Removes all policies created by this instance and frees up + all the resources. + """ + pass + + @abc.abstractmethod + def update_firewall_group(self, agent_mode, apply_list, firewall): + """Apply the policy on all trusted interfaces. + + Remove previous policy and apply the new policy on all trusted + interfaces. + """ + pass + + @abc.abstractmethod + def apply_default_policy(self, agent_mode, apply_list, firewall): + """Apply the default policy on all trusted interfaces. + + Remove current policy and apply the default policy on all trusted + interfaces. + """ + pass diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/__init__.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/iptables_fwaas_v2.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/iptables_fwaas_v2.py new file mode 100644 index 000000000..6492a1f6e --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/iptables_fwaas_v2.py @@ -0,0 +1,550 @@ +# Copyright (c) 2016 +# 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. + +from neutron.agent.linux import iptables_manager +from neutron.common import utils +from neutron_lib import constants +from neutron_lib.exceptions import firewall_v2 as fw_ext +from oslo_log import log as logging + +from neutron_fwaas.services.firewall.service_drivers.agents.drivers import\ + conntrack_base +from neutron_fwaas.services.firewall.service_drivers.agents.drivers import\ + fwaas_base_v2 + +LOG = logging.getLogger(__name__) +FWAAS_DRIVER_NAME = 'Fwaas iptables driver' +FWAAS_DEFAULT_CHAIN = 'fwaas-default-policy' + +# Introduce these chain for future processing like firewall logging +ACCEPTED_CHAIN = 'accepted' +DROPPED_CHAIN = 'dropped' +REJECTED_CHAIN = 'rejected' + +FWAAS_TO_IPTABLE_ACTION_MAP = { + 'allow': ACCEPTED_CHAIN, + 'deny': DROPPED_CHAIN, + 'reject': REJECTED_CHAIN +} + +CHAIN_NAME_PREFIX = {constants.INGRESS_DIRECTION: 'i', + constants.EGRESS_DIRECTION: 'o'} + +""" Firewall rules are applied on internal-interfaces of Neutron router. + The packets ingressing tenant's network will be on the output + direction on internal-interfaces. +""" +IPTABLES_DIR = {constants.INGRESS_DIRECTION: '-o', + constants.EGRESS_DIRECTION: '-i'} +IPV4 = 'ipv4' +IPV6 = 'ipv6' +IP_VER_TAG = {IPV4: 'v4', + IPV6: 'v6'} + +INTERNAL_DEV_PREFIX = 'qr-' +SNAT_INT_DEV_PREFIX = 'sg-' +ROUTER_2_FIP_DEV_PREFIX = 'rfp-' + +MAX_INTF_NAME_LEN = 14 + + +class IptablesFwaasDriver(fwaas_base_v2.FwaasDriverBase): + """IPTables driver for Firewall As A Service.""" + + def __init__(self): + LOG.debug("Initializing fwaas iptables driver") + self.pre_firewall = None + self.conntrack = conntrack_base.load_and_init_conntrack_driver() + + def _get_intf_name(self, if_prefix, port_id): + _name = "%s%s" % (if_prefix, port_id) + return _name[:MAX_INTF_NAME_LEN] + + def create_firewall_group(self, agent_mode, apply_list, firewall): + LOG.debug('Creating firewall %(fw_id)s for tenant %(tid)s', + {'fw_id': firewall['id'], 'tid': firewall['tenant_id']}) + try: + if firewall['admin_state_up']: + self._setup_firewall(agent_mode, apply_list, firewall) + self._remove_conntrack_new_firewall(agent_mode, + apply_list, firewall) + self.pre_firewall = dict(firewall) + else: + self.apply_default_policy(agent_mode, apply_list, firewall) + except (LookupError, RuntimeError): + # catch known library exceptions and raise Fwaas generic exception + LOG.exception("Failed to create firewall: %s", firewall['id']) + raise fw_ext.FirewallInternalDriverError(driver=FWAAS_DRIVER_NAME) + + def _get_ipt_mgrs_with_if_prefix(self, agent_mode, ri): + """Gets the iptables manager along with the if prefix to apply rules. + + With DVR we can have differing namespaces depending on which agent + (on Network or Compute node). Also, there is an associated i/f for + each namespace. The iptables on the relevant namespace and matching + i/f are provided. On the Network node we could have both the snat + namespace and a fip so this is provided back as a list - so in that + scenario rules can be applied on both. + """ + if not ri.router.get('distributed'): + return [{'ipt': ri.iptables_manager, + 'if_prefix': INTERNAL_DEV_PREFIX}] + ipt_mgrs = [] + # TODO(sridar): refactor to get strings to a common location. + if agent_mode == 'dvr_snat': + if ri.snat_iptables_manager: + ipt_mgrs.append({'ipt': ri.snat_iptables_manager, + 'if_prefix': SNAT_INT_DEV_PREFIX}) + if ri.rtr_fip_connect: + # handle the fip case on n/w or compute node. + ipt_mgrs.append({'ipt': ri.iptables_manager, + 'if_prefix': ROUTER_2_FIP_DEV_PREFIX}) + return ipt_mgrs + + def delete_firewall_group(self, agent_mode, apply_list, firewall): + LOG.debug('Deleting firewall %(fw_id)s for tenant %(tid)s', + {'fw_id': firewall['id'], 'tid': firewall['tenant_id']}) + fwid = firewall['id'] + try: + for ri, router_fw_ports in apply_list: + ipt_if_prefix_list = self._get_ipt_mgrs_with_if_prefix( + agent_mode, ri) + for ipt_if_prefix in ipt_if_prefix_list: + ipt_mgr = ipt_if_prefix['ipt'] + self._remove_chains(fwid, ipt_mgr) + self._remove_default_chains(ipt_mgr) + # apply the changes immediately (no defer in firewall path) + ipt_mgr.defer_apply_off() + self.pre_firewall = None + except (LookupError, RuntimeError): + # catch known library exceptions and raise Fwaas generic exception + LOG.exception("Failed to delete firewall: %s", fwid) + raise fw_ext.FirewallInternalDriverError(driver=FWAAS_DRIVER_NAME) + + def update_firewall_group(self, agent_mode, apply_list, firewall): + LOG.debug('Updating firewall %(fw_id)s for tenant %(tid)s', + {'fw_id': firewall['id'], 'tid': firewall['tenant_id']}) + try: + if firewall['admin_state_up']: + self._setup_firewall(agent_mode, apply_list, firewall) + if self.pre_firewall: + self._remove_conntrack_updated_firewall(agent_mode, + apply_list, self.pre_firewall, firewall) + else: + self._remove_conntrack_new_firewall(agent_mode, + apply_list, firewall) + else: + self.apply_default_policy(agent_mode, apply_list, firewall) + self.pre_firewall = dict(firewall) + except (LookupError, RuntimeError): + # catch known library exceptions and raise Fwaas generic exception + LOG.exception("Failed to update firewall: %s", firewall['id']) + raise fw_ext.FirewallInternalDriverError(driver=FWAAS_DRIVER_NAME) + + def apply_default_policy(self, agent_mode, apply_list, firewall): + LOG.debug('Applying firewall %(fw_id)s for tenant %(tid)s', + {'fw_id': firewall['id'], 'tid': firewall['tenant_id']}) + fwid = firewall['id'] + try: + for ri, router_fw_ports in apply_list: + ipt_if_prefix_list = self._get_ipt_mgrs_with_if_prefix( + agent_mode, ri) + for ipt_if_prefix in ipt_if_prefix_list: + # the following only updates local memory; no hole in FW + ipt_mgr = ipt_if_prefix['ipt'] + self._remove_chains(fwid, ipt_mgr) + self._remove_default_chains(ipt_mgr) + + # Create accepted/dropped/rejected chain + self._add_accepted_chain_v4v6(ipt_mgr) + self._add_dropped_chain_v4v6(ipt_mgr) + self._add_rejected_chain_v4v6(ipt_mgr) + + # create default 'DROP ALL' policy chain + self._add_default_policy_chain_v4v6(ipt_mgr) + self._enable_policy_chain(fwid, ipt_if_prefix, + router_fw_ports) + + # apply the changes immediately (no defer in firewall path) + ipt_mgr.defer_apply_off() + except (LookupError, RuntimeError): + # catch known library exceptions and raise Fwaas generic exception + LOG.exception( + "Failed to apply default policy on firewall: %s", fwid) + raise fw_ext.FirewallInternalDriverError(driver=FWAAS_DRIVER_NAME) + + def _setup_firewall(self, agent_mode, apply_list, firewall): + fwid = firewall['id'] + for ri, router_fw_ports in apply_list: + ipt_if_prefix_list = self._get_ipt_mgrs_with_if_prefix( + agent_mode, ri) + for ipt_if_prefix in ipt_if_prefix_list: + ipt_mgr = ipt_if_prefix['ipt'] + # the following only updates local memory; no hole in FW + self._remove_chains(fwid, ipt_mgr) + self._remove_default_chains(ipt_mgr) + + # Create accepted/dropped/rejected chain + self._add_accepted_chain_v4v6(ipt_mgr) + self._add_dropped_chain_v4v6(ipt_mgr) + self._add_rejected_chain_v4v6(ipt_mgr) + + # create default 'DROP ALL' policy chain + self._add_default_policy_chain_v4v6(ipt_mgr) + # create chain based on configured policy + self._setup_chains(firewall, ipt_if_prefix, router_fw_ports) + + # apply the changes immediately (no defer in firewall path) + ipt_mgr.defer_apply_off() + + def _get_chain_name(self, fwid, ver, direction): + return '%s%s%s' % (CHAIN_NAME_PREFIX[direction], + IP_VER_TAG[ver], + fwid) + + def _setup_chains(self, firewall, ipt_if_prefix, router_fw_ports): + """Create Fwaas chain using the rules in the policy + """ + egress_rule_list = firewall['egress_rule_list'] + ingress_rule_list = firewall['ingress_rule_list'] + fwid = firewall['id'] + ipt_mgr = ipt_if_prefix['ipt'] + + # default rules for invalid packets and established sessions + invalid_rule = self._drop_invalid_packets_rule() + est_rule = self._allow_established_rule() + + for ver in [IPV4, IPV6]: + if ver == IPV4: + table = ipt_mgr.ipv4['filter'] + else: + table = ipt_mgr.ipv6['filter'] + ichain_name = self._get_chain_name( + fwid, ver, constants.INGRESS_DIRECTION) + ochain_name = self._get_chain_name( + fwid, ver, constants.EGRESS_DIRECTION) + for name in [ichain_name, ochain_name]: + table.add_chain(name) + table.add_rule(name, invalid_rule) + table.add_rule(name, est_rule) + + for rule in ingress_rule_list: + if not rule['enabled']: + continue + iptbl_rule = self._convert_fwaas_to_iptables_rule(rule) + if rule['ip_version'] == constants.IP_VERSION_4: + ver = IPV4 + table = ipt_mgr.ipv4['filter'] + else: + ver = IPV6 + table = ipt_mgr.ipv6['filter'] + ichain_name = self._get_chain_name( + fwid, ver, constants.INGRESS_DIRECTION) + table.add_rule(ichain_name, iptbl_rule) + + for rule in egress_rule_list: + if not rule['enabled']: + continue + iptbl_rule = self._convert_fwaas_to_iptables_rule(rule) + if rule['ip_version'] == constants.IP_VERSION_4: + ver = IPV4 + table = ipt_mgr.ipv4['filter'] + else: + ver = IPV6 + table = ipt_mgr.ipv6['filter'] + ochain_name = self._get_chain_name( + fwid, ver, constants.EGRESS_DIRECTION) + table.add_rule(ochain_name, iptbl_rule) + + self._enable_policy_chain(fwid, ipt_if_prefix, router_fw_ports) + + def _find_changed_rules(self, pre_firewall, firewall): + """Find the rules changed between the current firewall + and the updating rule + """ + changed_rules = [] + for fw_rule_list in ['egress_rule_list', 'ingress_rule_list']: + pre_fw_rules = pre_firewall[fw_rule_list] + fw_rules = firewall[fw_rule_list] + for pre_fw_rule in pre_fw_rules: + for fw_rule in fw_rules: + if (pre_fw_rule.get('id') == fw_rule.get('id') and + pre_fw_rule != fw_rule): + changed_rules.append(pre_fw_rule) + changed_rules.append(fw_rule) + return changed_rules + + def _find_removed_rules(self, pre_firewall, firewall): + removed_rules = [] + for fw_rule_list in ['egress_rule_list', 'ingress_rule_list']: + pre_fw_rules = pre_firewall[fw_rule_list] + fw_rules = firewall[fw_rule_list] + fw_rule_ids = [fw_rule['id'] for fw_rule in fw_rules] + removed_rules.extend([pre_fw_rule for pre_fw_rule in pre_fw_rules + if pre_fw_rule['id'] not in fw_rule_ids]) + return removed_rules + + def _find_new_rules(self, pre_firewall, firewall): + return self._find_removed_rules(firewall, pre_firewall) + + def _remove_conntrack_new_firewall(self, agent_mode, apply_list, firewall): + """Remove conntrack when create new firewall""" + routers_list = list(set([apply_info[0] for apply_info in apply_list])) + for ri in routers_list: + ipt_if_prefix_list = self._get_ipt_mgrs_with_if_prefix( + agent_mode, ri) + for ipt_if_prefix in ipt_if_prefix_list: + ipt_mgr = ipt_if_prefix['ipt'] + self.conntrack.flush_entries(ipt_mgr.namespace) + + def _remove_conntrack_updated_firewall(self, agent_mode, + apply_list, pre_firewall, firewall): + """Remove conntrack when updated firewall""" + routers_list = list(set([apply_info[0] for apply_info in apply_list])) + for ri in routers_list: + ipt_if_prefix_list = self._get_ipt_mgrs_with_if_prefix( + agent_mode, ri) + for ipt_if_prefix in ipt_if_prefix_list: + ipt_mgr = ipt_if_prefix['ipt'] + ch_rules = self._find_changed_rules(pre_firewall, + firewall) + i_rules = self._find_new_rules(pre_firewall, firewall) + r_rules = self._find_removed_rules(pre_firewall, firewall) + removed_conntrack_rules_list = ch_rules + i_rules + r_rules + self.conntrack.delete_entries(removed_conntrack_rules_list, + ipt_mgr.namespace) + + def _remove_default_chains(self, nsid): + """Remove fwaas default policy chain.""" + self._remove_chain_by_name(IPV4, FWAAS_DEFAULT_CHAIN, nsid) + self._remove_chain_by_name(IPV6, FWAAS_DEFAULT_CHAIN, nsid) + + def _remove_chains(self, fwid, ipt_mgr): + """Remove fwaas policy chain.""" + for ver in [IPV4, IPV6]: + for direction in [constants.INGRESS_DIRECTION, + constants.EGRESS_DIRECTION]: + chain_name = self._get_chain_name(fwid, ver, direction) + self._remove_chain_by_name(ver, chain_name, ipt_mgr) + + def _add_default_policy_chain_v4v6(self, ipt_mgr): + dropped_chain = self._get_action_chain(DROPPED_CHAIN) + ipt_mgr.ipv4['filter'].add_chain(FWAAS_DEFAULT_CHAIN) + ipt_mgr.ipv4['filter'].add_rule( + FWAAS_DEFAULT_CHAIN, '-j %s' % dropped_chain) + ipt_mgr.ipv6['filter'].add_chain(FWAAS_DEFAULT_CHAIN) + ipt_mgr.ipv6['filter'].add_rule( + FWAAS_DEFAULT_CHAIN, '-j %s' % dropped_chain) + + def _add_accepted_chain_v4v6(self, ipt_mgr): + v4rules_in_chain = \ + ipt_mgr.get_chain("filter", ACCEPTED_CHAIN, + ip_version=constants.IP_VERSION_4) + if not v4rules_in_chain: + ipt_mgr.ipv4['filter'].add_chain(ACCEPTED_CHAIN) + ipt_mgr.ipv4['filter'].add_rule(ACCEPTED_CHAIN, '-j ACCEPT') + + v6rules_in_chain = \ + ipt_mgr.get_chain("filter", ACCEPTED_CHAIN, + ip_version=constants.IP_VERSION_6) + if not v6rules_in_chain: + ipt_mgr.ipv6['filter'].add_chain(ACCEPTED_CHAIN) + ipt_mgr.ipv6['filter'].add_rule(ACCEPTED_CHAIN, '-j ACCEPT') + + def _add_dropped_chain_v4v6(self, ipt_mgr): + v4rules_in_chain = \ + ipt_mgr.get_chain("filter", DROPPED_CHAIN, + ip_version=constants.IP_VERSION_4) + if not v4rules_in_chain: + ipt_mgr.ipv4['filter'].add_chain(DROPPED_CHAIN) + ipt_mgr.ipv4['filter'].add_rule(DROPPED_CHAIN, '-j DROP') + + v6rules_in_chain = \ + ipt_mgr.get_chain("filter", DROPPED_CHAIN, + ip_version=constants.IP_VERSION_6) + if not v6rules_in_chain: + ipt_mgr.ipv6['filter'].add_chain(DROPPED_CHAIN) + ipt_mgr.ipv6['filter'].add_rule(DROPPED_CHAIN, '-j DROP') + + def _add_rejected_chain_v4v6(self, ipt_mgr): + v4rules_in_chain = \ + ipt_mgr.get_chain("filter", REJECTED_CHAIN, + ip_version=constants.IP_VERSION_4) + if not v4rules_in_chain: + ipt_mgr.ipv4['filter'].add_chain(REJECTED_CHAIN) + ipt_mgr.ipv4['filter'].add_rule( + REJECTED_CHAIN, + '-j REJECT --reject-with icmp-port-unreachable') + + v6rules_in_chain = \ + ipt_mgr.get_chain("filter", REJECTED_CHAIN, + ip_version=constants.IP_VERSION_6) + if not v6rules_in_chain: + ipt_mgr.ipv6['filter'].add_chain(REJECTED_CHAIN) + ipt_mgr.ipv6['filter'].add_rule( + REJECTED_CHAIN, + '-j REJECT --reject-with icmp6-port-unreachable') + + def _remove_chain_by_name(self, ver, chain_name, ipt_mgr): + if ver == IPV4: + ipt_mgr.ipv4['filter'].remove_chain(chain_name) + else: + ipt_mgr.ipv6['filter'].remove_chain(chain_name) + + def _remove_chain_by_name_v4v6(self, chain_name, ipt_mgr): + ipt_mgr.ipv4['filter'].remove_chain(chain_name) + ipt_mgr.ipv6['filter'].remove_chain(chain_name) + + def _add_rules_to_chain(self, ipt_mgr, ver, chain_name, rules): + if ver == IPV4: + table = ipt_mgr.ipv4['filter'] + else: + table = ipt_mgr.ipv6['filter'] + for rule in rules: + table.add_rule(chain_name, rule) + + def _get_action_chain(self, name): + binary_name = iptables_manager.binary_name + chain_name = iptables_manager.get_chain_name(name) + return '%s-%s' % (binary_name, chain_name) + + def _enable_policy_chain(self, fwid, ipt_if_prefix, router_fw_ports): + bname = iptables_manager.binary_name + ipt_mgr = ipt_if_prefix['ipt'] + if_prefix = ipt_if_prefix['if_prefix'] + + for (ver, tbl) in [(IPV4, ipt_mgr.ipv4['filter']), + (IPV6, ipt_mgr.ipv6['filter'])]: + for direction in [constants.INGRESS_DIRECTION, + constants.EGRESS_DIRECTION]: + chain_name = self._get_chain_name(fwid, ver, direction) + chain_name = iptables_manager.get_chain_name(chain_name) + if chain_name in tbl.chains: + for router_fw_port in router_fw_ports: + intf_name = self._get_intf_name(if_prefix, + router_fw_port) + jump_rule = ['%s %s -j %s-%s' % ( + IPTABLES_DIR[direction], intf_name, + bname, chain_name)] + self._add_rules_to_chain(ipt_mgr, ver, + 'FORWARD', jump_rule) + + # jump to DROP_ALL policy + chain_name = iptables_manager.get_chain_name(FWAAS_DEFAULT_CHAIN) + for router_fw_port in router_fw_ports: + intf_name = self._get_intf_name(if_prefix, + router_fw_port) + jump_rule = ['-o %s -j %s-%s' % (intf_name, bname, chain_name)] + self._add_rules_to_chain(ipt_mgr, IPV4, 'FORWARD', jump_rule) + self._add_rules_to_chain(ipt_mgr, IPV6, 'FORWARD', jump_rule) + + # jump to DROP_ALL policy + chain_name = iptables_manager.get_chain_name(FWAAS_DEFAULT_CHAIN) + for router_fw_port in router_fw_ports: + intf_name = self._get_intf_name(if_prefix, + router_fw_port) + jump_rule = ['-i %s -j %s-%s' % (intf_name, bname, chain_name)] + self._add_rules_to_chain(ipt_mgr, IPV4, 'FORWARD', jump_rule) + self._add_rules_to_chain(ipt_mgr, IPV6, 'FORWARD', jump_rule) + + def _convert_fwaas_to_iptables_rule(self, rule): + action = FWAAS_TO_IPTABLE_ACTION_MAP[rule.get('action')] + + # Output ordering is important here as it must exactly match what + # is returned by iptables-save. If not we risk unnecessarily removing + # and readding rules. + args = [] + + args += self._protocol_arg(rule.get('protocol'), + rule.get('ip_version')) + + args += self._ip_prefix_arg('s', rule.get('source_ip_address')) + args += self._ip_prefix_arg('d', rule.get('destination_ip_address')) + + # iptables adds '-m protocol' when any source + # or destination port number is specified + if (rule.get('source_port') is not None or + rule.get('destination_port') is not None): + args += self._match_arg(rule.get('protocol')) + + args += self._port_arg('sport', + rule.get('protocol'), + rule.get('source_port')) + + args += self._port_arg('dport', + rule.get('protocol'), + rule.get('destination_port')) + + args += self._action_arg(action) + + iptables_rule = ' '.join(args) + return iptables_rule + + def _drop_invalid_packets_rule(self): + dropped_chain = self._get_action_chain(DROPPED_CHAIN) + return '-m state --state INVALID -j %s' % dropped_chain + + def _allow_established_rule(self): + return '-m state --state RELATED,ESTABLISHED -j ACCEPT' + + def _action_arg(self, action): + if not action: + return [] + + args = ['-j', self._get_action_chain(action)] + + return args + + def _protocol_arg(self, protocol, ip_version): + if not protocol: + return [] + + if (protocol == constants.PROTO_NAME_ICMP and + ip_version == constants.IP_VERSION_6): + protocol = constants.PROTO_NAME_IPV6_ICMP + + args = ['-p', protocol] + + return args + + def _match_arg(self, protocol): + if not protocol: + return [] + + protocol_modules = {constants.PROTO_NAME_UDP: 'udp', + constants.PROTO_NAME_TCP: 'tcp', + constants.PROTO_NAME_ICMP: 'icmp', + constants.PROTO_NAME_IPV6_ICMP: 'icmp6'} + # iptables adds '-m protocol' when the port number is specified + args = ['-m', protocol_modules[protocol]] + + return args + + def _port_arg(self, direction, protocol, port): + if protocol not in [constants.PROTO_NAME_UDP, + constants.PROTO_NAME_TCP] or port is None: + return [] + + args = ['--%s' % direction, '%s' % port] + + return args + + def _ip_prefix_arg(self, direction, ip_prefix): + + if not(ip_prefix): + return [] + + args = ['-%s' % direction, '%s' % utils.ip_to_cidr(ip_prefix)] + return args diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/__init__.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/driver_base.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/driver_base.py new file mode 100644 index 000000000..0ed15b4fa --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/driver_base.py @@ -0,0 +1,63 @@ +# Copyright (C) 2017 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import contextlib + +import six + + +@six.add_metaclass(abc.ABCMeta) +class FirewallL2DriverBase(object): + """Abstract firewall L2 driver base""" + + def __init__(self, integration_bridge, sg_enabled=False): + pass + + def filter_defer_apply_on(self): + """Defer application of filtering rule.""" + pass + + def filter_defer_apply_off(self): + """Turn off deferral of rules and apply the rules now.""" + pass + + @property + def ports(self): + """Returns filtered ports.""" + pass + + @contextlib.contextmanager + def defer_apply(self): + """Defer apply context.""" + self.filter_defer_apply_on() + try: + yield + finally: + self.filter_defer_apply_off() + + def create_firewall_group(self, ports, firewall_group): + """Called when a firewall group is created. + """ + raise NotImplementedError() + + def update_firewall_group(self, ports, firewall_group): + """Called when a firewall group is updated. + """ + raise NotImplementedError() + + def delete_firewall_group(self, ports, firewall_group): + """Called when a firewall group is deleted. + """ + raise NotImplementedError() diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/noop/__init__.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/noop/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/noop/noop_driver.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/noop/noop_driver.py new file mode 100644 index 000000000..41f581540 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/noop/noop_driver.py @@ -0,0 +1,41 @@ +# Copyright (C) 2017 Fujitsu Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import helpers as log_helpers + +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2\ + import driver_base + + +class NoopFirewallL2Driver(driver_base.FirewallL2DriverBase): + + @log_helpers.log_method_call + def create_firewall_group(self, ports, firewall_group): + pass + + @log_helpers.log_method_call + def update_firewall_group(self, ports, firewall_group): + pass + + @log_helpers.log_method_call + def delete_firewall_group(self, ports, firewall_group): + pass + + @log_helpers.log_method_call + def process_trusted_ports(self, ports): + pass + + @log_helpers.log_method_call + def remove_trusted_ports(self, port_ids): + pass diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/__init__.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/__init__.py new file mode 100644 index 000000000..892e565fb --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2015 +# 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. + +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import firewall + +OVSFirewallDriver = firewall.OVSFirewallDriver diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/constants.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/constants.py new file mode 100644 index 000000000..b901d1086 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/constants.py @@ -0,0 +1,63 @@ +# Copyright 2015 +# 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. + +from neutron_lib import constants + + +OF_STATE_NOT_TRACKED = "-trk" +OF_STATE_TRACKED = "+trk" +OF_STATE_NEW_NOT_ESTABLISHED = "+new-est" +OF_STATE_NOT_ESTABLISHED = "-est" +OF_STATE_ESTABLISHED = "+est" +OF_STATE_ESTABLISHED_NOT_REPLY = "+est-rel-rpl" +OF_STATE_ESTABLISHED_REPLY = "+est-rel+rpl" +OF_STATE_RELATED = "-new-est+rel-inv" +OF_STATE_INVALID = "+trk+inv" +OF_STATE_NEW = "+new" +OF_STATE_NOT_REPLY_NOT_NEW = "-new-rpl" + +CT_MARK_NORMAL = '0x0' +CT_MARK_INVALID = '0x1' + +REG_PORT = 5 +REG_NET = 6 + +FW_BASE_EGRESS_TABLE = 64 +FW_RULES_EGRESS_TABLE = 65 +FW_ACCEPT_OR_INGRESS_TABLE = 66 +FW_BASE_INGRESS_TABLE = 68 +FW_RULES_INGRESS_TABLE = 69 + +OVS_FIREWALL_TABLES = ( + FW_BASE_EGRESS_TABLE, + FW_RULES_EGRESS_TABLE, + FW_ACCEPT_OR_INGRESS_TABLE, + FW_BASE_INGRESS_TABLE, + FW_RULES_INGRESS_TABLE, +) + +PROTOCOLS_WITH_PORTS = (constants.PROTO_NAME_SCTP, + constants.PROTO_NAME_TCP, + constants.PROTO_NAME_UDP) + +# Only map protocols that need special handling +REVERSE_IP_PROTOCOL_MAP_WITH_PORTS = { + constants.IP_PROTOCOL_MAP[proto]: proto for proto in + PROTOCOLS_WITH_PORTS} + +ethertype_to_dl_type_map = { + constants.IPv4: constants.ETHERTYPE_IP, + constants.IPv6: constants.ETHERTYPE_IPV6, +} diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/exceptions.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/exceptions.py new file mode 100644 index 000000000..bd7618cf9 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/exceptions.py @@ -0,0 +1,26 @@ +# Copyright 2016, Red Hat, Inc. +# 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. + +from neutron_lib import exceptions + +from neutron_fwaas._i18n import _ + + +class OVSFWaaSPortNotFound(exceptions.NeutronException): + message = _("Port %(port_id)s is not managed by this agent.") + + +class OVSFWaaSTagNotFound(exceptions.NeutronException): + message = _("Cannot get vlan tag for port %(port_id)s.") diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/firewall.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/firewall.py new file mode 100644 index 000000000..a317f9e4a --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/firewall.py @@ -0,0 +1,1017 @@ +# Copyright 2015 +# 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. + +import netaddr + +from neutron_lib import constants as lib_const +from oslo_log import log as logging +from oslo_utils import netutils + +from neutron.agent import firewall +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts + +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2\ + import driver_base +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import constants as fwaas_ovs_consts +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import exceptions +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import rules + +LOG = logging.getLogger(__name__) + +ACTION_ALLOW = 'allow' + + +# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver. +def _replace_register(flow_params, register_number, register_value): + """Replace value from flows to given register number + + 'register_value' key in dictionary will be replaced by register number + given by 'register_number' + + :param flow_params: Dictionary containing defined flows + :param register_number: The number of register where value will be stored + :param register_value: Key to be replaced by register number + + """ + try: + reg_port = flow_params[register_value] + del flow_params[register_value] + flow_params['reg{:d}'.format(register_number)] = reg_port + except KeyError: + pass + + +# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that +# differs only in constants REG_PORT/REG_NET. +def create_reg_numbers(flow_params): + """Replace reg_(port|net) values with defined register numbers""" + _replace_register(flow_params, fwaas_ovs_consts.REG_PORT, 'reg_port') + _replace_register(flow_params, fwaas_ovs_consts.REG_NET, 'reg_net') + + +class FirewallGroup(object): + def __init__(self, id_): + self.id = id_ + self.ingress_rules = [] + self.egress_rules = [] + self.members = {} + self.ports = set() + + def update_rules(self, ingress_rules, egress_rules): + """Update firewall group with ingress/egress rules. + + If a rule has a protocol field, it is normalized to a number + here in order to ease later processing. + """ + def _translate_protocol_to_number(rule): + protocol = rule.get('protocol') + if protocol is not None: + if protocol.isdigit(): + rule['protocol'] = int(protocol) + elif (rule.get('ethertype') == lib_const.IPv6 and + protocol == lib_const.PROTO_NAME_ICMP): + rule['protocol'] = lib_const.PROTO_NUM_IPV6_ICMP + else: + rule['protocol'] = lib_const.IP_PROTOCOL_MAP.get( + protocol, protocol) + return rule + + self.ingress_rules = [_translate_protocol_to_number(ir) + for ir in ingress_rules] + self.egress_rules = [_translate_protocol_to_number(er) + for er in egress_rules] + + def get_ethertype_filtered_addresses(self, ethertype, + exclude_addresses=None): + exclude_addresses = set(exclude_addresses if exclude_addresses else []) + group_addresses = set(self.members.get(ethertype, [])) + return list(group_addresses - exclude_addresses) + + +# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that +# differs only in firewall groups list field name +class OFPort(object): + def __init__(self, port_dict, ovs_port, vlan_tag): + self.id = port_dict['device'] + self.vlan_tag = vlan_tag + self.mac = ovs_port.vif_mac + self.lla_address = str(netutils.get_ipv6_addr_by_EUI64( + lib_const.IPv6_LLA_PREFIX, self.mac)) + self.ofport = ovs_port.ofport + self.fw_group = None + self.fixed_ips = port_dict.get('fixed_ips', []) + self.neutron_port_dict = port_dict.copy() + self.allowed_pairs_v4 = self._get_allowed_pairs(port_dict, version=4) + self.allowed_pairs_v6 = self._get_allowed_pairs(port_dict, version=6) + + @staticmethod + def _get_allowed_pairs(port_dict, version): + aap_dict = port_dict.get('allowed_address_pairs', set()) + return {(aap['mac_address'], aap['ip_address']) for aap in aap_dict + if netaddr.IPNetwork(aap['ip_address']).version == version} + + @property + def all_allowed_macs(self): + macs = {item[0] for item in self.allowed_pairs_v4.union( + self.allowed_pairs_v6)} + macs.add(self.mac) + return macs + + @property + def ipv4_addresses(self): + return [ip_addr for ip_addr in + [fixed_ip['ip_address'] for fixed_ip in self.fixed_ips] + if netaddr.IPAddress(ip_addr).version == 4] + + @property + def ipv6_addresses(self): + return [ip_addr for ip_addr in + [fixed_ip['ip_address'] for fixed_ip in self.fixed_ips] + if netaddr.IPAddress(ip_addr).version == 6] + + def update(self, port_dict): + self.allowed_pairs_v4 = self._get_allowed_pairs(port_dict, + version=4) + self.allowed_pairs_v6 = self._get_allowed_pairs(port_dict, + version=6) + # Neighbour discovery uses LLA + self.allowed_pairs_v6.add((self.mac, self.lla_address)) + self.fixed_ips = port_dict.get('fixed_ips', []) + self.neutron_port_dict = port_dict.copy() + + +# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that +# differs in methods name [s/sg/fwg] and update_rules method. +class FWGPortMap(object): + def __init__(self): + self.ports = {} + self.fw_groups = {} + # Maps port_id to ofport number + self.unfiltered = {} + + def get_fwg(self, fwg_id): + return self.fw_groups.get(fwg_id, None) + + def get_or_create_fwg(self, fwg_id): + fw_group = self.get_fwg(fwg_id) + if not fw_group: + fw_group = FirewallGroup(fwg_id) + self.fw_groups[fwg_id] = fw_group + return fw_group + + def delete_fwg(self, fwg_id): + del self.fw_groups[fwg_id] + + # XXX NOTE(ivasilevskaya) couldn't find any logical definition why + # firewall_group should come as 3rd argument instead of adding fwg_id + # to port_dict. Removed in favor of SG api + def create_port(self, port, port_dict): + self.ports[port.id] = port + self.update_port(port, port_dict) + + # XXX NOTE(ivasilevskaya) couldn't find any logical definition why + # firewall_group should come as 3rd argument instead of adding fwg_id + # to port_dict. Removed in favor of SG api + def update_port(self, port, port_dict): + for fw_group in self.fw_groups.values(): + fw_group.ports.discard(port) + + fw_group = self.get_or_create_fwg(port_dict['firewall_group']) + port.fw_group = fw_group + fw_group.ports.add(port) + port.update(port_dict) + + def remove_port(self, port): + if port.fw_group: + port.fw_group.ports.discard(port) + del self.ports[port.id] + + def update_rules(self, fwg_id, ingress_rules, egress_rules): + fw_group = self.get_or_create_fwg(fwg_id) + fw_group.update_rules(ingress_rules, egress_rules) + + def update_members(self, fwg_id, members): + fw_group = self.get_or_create_fwg(fwg_id) + fw_group.members = members + + +# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that +# doesn't have a conjunction manager because no remote_group_id concept is +# applicable to firewall groups +class OVSFirewallDriver(driver_base.FirewallL2DriverBase): + REQUIRED_PROTOCOLS = [ + ovs_consts.OPENFLOW10, + ovs_consts.OPENFLOW11, + ovs_consts.OPENFLOW12, + ovs_consts.OPENFLOW13, + ovs_consts.OPENFLOW14, + ] + + provides_arp_spoofing_protection = True + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver. + # This driver won't have any conj_manager logic because there is no concept + # of remote_group_id for firewall groups (that I know of at least) + def __init__(self, agent_api, sg_with_ovs=False): + """Initialize object""" + + integration_bridge = agent_api.request_int_br() + self.int_br = self.initialize_bridge(integration_bridge) + self.fwg_port_map = FWGPortMap() + self.fwg_to_delete = set() + self._deferred = False + self.sg_with_ovs = sg_with_ovs + self._drop_all_unmatched_flows() + self._initialize_third_party_tables() + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def _accept_flow(self, **flow): + for f in rules.create_accept_flows(flow, self.sg_with_ovs): + self._add_flow(**f) + + def _drop_flow(self, **flow): + for f in rules.create_drop_flows(flow): + self._add_flow(**f) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def _add_flow(self, **kwargs): + dl_type = kwargs.get('dl_type') + create_reg_numbers(kwargs) + if isinstance(dl_type, int): + kwargs['dl_type'] = "0x{:04x}".format(dl_type) + if self._deferred: + self.int_br.add_flow(**kwargs) + else: + self.int_br.br.add_flow(**kwargs) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def _delete_flows(self, **kwargs): + create_reg_numbers(kwargs) + if self._deferred: + self.int_br.delete_flows(**kwargs) + else: + self.int_br.br.delete_flows(**kwargs) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def _strict_delete_flow(self, **kwargs): + """Delete given flow right away even if bridge is deferred. + + Delete command will use strict delete. + """ + create_reg_numbers(kwargs) + self.int_br.br.delete_flows(strict=True, **kwargs) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + @staticmethod + def initialize_bridge(int_br): + int_br.add_protocols(*OVSFirewallDriver.REQUIRED_PROTOCOLS) + return int_br.deferred(full_ordered=True) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver, + # differs in constants + def _drop_all_unmatched_flows(self): + for table in fwaas_ovs_consts.OVS_FIREWALL_TABLES: + if (table == fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE and + self.sg_with_ovs): + continue + self.int_br.br.add_flow(table=table, priority=0, actions='drop') + + def _initialize_third_party_tables(self): + self.int_br.br.add_flow( + table=ovs_consts.ACCEPTED_EGRESS_TRAFFIC_TABLE, + priority=1, + actions='normal') + for table in (ovs_consts.ACCEPTED_INGRESS_TRAFFIC_TABLE, + ovs_consts.DROPPED_TRAFFIC_TABLE): + self.int_br.br.add_flow( + table=table, priority=0, actions='drop') + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def get_ovs_port(self, port_id): + ovs_port = self.int_br.br.get_vif_port_by_id(port_id) + if not ovs_port: + raise exceptions.OVSFWaaSPortNotFound(port_id=port_id) + return ovs_port + + def _get_port_vlan_tag(self, port): + vlan_tag = port.get('lvlan', None) + if not vlan_tag: + raise exceptions.OVSFWaaSTagNotFound(port_id=port['device']) + return vlan_tag + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def get_ofport(self, port): + port_id = port['device'] + return self.fwg_port_map.ports.get(port_id) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver, + # self.sg_port_map -> self.fwg_port_map + def get_or_create_ofport(self, port): + """Get ofport specified by port['device'], checking and reflecting + ofport changes. + If ofport is nonexistent, create and return one. + """ + port_id = port['device'] + ovs_port = self.get_ovs_port(port_id) + try: + of_port = self.fwg_port_map.ports[port_id] + except KeyError: + port_vlan_id = self._get_port_vlan_tag(port) + of_port = OFPort(port, ovs_port, port_vlan_id) + self.fwg_port_map.create_port(of_port, port) + else: + if of_port.ofport != ovs_port.ofport: + self.fwg_port_map.remove_port(of_port) + of_port = OFPort(port, ovs_port, of_port.vlan_tag) + self.fwg_port_map.update_port(of_port, port) + + return of_port + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def is_port_managed(self, port): + return port['device'] in self.fwg_port_map.ports + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def prepare_port_filter(self, port): + # NOTE(annp): port no security should be handled by security group in + # co-existence mode, otherwise(standalone mode) fwg will handle it. + if not firewall.port_sec_enabled(port) and not self.sg_with_ovs: + self._initialize_egress_no_port_security(port) + return + old_of_port = self.get_ofport(port) + # Make sure delete old allow_address_pair MACs because + # allow_address_pair MACs will be updated in + # self.get_or_create_ofport(port) + if old_of_port: + LOG.error("Initializing port %s that was already " + "initialized.", + port['device']) + self.delete_all_port_flows(old_of_port) + of_port = self.get_or_create_ofport(port) + self.initialize_port_flows(of_port) + self.add_flows_from_rules(of_port) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def update_port_filter(self, port): + """Update rules for given port + + Current existing filtering rules are removed and new ones are generated + based on current loaded firewall group rules and members. + + Note: port no security should be handled by security group in + co-existence mode, otherwise fwg will handle it. + + """ + if not firewall.port_sec_enabled(port) and not self.sg_with_ovs: + self.remove_port_filter(port) + self._initialize_egress_no_port_security(port) + return + elif not self.is_port_managed(port): + if not self.sg_with_ovs: + self._remove_egress_no_port_security(port['device']) + self.prepare_port_filter(port) + return + + old_of_port = self.get_ofport(port) + of_port = self.get_or_create_ofport(port) + # TODO(jlibosva): Handle firewall blink + self.delete_all_port_flows(old_of_port) + self.initialize_port_flows(of_port) + self.add_flows_from_rules(of_port) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver, + # sg_port_map -> fwg_port_map + def remove_port_filter(self, port): + """Remove port from firewall + + All flows related to this port are removed from ovs. Port is also + removed from ports managed by this firewall. + + """ + if self.is_port_managed(port): + of_port = self.get_ofport(port) + self.delete_all_port_flows(of_port) + self.fwg_port_map.remove_port(of_port) + self._schedule_fwg_deletion_maybe(of_port.fw_group.id) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # with ingress\egress rules arguments instead of single rules + def update_firewall_group_rules(self, fwg_id, ingress_rules, egress_rules): + self.fwg_port_map.update_rules(fwg_id, ingress_rules, egress_rules) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # with sg_port_map -> fwg_port_map + def _schedule_fwg_deletion_maybe(self, fwg_id): + """Schedule possible deletion of the given firewall group. + + This function must be called when the number of ports + associated to fwg_id drops to zero, as it isn't possible + to know FWG deletions from agents due to RPC API design. + """ + fwg_group = self.fwg_port_map.get_or_create_fwg(fwg_id) + if not fwg_group.members or not fwg_group.ports: + self.fwg_to_delete.add(fwg_id) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # with sg_port_map -> fwg_port_map + def _cleanup_stale_fwg(self): + fwg_to_delete = self.fwg_to_delete + self.fwg_to_delete = set() + + for fwg_id in fwg_to_delete: + fw_group = self.fwg_port_map.get_fwg(fwg_id) + if fw_group.members and fw_group.ports: + # firewall group is still in use + continue + + self.fwg_port_map.delete_fwg(fwg_id) + + def process_trusted_ports(self, ports): + """Pass packets from these ports directly to ingress pipeline.""" + if self.sg_with_ovs: + return + + for port in ports: + self._initialize_egress_no_port_security(port) + + def remove_trusted_ports(self, port_ids): + if self.sg_with_ovs: + return + + for port_id in port_ids: + self._remove_egress_no_port_security(port_id) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def filter_defer_apply_on(self): + self._deferred = True + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def filter_defer_apply_off(self): + if self._deferred: + self._cleanup_stale_fwg() + self.int_br.apply_flows() + self._deferred = False + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # with sg_port_map -> fwg_port_map + @property + def ports(self): + return {id_: port.neutron_port_dict + for id_, port in self.fwg_port_map.ports.items()} + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def initialize_port_flows(self, port): + """Set base flows for port + + :param port: OFPort instance + + """ + # Identify egress flow + self._add_flow( + table=ovs_consts.TRANSIENT_TABLE, + priority=105, + in_port=port.ofport, + actions='set_field:{:d}->reg{:d},' + 'set_field:{:d}->reg{:d},' + 'resubmit(,{:d})'.format( + port.ofport, + fwaas_ovs_consts.REG_PORT, + port.vlan_tag, + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.FW_BASE_EGRESS_TABLE) + ) + + # Identify ingress flows after egress filtering + for mac_addr in port.all_allowed_macs: + self._add_flow( + table=ovs_consts.TRANSIENT_TABLE, + priority=95, + dl_dst=mac_addr, + dl_vlan='0x%x' % port.vlan_tag, + actions='set_field:{:d}->reg{:d},' + 'set_field:{:d}->reg{:d},' + 'strip_vlan,resubmit(,{:d})'.format( + port.ofport, + fwaas_ovs_consts.REG_PORT, + port.vlan_tag, + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.FW_BASE_INGRESS_TABLE), + ) + + self._initialize_egress(port) + self._initialize_ingress(port) + + def _fwaas_process_colocated_ingress(self, port): + for mac_addr in port.all_allowed_macs: + self._add_flow( + table=ovs_consts.ACCEPT_OR_INGRESS_TABLE, + priority=105, + dl_dst=mac_addr, + reg_net=port.vlan_tag, + actions='set_field:{:d}->reg{:d},resubmit(,{:d})'.format( + port.ofport, + fwaas_ovs_consts.REG_PORT, + fwaas_ovs_consts.FW_BASE_INGRESS_TABLE), + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_egress_ipv6_icmp(self, port): + for icmp_type in firewall.ICMPV6_ALLOWED_EGRESS_TYPES: + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=95, + in_port=port.ofport, + reg_port=port.ofport, + dl_type=lib_const.ETHERTYPE_IPV6, + nw_proto=lib_const.PROTO_NUM_IPV6_ICMP, + icmp_type=icmp_type, + actions='normal') + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) and exception classes + def _initialize_egress_no_port_security(self, port): + port_id = port['device'] + try: + ovs_port = self.get_ovs_port(port_id) + vlan_tag = self._get_port_vlan_tag(port) + except exceptions.OVSFWaaSTagNotFound: + # It's a patch port, don't set anything + return + except exceptions.OVSFWaaSPortNotFound as not_found_e: + LOG.error("Initializing unfiltered port %(port_id)s that does not " + "exist in ovsdb: %(err)s.", + {'port_id': port_id, + 'err': not_found_e}) + return + self.fwg_port_map.unfiltered[port_id] = ovs_port.ofport + self._add_flow( + table=ovs_consts.TRANSIENT_TABLE, + priority=100, + in_port=ovs_port.ofport, + actions='set_field:%d->reg%d,' + 'set_field:%d->reg%d,' + 'resubmit(,%d)' % ( + ovs_port.ofport, + fwaas_ovs_consts.REG_PORT, + vlan_tag, + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE) + ) + self._add_flow( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + priority=80, + reg_port=ovs_port.ofport, + actions='normal', + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _remove_egress_no_port_security(self, port_id): + try: + ofport = self.fwg_port_map.unfiltered[port_id] + except KeyError: + LOG.debug("Port %s is not handled by the firewall.", port_id) + return + self._delete_flows( + table=ovs_consts.TRANSIENT_TABLE, + in_port=ofport + ) + self._delete_flows( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + reg_port=ofport + ) + del self.fwg_port_map.unfiltered[port_id] + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_egress(self, port): + """Identify egress traffic and send it to egress base""" + self._initialize_egress_ipv6_icmp(port) + + # Apply mac/ip pairs for IPv4 + allowed_pairs = port.allowed_pairs_v4.union( + {(port.mac, ip_addr) for ip_addr in port.ipv4_addresses}) + for mac_addr, ip_addr in allowed_pairs: + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=95, + in_port=port.ofport, + reg_port=port.ofport, + dl_src=mac_addr, + dl_type=lib_const.ETHERTYPE_ARP, + arp_spa=ip_addr, + actions='normal' + ) + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=65, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED, + dl_type=lib_const.ETHERTYPE_IP, + in_port=port.ofport, + dl_src=mac_addr, + nw_src=ip_addr, + actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format( + fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + fwaas_ovs_consts.REG_NET) + ) + + # Apply mac/ip pairs for IPv6 + allowed_pairs = port.allowed_pairs_v6.union( + {(port.mac, ip_addr) for ip_addr in port.ipv6_addresses}) + for mac_addr, ip_addr in allowed_pairs: + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=65, + reg_port=port.ofport, + in_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED, + dl_type=lib_const.ETHERTYPE_IPV6, + dl_src=mac_addr, + ipv6_src=ip_addr, + actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format( + fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + fwaas_ovs_consts.REG_NET) + ) + + # DHCP discovery + accept_or_ingress = fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE + if self.sg_with_ovs: + accept_or_ingress = ovs_consts.ACCEPT_OR_INGRESS_TABLE + for dl_type, src_port, dst_port in ( + (lib_const.ETHERTYPE_IP, 68, 67), + (lib_const.ETHERTYPE_IPV6, 546, 547)): + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=80, + reg_port=port.ofport, + in_port=port.ofport, + dl_type=dl_type, + nw_proto=lib_const.PROTO_NUM_UDP, + tp_src=src_port, + tp_dst=dst_port, + actions='resubmit(,{:d})'.format(accept_or_ingress) + ) + # Ban dhcp service running on an instance + for dl_type, src_port, dst_port in ( + (lib_const.ETHERTYPE_IP, 67, 68), + (lib_const.ETHERTYPE_IPV6, 547, 546)): + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=70, + in_port=port.ofport, + reg_port=port.ofport, + dl_type=dl_type, + nw_proto=lib_const.PROTO_NUM_UDP, + tp_src=src_port, + tp_dst=dst_port, + actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE + ) + + # Drop Router Advertisements from instances + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=70, + in_port=port.ofport, + reg_port=port.ofport, + dl_type=lib_const.ETHERTYPE_IPV6, + nw_proto=lib_const.PROTO_NUM_IPV6_ICMP, + icmp_type=lib_const.ICMPV6_TYPE_RA, + actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE + ) + + # Drop all remaining not tracked egress connections + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=10, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED, + in_port=port.ofport, + reg_port=port.ofport, + actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE + ) + + # Fill in accept_or_ingress table by checking that traffic is ingress + # and if not, accept it + if self.sg_with_ovs: + self._fwaas_process_colocated_ingress(port) + else: + for mac_addr in port.all_allowed_macs: + self._add_flow( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + priority=100, + dl_dst=mac_addr, + reg_net=port.vlan_tag, + actions='set_field:{:d}->reg{:d},resubmit(,{:d})'.format( + port.ofport, + fwaas_ovs_consts.REG_PORT, + fwaas_ovs_consts.FW_BASE_INGRESS_TABLE), + ) + for ethertype in [lib_const.ETHERTYPE_IP, + lib_const.ETHERTYPE_IPV6]: + self._add_flow( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + priority=90, + dl_type=ethertype, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED, + actions='ct(commit,zone=NXM_NX_REG{:d}[0..15]),' + 'resubmit(,{:d})'.format( + fwaas_ovs_consts.REG_NET, + ovs_consts.ACCEPTED_EGRESS_TRAFFIC_TABLE) + ) + self._add_flow( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + priority=80, + reg_port=port.ofport, + actions='normal' + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_tracked_egress(self, port): + # Drop invalid packets + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + priority=50, + ct_state=fwaas_ovs_consts.OF_STATE_INVALID, + actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE + ) + # Drop traffic for removed fwg rules + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + priority=50, + reg_port=port.ofport, + ct_mark=fwaas_ovs_consts.CT_MARK_INVALID, + actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE + ) + + for state in ( + fwaas_ovs_consts.OF_STATE_ESTABLISHED_REPLY, + fwaas_ovs_consts.OF_STATE_RELATED, + ): + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + priority=50, + ct_state=state, + ct_mark=fwaas_ovs_consts.CT_MARK_NORMAL, + reg_port=port.ofport, + ct_zone=port.vlan_tag, + actions='normal' + ) + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + priority=40, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_ESTABLISHED, + actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE + ) + for ethertype in [lib_const.ETHERTYPE_IP, lib_const.ETHERTYPE_IPV6]: + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + priority=40, + dl_type=ethertype, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED, + actions="ct(commit,zone=NXM_NX_REG{:d}[0..15]," + "exec(set_field:{:s}->ct_mark))".format( + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.CT_MARK_INVALID) + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_ingress_ipv6_icmp(self, port): + for icmp_type in firewall.ICMPV6_ALLOWED_INGRESS_TYPES: + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE, + priority=100, + reg_port=port.ofport, + dl_dst=port.mac, + dl_type=lib_const.ETHERTYPE_IPV6, + nw_proto=lib_const.PROTO_NUM_IPV6_ICMP, + icmp_type=icmp_type, + actions='output:{:d}'.format(port.ofport) + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_ingress(self, port): + # Allow incoming ARPs + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE, + priority=100, + dl_type=lib_const.ETHERTYPE_ARP, + reg_port=port.ofport, + actions='output:{:d}'.format(port.ofport) + ) + self._initialize_ingress_ipv6_icmp(port) + + # DHCP offers + for dl_type, src_port, dst_port in ( + (lib_const.ETHERTYPE_IP, 67, 68), + (lib_const.ETHERTYPE_IPV6, 547, 546)): + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE, + priority=95, + reg_port=port.ofport, + dl_type=dl_type, + nw_proto=lib_const.PROTO_NUM_UDP, + tp_src=src_port, + tp_dst=dst_port, + actions='output:{:d}'.format(port.ofport) + ) + + # Track untracked + for dl_type in (lib_const.ETHERTYPE_IP, lib_const.ETHERTYPE_IPV6): + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE, + priority=90, + reg_port=port.ofport, + dl_type=dl_type, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED, + actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format( + fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + fwaas_ovs_consts.REG_NET) + ) + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE, + ct_state=fwaas_ovs_consts.OF_STATE_TRACKED, + priority=80, + reg_port=port.ofport, + actions='resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_RULES_INGRESS_TABLE) + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_tracked_ingress(self, port): + # Drop invalid packets + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + priority=50, + ct_state=fwaas_ovs_consts.OF_STATE_INVALID, + actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE + ) + # Drop traffic for removed fwg rules + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + priority=50, + reg_port=port.ofport, + ct_mark=fwaas_ovs_consts.CT_MARK_INVALID, + actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE + ) + + # Allow established and related connections + for state in (fwaas_ovs_consts.OF_STATE_ESTABLISHED_REPLY, + fwaas_ovs_consts.OF_STATE_RELATED): + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + priority=50, + reg_port=port.ofport, + ct_state=state, + ct_mark=fwaas_ovs_consts.CT_MARK_NORMAL, + ct_zone=port.vlan_tag, + actions='output:{:d}'.format(port.ofport) + ) + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + priority=40, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_ESTABLISHED, + actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE + ) + for ethertype in [lib_const.ETHERTYPE_IP, lib_const.ETHERTYPE_IPV6]: + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + priority=40, + dl_type=ethertype, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED, + actions="ct(commit,zone=NXM_NX_REG{:d}[0..15]," + "exec(set_field:{:s}->ct_mark))".format( + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.CT_MARK_INVALID) + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) and rules_generator method + def add_flows_from_rules(self, port): + self._initialize_tracked_ingress(port) + self._initialize_tracked_egress(port) + LOG.debug('Creating flow rules for port %s that is port %d in OVS', + port.id, port.ofport) + for rule in self.create_rules_generator_for_port(port): + flows = rules.create_flows_from_rule_and_port(rule, port) + LOG.debug("RULGEN: Rules generated for flow %s are %s", + rule, flows) + for flow in flows: + if rule.get('action') == ACTION_ALLOW: + self._accept_flow(**flow) + else: + self._drop_flow(**flow) + + def create_rules_generator_for_port(self, port): + """Returns a generator emitting rules valid for further processing + + Injects necessary fields to feed one-by-one to rules module to + transform into valid openflow rules. + """ + + def inject_fields(rule, direction, offset=0): + """Add fields to rule dict to be able to utilize rules module + + Currently such fields are added: + 'offset', 'direction', 'ethertype', 'source_port_range_min', + 'source_port_range_max', 'port_range_min', 'port_range_max' + """ + # XXX NOTE(ivasilevskaya) maybe there's a clever way to do that + version_ethertype_map = {lib_const.IP_VERSION_4: lib_const.IPv4, + lib_const.IP_VERSION_6: lib_const.IPv6} + + rule['direction'] = direction + rule['ethertype'] = version_ethertype_map[rule['ip_version']] + rule['offset'] = offset + + # transfer destination_port into port_range_min/port_range_max + def add_range(range_key, key_min, key_max): + range_str = rule.get(range_key) + if not range_str: + return + ports = range_str.split(':', 1) + rule[key_min] = int(ports[0]) + rule['port_range_max'] = ( + int(ports[1]) if len(ports) == 2 else int(ports[0])) + + add_range('destination_port', 'port_range_min', 'port_range_max') + add_range('source_port', 'source_port_range_min', + 'source_port_range_max') + + # add direction field + offset = len(port.fw_group.ingress_rules) - 1 + for rule in port.fw_group.ingress_rules: + inject_fields(rule, lib_const.INGRESS_DIRECTION, offset) + offset -= 1 + yield rule + + offset = len(port.fw_group.egress_rules) - 1 + for rule in port.fw_group.egress_rules: + inject_fields(rule, lib_const.EGRESS_DIRECTION, offset) + offset -= 1 + yield rule + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def delete_all_port_flows(self, port): + """Delete all flows for given port""" + accept_or_ingress = fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE + if self.sg_with_ovs: + accept_or_ingress = ovs_consts.ACCEPT_OR_INGRESS_TABLE + + for mac_addr in port.all_allowed_macs: + self._strict_delete_flow(priority=95, + table=ovs_consts.TRANSIENT_TABLE, + dl_dst=mac_addr, + dl_vlan=port.vlan_tag) + self._delete_flows( + table=accept_or_ingress, + dl_dst=mac_addr, reg_net=port.vlan_tag) + self._strict_delete_flow(priority=105, + table=ovs_consts.TRANSIENT_TABLE, + in_port=port.ofport) + self._delete_flows(reg_port=port.ofport) + + def create_firewall_group(self, ports_for_fwg, firewall_group): + egress_rules = firewall_group['egress_rule_list'] + ingress_rules = firewall_group['ingress_rule_list'] + fwg_id = firewall_group['id'] + + self.update_firewall_group_rules(fwg_id, ingress_rules, egress_rules) + for port in ports_for_fwg: + port['firewall_group'] = fwg_id + self.update_port_filter(port) + + def update_firewall_group(self, ports_for_fwg, firewall_group): + self.create_firewall_group(ports_for_fwg, firewall_group) + + def delete_firewall_group(self, ports_for_fwg, firewall_group): + for port in ports_for_fwg: + port['firewall_group'] = firewall_group['id'] + self.remove_port_filter(port) diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/rules.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/rules.py new file mode 100644 index 000000000..2fbe1bc49 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/rules.py @@ -0,0 +1,208 @@ +# Copyright 2015 Red Hat, Inc. +# 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. + +import netaddr + +from neutron.common import utils +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts +from neutron_lib import constants as n_consts +from oslo_log import log as logging + +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import constants as fwaas_ovs_consts + + +LOG = logging.getLogger(__name__) + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver, differs in +# constants +CT_STATES = [ + fwaas_ovs_consts.OF_STATE_ESTABLISHED_NOT_REPLY, + fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED] + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +FLOW_FIELD_FOR_IPVER_AND_DIRECTION = { + (n_consts.IP_VERSION_4, n_consts.EGRESS_DIRECTION): 'nw_dst', + (n_consts.IP_VERSION_6, n_consts.EGRESS_DIRECTION): 'ipv6_dst', + (n_consts.IP_VERSION_4, n_consts.INGRESS_DIRECTION): 'nw_src', + (n_consts.IP_VERSION_6, n_consts.INGRESS_DIRECTION): 'ipv6_src', +} + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +FORBIDDEN_PREFIXES = (n_consts.IPv4_ANY, n_consts.IPv6_ANY) + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +def is_valid_prefix(ip_prefix): + # IPv6 have multiple ways how to describe ::/0 network, converting to + # IPNetwork and back to string unifies it + return (ip_prefix and + str(netaddr.IPNetwork(ip_prefix)) not in FORBIDDEN_PREFIXES) + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +def create_flows_from_rule_and_port(rule, port): + ethertype = rule['ethertype'] + direction = rule['direction'] + dst_ip_prefix = rule.get('dest_ip_prefix') + src_ip_prefix = rule.get('source_ip_prefix') + offset = int(rule.get('offset', 0)) + + flow_template = { + 'priority': 70 + offset, + 'dl_type': fwaas_ovs_consts.ethertype_to_dl_type_map[ethertype], + 'reg_port': port.ofport, + } + + if is_valid_prefix(dst_ip_prefix): + flow_template[FLOW_FIELD_FOR_IPVER_AND_DIRECTION[( + utils.get_ip_version(dst_ip_prefix), n_consts.EGRESS_DIRECTION)] + ] = dst_ip_prefix + + if is_valid_prefix(src_ip_prefix): + flow_template[FLOW_FIELD_FOR_IPVER_AND_DIRECTION[( + utils.get_ip_version(src_ip_prefix), n_consts.INGRESS_DIRECTION)] + ] = src_ip_prefix + + flows = create_protocol_flows(direction, flow_template, port, rule) + + return flows + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver, differs in +# constants +def populate_flow_common(direction, flow_template, port): + """Initialize common flow fields.""" + if direction == n_consts.INGRESS_DIRECTION: + flow_template['table'] = fwaas_ovs_consts.FW_RULES_INGRESS_TABLE + flow_template['actions'] = "output:{:d}".format(port.ofport) + elif direction == n_consts.EGRESS_DIRECTION: + flow_template['table'] = fwaas_ovs_consts.FW_RULES_EGRESS_TABLE + # Traffic can be both ingress and egress, check that no ingress rules + # should be applied + flow_template['actions'] = 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE) + return flow_template + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +def create_protocol_flows(direction, flow_template, port, rule): + flow_template = populate_flow_common(direction, + flow_template.copy(), + port) + protocol = rule.get('protocol') + if protocol is not None: + flow_template['nw_proto'] = protocol + + if protocol in [n_consts.PROTO_NUM_ICMP, n_consts.PROTO_NUM_IPV6_ICMP]: + flows = create_icmp_flows(flow_template, rule) + else: + flows = create_port_range_flows(flow_template, rule) + return flows or [flow_template] + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver, differs only in +# constant +def create_port_range_flows(flow_template, rule): + protocol = fwaas_ovs_consts.REVERSE_IP_PROTOCOL_MAP_WITH_PORTS.get( + rule.get('protocol')) + if protocol is None: + return [] + flows = [] + src_port_match = '{:s}_src'.format(protocol) + src_port_min = rule.get('source_port_range_min') + src_port_max = rule.get('source_port_range_max') + dst_port_match = '{:s}_dst'.format(protocol) + dst_port_min = rule.get('port_range_min') + dst_port_max = rule.get('port_range_max') + + dst_port_range = [] + if dst_port_min and dst_port_max: + dst_port_range = utils.port_rule_masking(dst_port_min, dst_port_max) + + src_port_range = [] + if src_port_min and src_port_max: + src_port_range = utils.port_rule_masking(src_port_min, src_port_max) + for port in src_port_range: + flow = flow_template.copy() + flow[src_port_match] = port + if dst_port_range: + for port in dst_port_range: + dst_flow = flow.copy() + dst_flow[dst_port_match] = port + flows.append(dst_flow) + else: + flows.append(flow) + else: + for port in dst_port_range: + flow = flow_template.copy() + flow[dst_port_match] = port + flows.append(flow) + + return flows + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +def create_icmp_flows(flow_template, rule): + icmp_type = rule.get('port_range_min') + if icmp_type is None: + return + flow = flow_template.copy() + flow['icmp_type'] = icmp_type + + icmp_code = rule.get('port_range_max') + if icmp_code is not None: + flow['icmp_code'] = icmp_code + return [flow] + + +def resubmit_to_sg(flow): + if flow['table'] == fwaas_ovs_consts.FW_RULES_EGRESS_TABLE: + flow['actions'] = 'resubmit(,{:d})'.format( + ovs_consts.RULES_EGRESS_TABLE) + if flow['table'] == fwaas_ovs_consts.FW_RULES_INGRESS_TABLE: + flow['actions'] = 'resubmit(,{:d})'.format( + ovs_consts.RULES_INGRESS_TABLE) + + +def create_accept_flows(flow, sg_enabled=False): + flow['ct_state'] = CT_STATES[0] + if sg_enabled: + resubmit_to_sg(flow) + result = [flow.copy()] + flow['ct_state'] = CT_STATES[1] + if sg_enabled: + resubmit_to_sg(flow) + elif flow['table'] == fwaas_ovs_consts.FW_RULES_INGRESS_TABLE: + flow['actions'] = ( + 'ct(commit,zone=NXM_NX_REG{:d}[0..15]),{:s},' + 'resubmit(,{:d})'.format( + fwaas_ovs_consts.REG_NET, flow['actions'], + ovs_consts.ACCEPTED_INGRESS_TRAFFIC_TABLE) + ) + result.append(flow) + return result + + +def create_drop_flows(flow): + if flow['table'] in [fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + fwaas_ovs_consts.FW_RULES_EGRESS_TABLE]: + flow['actions'] = 'resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE + flow['ct_state'] = fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED + result = [flow.copy()] + flow['ct_state'] = fwaas_ovs_consts.OF_STATE_ESTABLISHED_NOT_REPLY + result.append(flow) + return result diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/legacy_conntrack.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/legacy_conntrack.py new file mode 100644 index 000000000..398893260 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/legacy_conntrack.py @@ -0,0 +1,234 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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. + +from neutron.agent.linux import utils as linux_utils +from neutron_lib import constants +from oslo_log import log as logging + +from neutron_fwaas.services.firewall.service_drivers.agents.drivers import\ + conntrack_base + +LOG = logging.getLogger(__name__) + +IP_VERSIONS = [constants.IP_VERSION_4, constants.IP_VERSION_6] + +ATTR_POSITIONS = { + 'icmp': (('type', 5), ('code', 6), ('src', 3), ('dst', 4), ('id', 7)), + 'icmpv6': (('type', 5), ('code', 6), ('src', 3), ('dst', 4), ('id', 7)), + 'tcp': (('sport', 6), ('dport', 7), ('src', 4), ('dst', 5)), + 'udp': (('sport', 5), ('dport', 6), ('src', 3), ('dst', 4)) +} + + +def normalize_filters_tuple(rule_tuple): + new_rule = [] + for el in rule_tuple: + if el is None: + new_rule.append('') + else: + new_rule.append(el) + return tuple(new_rule) + + +class ConntrackLegacy(conntrack_base.ConntrackDriverBase): + def initialize(self, execute=None): + LOG.debug('Initialize Conntrack Legacy') + self.execute = execute or linux_utils.execute + + def flush_entries(self, namespace): + prefixcmd = ['ip', 'netns', 'exec', namespace] if namespace else [] + cmd = prefixcmd + ['conntrack', '-D'] + self._execute_command(cmd) + + def delete_entries(self, rules, namespace): + rule_filters = [self._get_filter_from_rule(r) for r in rules] + rule_filters.sort(key=normalize_filters_tuple) + + delete_entries = self._get_entries_to_delete( + rule_filters, self.list_entries(namespace)) + for delete_entry in delete_entries: + cmd = self._get_conntrack_cmd_from_entry(delete_entry, namespace) + self._execute_command(cmd) + + def _execute_command(self, cmd): + try: + output = self.execute(cmd, + run_as_root=True, + check_exit_code=True, + extra_ok_codes=[1], + privsep_exec=True) + except RuntimeError: + msg = "Failed execute conntrack command %s" % cmd + raise RuntimeError(msg) + return output + + def list_entries(self, namespace): + """List and parse all conntrack entries + + :param namespace: namespace to get conntrack entries + :returns: sorted list of conntrack entries in Python tuple + for example: [(4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 1234), + (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2')] + """ + parsed_entries = [] + prefixcmd = ['ip', 'netns', 'exec', namespace] if namespace else [] + for ip_version in IP_VERSIONS: + cmd = prefixcmd + ['conntrack', '-L', + '-f', 'ipv' + str(ip_version)] + raw_entries = self._execute_command(cmd).splitlines() + for raw_entry in raw_entries: + parsed_entry = self._parse_entry(raw_entry.split(), ip_version) + if parsed_entry is not None: + parsed_entries.append(parsed_entry) + return sorted(parsed_entries) + + def _get_conntrack_cmd_from_entry(self, entry, namespace): + prefixcmd = ['ip', 'netns', 'exec', namespace] if namespace else [] + cmd = ['conntrack', '-D'] + contrack_filter = ['-f', 'ipv' + str(entry[0]), '-p', entry[1]] + if entry[1] in ['icmp', 'icmpv6']: + contrack_filter.extend(['--icmp-type', entry[2], + '--icmp-code', entry[3], + '-s', entry[4], + '-d', entry[5], + '--icmp-id', entry[6]]) + else: + contrack_filter.extend(['--sport', entry[2], + '--dport', entry[3], + '-s', entry[4], + '-d', entry[5]]) + exec_cmd = prefixcmd + cmd + contrack_filter + return exec_cmd + + def _parse_entry(self, entry, ip_version): + """Parse entry from text to Python tuple + + :param entry: conntrack entry as a list of string + :param ip_version: ip version 4 or 6 + :returns: conntrack entry in Python tuple + for example: (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2') + The attributes are ordered to be easy to compare with other entries + and compare with firewall rule + """ + protocol = entry[0] + if protocol not in ATTR_POSITIONS: + LOG.warning( + 'Skipping conntrack entry %s with unsupported protocol', entry) + return None + + parsed_entry = [ip_version, protocol] + for attr, position in ATTR_POSITIONS[protocol]: + val = entry[position].partition('=')[2] + parsed_entry.append(int(val) if attr in ['sport', 'dport', 'type', + 'code', 'id'] else val) + return tuple(parsed_entry) + + def _get_entries_to_delete(self, rule_filters, entries): + """Specify conntrack entries to delete + + :param rule_filters: List of filters parsed from firewall rules + :param entries: all entries within namespace + :returns: conntrack entries to delete + """ + # List all entries from namespace, they are already parsed + # to a list of tuples: + # [(4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 1234), + # (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2')] + delete_entries = [] + entry_index = 0 + entry_number = len(entries) + for rule_filter in rule_filters: + while entry_index < entry_number: + # Compare entry with rule + comp = self._compare_entry_and_rule_filter( + rule_filter, entries[entry_index]) + # Increase entry_index when entry is under rule + if comp < 0: + entry_index += 1 + # Append entry to delete_entry if it matches with rule + elif comp == 0: + delete_entries.append(entries[entry_index]) + entry_index += 1 + # Switch to new higher rule + else: + break + return delete_entries + + @staticmethod + def _get_filter_from_rule(rule): + """Parse the firewall rule to a tuple + + :param rule: firewall rule + :returns: a tuple of parsed information + """ + rule_filter = [] + keys = ('ip_version', 'protocol', + 'source_port', 'destination_port', + 'source_ip_address', 'destination_ip_address') + for key in keys: + if key in ('source_port', 'destination_port'): + port_range = rule.get(key, []) + if port_range: + port_lower, sep, port_upper = port_range.partition(':') + port_upper = port_upper if sep else port_lower + port_range = [port_lower, port_upper] + rule_filter.append(port_range or []) + else: + rule_filter.append(rule.get(key, [])) + return tuple(rule_filter) + + @staticmethod + def _compare_entry_and_rule_filter(rule_filter, entry): + """Define that the entry should be deleted or not + + :param rule_filter: filter that is parsed from a firewall rule + for example: (4, 'tcp', ['22', '33'], ['44', '55']) + :param entry: parsed conntrack entry, + for example: (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2') + :returns: -1 if entry is lower than rule + 0 if entry matches rule, + 1 if entry is higher than rule + """ + ENTRY_IS_LOWER = -1 + ENTRY_MATCHES = 0 + ENTRY_IS_HIGHER = 1 + rule_ip_version = rule_filter[0] + if entry[0] < rule_ip_version: + return ENTRY_IS_LOWER + elif entry[0] > rule_ip_version: + return ENTRY_IS_HIGHER + rule_protocol = rule_filter[1] + if rule_protocol == constants.PROTO_NAME_IPV6_ICMP: + rule_protocol = constants.PROTO_NAME_IPV6_ICMP_LEGACY + if rule_protocol: + if entry[1] < rule_protocol: + return ENTRY_IS_LOWER + elif entry[1] > rule_protocol: + return ENTRY_IS_HIGHER + sport_range = rule_filter[2] + if sport_range: + sport_range = [int(port) for port in sport_range] + if entry[2] < min(sport_range[0], sport_range[-1]): + return ENTRY_IS_LOWER + elif entry[2] > max(sport_range[0], sport_range[-1]): + return ENTRY_IS_HIGHER + dport_range = rule_filter[3] + if dport_range: + dport_range = [int(port) for port in dport_range] + if entry[3] < min(dport_range[0], dport_range[-1]): + return ENTRY_IS_LOWER + elif entry[3] > max(dport_range[0], dport_range[-1]): + return ENTRY_IS_HIGHER + return ENTRY_MATCHES diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/netlink_conntrack.py b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/netlink_conntrack.py new file mode 100644 index 000000000..a43cc5e39 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/drivers/linux/netlink_conntrack.py @@ -0,0 +1,144 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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. + +from neutron_lib import constants +from oslo_log import log as logging + +from neutron_fwaas.privileged import netlink_lib as nl_lib +from neutron_fwaas.services.firewall.service_drivers.agents.drivers import\ + conntrack_base + +LOG = logging.getLogger(__name__) + + +class ConntrackNetlink(conntrack_base.ConntrackDriverBase): + def initialize(self, *args, **kwargs): + LOG.debug('Conntrack Netlink loaded') + + def flush_entries(self, namespace): + """Flush all conntrack entries within the namespace + + :param namespace: namespace to flush + :return: None + """ + nl_lib.flush_entries(namespace) + + def delete_entries(self, rules, namespace): + rule_filters = (self._get_filter_from_rule(r) for r in rules) + rule_filters = sorted(rule_filters) + entries = nl_lib.list_entries(namespace) + delete_entries = self._get_entries_to_delete(rule_filters, entries) + if delete_entries: + nl_lib.delete_entries(delete_entries, namespace) + + def _get_entries_to_delete(self, rule_filters, entries): + """Specify conntrack entries to delete + + :param rule_filters: List of filters parsed from firewall rules + :param entries: all entries within namespace + :return: conntrack entries to delete + """ + # List all entries from namespace, they are already parsed + # to a list of tuples: + # [(4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 1234), + # (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2')] + delete_entries = [] + entry_index = 0 + entry_number = len(entries) + for rule_filter in rule_filters: + while entry_index < entry_number: + # Compare entry with rule + comp = self._compare_entry_and_rule(rule_filter, + entries[entry_index]) + # Increase entry_index when entry is under rule + if comp < 0: + entry_index += 1 + # Append entry to delete_entry if it matches with rule + elif comp == 0: + delete_entries.append(entries[entry_index]) + entry_index += 1 + # Switch to new higher rule + else: + break + return delete_entries + + @staticmethod + def _get_filter_from_rule(rule): + """Parse the firewall rule to a tuple + + :param rule: firewall rule + :return: a tuple of parsed information + """ + rule_filter = [] + keys = ['ip_version', 'protocol', + 'source_port', 'destination_port', + 'source_ip_address', 'destination_ip_address'] + for key in keys: + if key in ['source_port', 'destination_port']: + port_range = rule.get(key, []) + if port_range: + port_lower, sep, port_upper = port_range.partition(':') + port_upper = port_upper if sep else port_lower + port_range = [port_lower, port_upper] + rule_filter.append(port_range or []) + else: + rule_filter.append(rule.get(key, [])) + return tuple(rule_filter) + + @staticmethod + def _compare_entry_and_rule(rule_filter, entry): + """Define that the entry should be deleted or not + + :param rule_filter: filter that is parsed from a firewall rule + ex: (4, 'tcp', 1, 2) + :param entry: parsed conntrack entry, + ex: (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2') + :return: -1 if entry is lower than rule, 0 if entry matches rule, + 1 if entry is higher than rule + """ + ENTRY_IS_LOWER = -1 + ENTRY_MATCHES = 0 + ENTRY_IS_HIGHER = 1 + rule_ipversion = rule_filter[0] + + if entry[0] < rule_ipversion: + return ENTRY_IS_LOWER + elif entry[0] > rule_ipversion: + return ENTRY_IS_HIGHER + rule_protocol = rule_filter[1] + + if rule_protocol: + if rule_protocol == constants.PROTO_NAME_IPV6_ICMP: + rule_protocol = constants.PROTO_NAME_IPV6_ICMP_LEGACY + if entry[1] < rule_protocol: + return ENTRY_IS_LOWER + elif entry[1] > rule_protocol: + return ENTRY_IS_HIGHER + + sport_range = rule_filter[2] + if sport_range: + sport_range = [int(port) for port in sport_range] + if entry[2] < min(sport_range[0], sport_range[-1]): + return ENTRY_IS_LOWER + elif entry[2] > max(sport_range[0], sport_range[-1]): + return ENTRY_IS_HIGHER + dport_range = rule_filter[3] + if dport_range: + dport_range = [int(port) for port in dport_range] + if entry[3] < min(dport_range[0], dport_range[-1]): + return ENTRY_IS_LOWER + elif entry[3] > max(dport_range[0], dport_range[-1]): + return ENTRY_IS_HIGHER + return ENTRY_MATCHES diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/firewall_agent_api.py b/neutron_fwaas/services/firewall/service_drivers/agents/firewall_agent_api.py new file mode 100644 index 000000000..9fb36113c --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/firewall_agent_api.py @@ -0,0 +1,94 @@ +# Copyright (c) 2013 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. + +from neutron_lib import rpc as n_rpc +from oslo_config import cfg +import oslo_messaging + +from neutron_fwaas._i18n import _ + + +FWAAS_V1 = "v1" +FWAAS_V2 = "v2" +FW_L2_NOOP_DRIVER = 'noop' + +FWaaSOpts = [ + cfg.StrOpt( + 'driver', + default='', + help=_("Name of the FWaaS Driver")), + cfg.BoolOpt( + 'enabled', + default=False, + help=_("Enable FWaaS")), + cfg.StrOpt( + 'agent_version', + default=FWAAS_V2, + help=_("Firewall agent class")), + cfg.StrOpt( + 'conntrack_driver', + default='conntrack', + help=_("Name of the FWaaS Conntrack Driver")), + cfg.StrOpt( + 'firewall_l2_driver', + default=FW_L2_NOOP_DRIVER, + help=_("Name of the firewall l2 driver") + ) +] +cfg.CONF.register_opts(FWaaSOpts, 'fwaas') + + +class FWaaSPluginApiMixin(object): + """Agent side of the FWaaS agent to FWaaS Plugin RPC API.""" + + def __init__(self, topic, host): + # NOTE(annp): Mixin class should call super + super(FWaaSPluginApiMixin, self).__init__() + + self.host = host + target = oslo_messaging.Target(topic=topic, version='1.0') + self.client = n_rpc.get_client(target) + + def set_firewall_status(self, context, firewall_id, status): + """Make a RPC to set the status of a firewall.""" + cctxt = self.client.prepare() + return cctxt.call(context, 'set_firewall_status', host=self.host, + firewall_id=firewall_id, status=status) + + def firewall_deleted(self, context, firewall_id): + """Make a RPC to indicate that the firewall resources are deleted.""" + cctxt = self.client.prepare() + return cctxt.call(context, 'firewall_deleted', host=self.host, + firewall_id=firewall_id) + + +class FWaaSAgentRpcCallbackMixin(object): + """Mixin for FWaaS agent Implementations.""" + + def __init__(self, host): + + super(FWaaSAgentRpcCallbackMixin, self).__init__(host) + + def create_firewall(self, context, firewall, host): + """Handle RPC cast from plugin to create a firewall.""" + pass + + def update_firewall(self, context, firewall, host): + """Handle RPC cast from plugin to update a firewall.""" + pass + + def delete_firewall(self, context, firewall, host): + """Handle RPC cast from plugin to delete a firewall.""" + pass diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/firewall_service.py b/neutron_fwaas/services/firewall/service_drivers/agents/firewall_service.py new file mode 100644 index 000000000..aa0f17d7a --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/firewall_service.py @@ -0,0 +1,44 @@ +# Copyright 2014 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. + +from neutron.services import provider_configuration as provconf +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import importutils + +from neutron_fwaas._i18n import _ + +LOG = logging.getLogger(__name__) + +FIREWALL_DRIVERS = 'firewall_drivers' + + +class FirewallService(object): + """Firewall Service observer.""" + + def load_device_drivers(self): + """Loads a single device driver for FWaaS.""" + device_driver = provconf.get_provider_driver_class( + cfg.CONF.fwaas.driver, FIREWALL_DRIVERS) + try: + driver = importutils.import_object(device_driver) + LOG.debug('Loaded FWaaS device driver: %s', device_driver) + return driver + except ImportError: + msg = _('Error importing FWaaS device driver: %s') + raise ImportError(msg % device_driver) + except ValueError: + msg = _('Configuration error - no FWaaS device_driver specified') + raise ValueError(msg) diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/l2/__init__.py b/neutron_fwaas/services/firewall/service_drivers/agents/l2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/l2/fwaas_v2.py b/neutron_fwaas/services/firewall/service_drivers/agents/l2/fwaas_v2.py new file mode 100644 index 000000000..f9b0394d2 --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/l2/fwaas_v2.py @@ -0,0 +1,493 @@ +# Copyright 2017-2018 FUJITSU LIMITED +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_concurrency import lockutils +from oslo_config import cfg +from oslo_log import log as logging +import six + +from neutron.agent import securitygroups_rpc +from neutron import manager +from neutron.plugins.ml2.drivers.openvswitch.agent import vlanmanager +from neutron_lib.agent import l2_extension +from neutron_lib import constants as nl_const +from neutron_lib.exceptions import firewall_v2 as f_exc +from neutron_lib import rpc as n_rpc +from neutron_lib.utils import net as nl_net + +from neutron_fwaas._i18n import _ +from neutron_fwaas.common import fwaas_constants as consts +from neutron_fwaas.services.firewall.service_drivers.agents import\ + firewall_agent_api as api + +LOG = logging.getLogger(__name__) + +FWAAS_L2_DRIVER = 'neutron.agent.l2.firewall_drivers' +SG_OVS_DRIVER = 'openvswitch' + + +class FWaaSL2PluginApi(api.FWaaSPluginApiMixin): + """L2 agent side of FWaaS agent-to-plugin RPC API""" + + def get_firewall_group_for_port(self, context, port_id): + """Get firewall group is associated with a port""" + + LOG.debug("Get firewall group is associated with port %s", port_id) + cctxt = self.client.prepare() + return cctxt.call(context, 'get_firewall_group_for_port', + port_id=port_id) + + def set_firewall_group_status(self, context, fwg_id, status, host): + """Set the status of a group operation.""" + + LOG.debug("Fetch firewall group changing status") + cctxt = self.client.prepare() + return cctxt.call(context, 'set_firewall_group_status', + fwg_id=fwg_id, status=status, host=host) + + def firewall_group_deleted(self, context, fwg_id, host): + """Notifies the plugin that a firewall group has been deleted.""" + + LOG.debug("Notify to the plugin that firewall group has been deleted") + cctxt = self.client.prepare() + return cctxt.call(context, 'firewall_group_deleted', + fwg_id=fwg_id, host=host) + + +class FWaaSV2AgentExtension(l2_extension.L2AgentExtension): + + def initialize(self, connection, driver_type): + """Perform Agent Extension initialization""" + + self.conf = cfg.CONF + self.vlan_manager = vlanmanager.LocalVlanManager() + fw_l2_driver_cls = self._load_l2_driver_class(driver_type) + sg_enabled = securitygroups_rpc.is_firewall_enabled() + sg_firewall_driver = self.conf.SECURITYGROUP.firewall_driver + sg_with_ovs = sg_enabled and (sg_firewall_driver == SG_OVS_DRIVER) + self.driver = manager.NeutronManager.load_class_for_provider( + FWAAS_L2_DRIVER, fw_l2_driver_cls)(self.agent_api, sg_with_ovs) + self.plugin_rpc = FWaaSL2PluginApi( + consts.FIREWALL_PLUGIN, self.conf.host) + self.start_rpc_listeners() + self.fwg_map = PortFirewallGroupMap() + + def consume_api(self, agent_api): + self.agent_api = agent_api + + def start_rpc_listeners(self): + self.conn = n_rpc.Connection() + endpoints = [self] + self.conn.create_consumer(consts.FW_AGENT, endpoints, fanout=False) + return self.conn.consume_in_threads() + + def _load_l2_driver_class(self, driver_type): + driver = self.conf.fwaas.firewall_l2_driver or 'noop' + if driver == api.FW_L2_NOOP_DRIVER: + return driver + + if driver != driver_type: + raise Exception( + _("Firewall l2 driver: %s is not compatible"), driver_type) + return driver + + def _is_port_layer2(self, port): + """This function checks if a port belongs to a L2 case. + + Currently both DHCP and router ports are eliminated. + """ + + return port and port.get('device_owner', '').startswith( + nl_const.DEVICE_OWNER_COMPUTE_PREFIX) + + def _get_firewall_group_ports(self, fwg, host, to_delete=False): + port_list = [] + port_ids = fwg['del-port-ids'] if to_delete else fwg['add-port-ids'] + + LOG.debug("_get_fwg fwg=%(fwg)s ports=%(port)s to_delete=%(delete)s", + {'fwg': fwg, 'port': port_ids, 'delete': to_delete}) + for fw_port in port_ids: + port_detail = fwg['port_details'].get(fw_port) + if (self._is_port_layer2(port_detail) and + port_detail.get('host') == host): + port_list.append(port_detail) + return port_list + + @staticmethod + def _has_ports(fwg, event): + """Verifying fwg has ports or not + + This function verify applying firewall group on ports + :param fwg: a fwg object + :param event: create/update firewall group or + create/update/delete port + :return: True if applying firewall group is fine. Otherwise is False + """ + if event == consts.UPDATE_FWG and 'last-port' in fwg: + return not fwg['last-port'] + else: + return bool(fwg['ports']) + + @staticmethod + def _has_policy(fwg): + """Verifying fwg has policy or not""" + return bool(fwg['ingress_firewall_policy_id'] or + fwg['egress_firewall_policy_id']) + + def _compute_status(self, fwg, result, event=consts.CREATE_FWG): + """Compute a status of specified firewall group for update + + Validates 'ACTIVE', 'DOWN', 'INACTIVE', 'ERROR' and None as follows: + - "ERROR" : if result is not True + - "ACTIVE" : admin_state_up is True and exists ports + - "INACTIVE" : admin_state_up is True and with no ports + - "DOWN" : admin_state_up is False + - None : In case of 'delete_firewall_group' + """ + if not result: + return nl_const.ERROR + + if not fwg['admin_state_up']: + return nl_const.DOWN + + if event == consts.DELETE_FWG: + # This firewall_group will be deleted. No need to update status. + return + + if (self._has_ports(fwg, event) and self._has_policy(fwg)): + return nl_const.ACTIVE + + return nl_const.INACTIVE + + def _get_network_id(self, fwg_port): + port_id = fwg_port.get('port_id', fwg_port.get('id')) + port_details = fwg_port.get('port_details') + + if port_details: + target = port_details.get(port_id) + if target: + return target.get('network_id') + return + + return fwg_port.get('network_id') + + def _add_local_vlan_to_ports(self, fwg_ports): + """Add local VLAN to ports if found + + This function tries to add local VLAN related to ports. + """ + + ports_with_lvlan = [] + for fwg_port in fwg_ports: + try: + network_id = self._get_network_id(fwg_port) + l_vlan = self.vlan_manager.get(network_id).vlan + fwg_port['lvlan'] = int(l_vlan) + except vlanmanager.MappingNotFound: + LOG.warning("No Local VLAN found in network %s", network_id) + # NOTE(yushiro): We ignore this exception because we should send + # all selected ports to driver layer. It depends on driver's + # behavior whether it occurs an error with no local VLAN or not. + ports_with_lvlan.append(fwg_port) + + return ports_with_lvlan + + def _apply_fwg_rules(self, fwg, ports, event=consts.UPDATE_FWG): + """This function invokes the driver create/update routine. """ + # Set firewall group status; will be overwritten if call to driver + # fails. + if event in [consts.CREATE_FWG, consts.UPDATE_FWG]: + ports_for_driver = self._add_local_vlan_to_ports(ports) + else: + ports_for_driver = ports + + # apply firewall group to driver + try: + if event == consts.UPDATE_FWG: + self.driver.update_firewall_group(ports_for_driver, fwg) + elif event == consts.DELETE_FWG: + self.driver.delete_firewall_group(ports_for_driver, fwg) + elif event == consts.CREATE_FWG: + self.driver.create_firewall_group(ports_for_driver, fwg) + except f_exc.FirewallInternalDriverError: + msg = _("FWaaS driver error in %(event)s_firewall_group " + "for firewall group: %(fwg_id)s") + LOG.exception(msg, {'event': event, 'fwg_id': fwg['id']}) + return False + return True + + def _send_fwg_status(self, context, fwg_id, status, host): + """Send firewall group's status to plugin. + + :returns: True if no exception occurred otherwise False + :rtype: boolean + """ + try: + self.plugin_rpc.set_firewall_group_status( + context, fwg_id, status, host) + LOG.debug("Successfully sent status(%s) for firewall_group(%s)", + status, fwg_id) + except Exception: + msg = _("Failed to send status for firewall_group(%s)") + LOG.exception(msg, fwg_id) + + def _create_firewall_group(self, context, fwg, host, + event=consts.CREATE_FWG): + """Handles RPC from plugin to create a firewall group. """ + + add_ports = self._get_firewall_group_ports(fwg, host) + if not add_ports: + status = nl_const.INACTIVE + else: + ret = self._apply_fwg_rules(fwg, add_ports, event) + + # cleanup port_map + for port in add_ports: + self.fwg_map.remove_port(port) + + status = self._compute_status(fwg, ret, event) + for port in add_ports: + self.fwg_map.set_port_fwg(port, fwg) + # Update status of firewall group which is associated with ports + # after updating. + self._send_fwg_status(context, fwg['id'], status, host) + + def _delete_firewall_group(self, context, fwg, host, + event=consts.DELETE_FWG): + """Handles RPC from plugin to delete a firewall group. """ + + del_ports = self._get_firewall_group_ports(fwg, host, to_delete=True) + if not del_ports: + return + + # cleanup all flows of del_ports + ret = self._apply_fwg_rules(fwg, del_ports, event=consts.DELETE_FWG) + del_port_ids = [] + for port in del_ports: + del_port_ids.append(port['id']) + self.fwg_map.remove_port(port) + + if event == consts.DELETE_FWG: + self.fwg_map.remove_fwg(fwg) + self.plugin_rpc.firewall_group_deleted( + context, fwg['id'], host=self.conf.host) + else: + status = self._compute_status(fwg, ret, event) + self._send_fwg_status(context, fwg['id'], status, self.conf.host) + + @lockutils.synchronized('fwg') + def create_firewall_group(self, context, firewall_group, host): + """Handles create firewall group event""" + + # TODO(chandanc): Fix agent RPC endpoint to remove host arg + host = cfg.CONF.host + with self.driver.defer_apply(): + try: + self._create_firewall_group(context, firewall_group, host) + except Exception as exc: + LOG.exception( + "Exception caught in create_firewall_group %s", exc) + self._send_fwg_status(context, firewall_group['id'], + status=nl_const.ERROR, host=host) + + @lockutils.synchronized('fwg') + def delete_firewall_group(self, context, firewall_group, host): + """Handles delete firewall group event""" + + # TODO(chandanc): Fix agent RPC endpoint to remove host arg + host = cfg.CONF.host + with self.driver.defer_apply(): + try: + self._delete_firewall_group(context, firewall_group, host) + except Exception as exc: + LOG.exception( + "Exception caught in delete_firewall_group %s", exc) + self._send_fwg_status(context, firewall_group['id'], + status=nl_const.ERROR, host=host) + + @lockutils.synchronized('fwg') + def update_firewall_group(self, context, firewall_group, host): + """Handles update firewall group event""" + + # TODO(chandanc): Fix agent RPC endpoint to remove host arg + host = cfg.CONF.host + with self.driver.defer_apply(): + try: + self._delete_firewall_group( + context, firewall_group, host, event=consts.UPDATE_FWG) + self._create_firewall_group( + context, firewall_group, host, event=consts.UPDATE_FWG) + except Exception as exc: + LOG.exception( + "Exception caught in update_firewall_group %s", exc) + self._send_fwg_status(context, firewall_group['id'], + status=nl_const.ERROR, host=host) + + @lockutils.synchronized('fwg-port') + def handle_port(self, context, port): + """Handle port update event""" + + # Check if port is trusted and called at once. + if nl_net.is_port_trusted(port) and not self.fwg_map.get_port(port): + self._add_rule_for_trusted_port(port) + self.fwg_map.set_port(port) + return + + if not self._is_port_layer2(port): + return + + # check if port is already assigned to a fwg + if self.fwg_map.get_port_fwg(port): + return + + fwg = self.plugin_rpc.get_firewall_group_for_port( + context, port.get('port_id')) + if not fwg: + LOG.info("Firewall group applied to port %s is " + "not available on server.", port['port_id']) + return + + ret = self._apply_fwg_rules(fwg, [port]) + status = self._compute_status(fwg, ret, event=consts.HANDLE_PORT) + self.fwg_map.set_port_fwg(port, fwg) + self._send_fwg_status( + context, fwg_id=fwg['id'], status=status, host=self.conf.host) + + def _add_rule_for_trusted_port(self, port): + self._add_local_vlan_to_ports([port]) + self.driver.process_trusted_ports([port]) + + def _delete_rule_for_trusted_port(self, port): + self.driver.remove_trusted_ports([port['port_id']]) + + def delete_port(self, context, port): + """This is being called when a port is deleted by the agent. """ + + # delete_port should be handled only unbound timing for a port. + # If 'vif_port' is included in the port dict, this is called after + # deleted the port and should be ignored. + if 'vif_port' in port: + return + + port = self.fwg_map.get_port(port) + + if port and nl_net.is_port_trusted(port): + self._delete_rule_for_trusted_port(port) + self.fwg_map.remove_port(port) + return + + if not self._is_port_layer2(port): + return + + fwg = self.fwg_map.get_port_fwg(port) + if not fwg: + LOG.info("Firewall group associated to port %(port_id)s is " + "not available on server.", {'port_id': port['port_id']}) + return + + ret = self._apply_fwg_rules(fwg, [port], event=consts.DELETE_FWG) + + port_id = self.fwg_map.port_id(port) + if port_id in fwg['ports']: + fwg['ports'].remove(port_id) + + # update the fwg dict to known_fwgs + self.fwg_map.set_fwg(fwg) + self.fwg_map.remove_port(port) + status = self._compute_status(fwg, ret, event=consts.DELETE_PORT) + self._send_fwg_status(context, fwg['id'], status, self.conf.host) + + +class PortFirewallGroupMap(object): + """Store relations between Port and Firewall Group and trusted port + + This map is used in deleting firewall_group because the firewall_group has + been deleted at that time. Therefore, it is impossible to refer 'ports'. + This map enables to refer 'ports' for specified firewall_group. + Furthermore, it is necessary to check 'device_owner' for trusted port, this + Map also stores trusted port data. + """ + def __init__(self): + self.known_fwgs = {} + self.port_fwg = {} + self.port_detail = {} + # TODO(yushiro): If agent is restarted, this map doesn't have any + # information. Need to consider map initialization in __init__() + + def port_id(self, port): + return (port if isinstance(port, six.string_types) + else port.get('port_id', port.get('id'))) + + def get_fwg(self, fwg_id): + return self.known_fwgs.get(fwg_id) + + def set_fwg(self, fwg): + self.known_fwgs[fwg['id']] = fwg + + def get_port(self, port): + return self.port_detail.get(self.port_id(port)) + + def get_port_fwg(self, port): + fwg_id = self.port_fwg.get(self.port_id(port)) + if fwg_id: + return self.get_fwg(fwg_id) + + def set_port(self, port): + """Add a new port into port_detail""" + port_id = self.port_id(port) + self.port_detail[port_id] = port + + def set_port_fwg(self, port, fwg): + """Add a new port into fwg['ports']""" + port_id = self.port_id(port) + # Update fwg['ports'] data + fwg['ports'] = list(set(fwg['ports'] + [port_id])) + # Update fwg_id -> firewall_group data + self.known_fwgs[fwg['id']] = fwg + # Update port_id -> port data + self.port_detail[port_id] = port + # Update port_id -> firewall_group_id relation + self.port_fwg[port_id] = fwg['id'] + + def remove_port(self, port): + """Remove port from fwg['ports'] and port_fwg dictionary + + When removing 'port' from several cases, the port should be removed + from this map. + """ + port_id = self.port_id(port) + # Check if 'port_id' has registered in port_fwg dictionary. + # Update firewall_group + if port_id in self.port_fwg: + fwg_id = self.port_fwg.get(port_id) + if not fwg_id: + # This case is trusted port. Try to delete port_detail dict + try: + del self.port_detail[port_id] + except KeyError: + pass + return + new_fwg = self.known_fwgs[fwg_id] + new_fwg['ports'] = [p for p in new_fwg['ports'] if p != port_id] + self.known_fwgs[fwg_id] = new_fwg + del self.port_fwg[port_id] + del self.port_detail[port_id] + + def remove_fwg(self, fwg): + """Remove firewall_group from known_fwgs dictionary + + When removing firewall_group, it should be removed from this map + """ + if fwg['id'] in self.known_fwgs: + del self.known_fwgs[fwg['id']] diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/l3reference/__init__.py b/neutron_fwaas/services/firewall/service_drivers/agents/l3reference/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/firewall/service_drivers/agents/l3reference/firewall_l3_agent_v2.py b/neutron_fwaas/services/firewall/service_drivers/agents/l3reference/firewall_l3_agent_v2.py new file mode 100644 index 000000000..3cb8ff84f --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/agents/l3reference/firewall_l3_agent_v2.py @@ -0,0 +1,552 @@ +# Copyright (c) 2016 +# 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. + +from neutron.agent.linux import ip_lib +from neutron_lib.agent import l3_extension +from neutron_lib import constants as nl_constants +from neutron_lib import context +from neutron_lib.exceptions import firewall_v2 as fw_ext +from neutron_lib import rpc as n_rpc +from oslo_config import cfg +from oslo_log import helpers as log_helpers +from oslo_log import log as logging + +from neutron_fwaas.common import fwaas_constants +from neutron_fwaas.common import resources as f_resources +from neutron_fwaas.services.firewall.service_drivers.agents import\ + firewall_agent_api as api +from neutron_fwaas.services.firewall.service_drivers.agents import\ + firewall_service + + +LOG = logging.getLogger(__name__) + + +class FWaaSL3PluginApi(api.FWaaSPluginApiMixin): + """Agent side of the FWaaS agent-to-plugin RPC API.""" + def __init__(self, topic, host): + super(FWaaSL3PluginApi, self).__init__(topic, host) + + def get_firewall_groups_for_project(self, context, **kwargs): + """Fetches a project's firewall groups from the plugin.""" + LOG.debug("Fetch firewall groups from plugin") + cctxt = self.client.prepare() + return cctxt.call(context, 'get_firewall_groups_for_project', + host=self.host) + + def get_projects_with_firewall_groups(self, context, **kwargs): + """Fetches from the plugin all projects that have firewall groups + configured. + """ + LOG.debug("Fetch from plugin projects that have firewall groups " + "configured") + cctxt = self.client.prepare() + return cctxt.call(context, + 'get_projects_with_firewall_groups', host=self.host) + + def firewall_group_deleted(self, context, fwg_id, **kwargs): + """Notifies the plugin that a firewall group has been deleted.""" + LOG.debug("Notify plugin that firewall group has been deleted") + cctxt = self.client.prepare() + return cctxt.call(context, 'firewall_group_deleted', fwg_id=fwg_id, + host=self.host) + + def set_firewall_group_status(self, context, fwg_id, status, **kwargs): + """Sets firewall group's status on the plugin.""" + LOG.debug("Set firewall groups from plugin") + cctxt = self.client.prepare() + return cctxt.call(context, 'set_firewall_group_status', + fwg_id=fwg_id, status=status, host=self.host) + + +class FWaaSL3AgentExtension(l3_extension.L3AgentExtension): + """FWaaS agent extension.""" + + SUPPORTED_RESOURCE_TYPES = [f_resources.FIREWALL_GROUP, + f_resources.FIREWALL_POLICY, + f_resources.FIREWALL_RULE] + + def initialize(self, connection, driver_type): + self._register_rpc_consumers(connection) + + def consume_api(self, agent_api): + LOG.debug("FWaaS consume_api call occurred with %s", agent_api) + self.agent_api = agent_api + + def _register_rpc_consumers(self, connection): + #TODO(njohnston): Add RPC consumer connection loading here. + pass + + def start_rpc_listeners(self, host, conf): + self.endpoints = [self] + + self.conn = n_rpc.Connection() + self.conn.create_consumer( + fwaas_constants.FW_AGENT, self.endpoints, fanout=False) + return self.conn.consume_in_threads() + + def __init__(self, host, conf): + LOG.debug("Initializing firewall group agent") + self.agent_api = None + self.neutron_service_plugins = None + self.conf = conf + self.fwaas_enabled = cfg.CONF.fwaas.enabled + + self.start_rpc_listeners(host, conf) + # None means l3-agent has no information on the server + # configuration due to the lack of RPC support. + if self.neutron_service_plugins is not None: + fwaas_plugin_configured = (fwaas_constants.FIREWALL + in self.neutron_service_plugins) + if fwaas_plugin_configured and not self.fwaas_enabled: + msg = ("FWaaS plugin is configured in the server side, but " + "FWaaS is disabled in L3-agent.") + LOG.error(msg) + raise SystemExit(1) + self.fwaas_enabled = self.fwaas_enabled and fwaas_plugin_configured + + if self.fwaas_enabled: + # NOTE: Temp location for creating service and loading driver + self.fw_service = firewall_service.FirewallService() + self.fwaas_driver = self.fw_service.load_device_drivers() + + self.services_sync_needed = False + self.fwplugin_rpc = FWaaSL3PluginApi(fwaas_constants.FIREWALL_PLUGIN, + host) + super(FWaaSL3AgentExtension, self).__init__() + + @property + def _local_namespaces(self): + local_ns_list = ip_lib.list_network_namespaces() + return local_ns_list + + def _has_port_insertion_fields(self, firewall_group): + """The presence of the 'add-port-ids' key in the firewall group dict + shows we are using the current version of the plugin. If this key + is absent, we are in an upgrade and message is from an older + version of the plugin. + """ + return 'add-port-ids' in firewall_group + + def _get_firewall_group_ports(self, context, firewall_group, + to_delete=False, require_new_plugin=False): + """Returns in-namespace ports, either from firewall group dict if ports + update or from project routers otherwise if only policies update. + + NOTE: Vernacular move from "tenant" to "project" doesn't yet appear + as a key in router or firewall group objects. + """ + fwg_port_ids = [] + if self._has_port_insertion_fields(firewall_group): + if to_delete: + fwg_port_ids = firewall_group['del-port-ids'] + else: + fwg_port_ids = firewall_group['add-port-ids'] + if not require_new_plugin and not fwg_port_ids: + routers = self.agent_api.get_routers_in_project( + firewall_group['tenant_id']) + for router in routers: + if router.router['tenant_id'] == firewall_group['tenant_id']: + fwg_port_ids.extend([p['id'] for p in + router.internal_ports]) + + # Return in-namespace port objects. + return self._get_in_ns_ports(fwg_port_ids) + + def _get_in_ns_ports(self, port_ids): + """Get ports in namespace by their IDs. + + Returns port objects in the local namespace, along with their + router_info. + + :param port_ids: IDs of router ports (set, list or tuple) + """ + in_ns_ports = {} # This will be converted to a list later. + if port_ids and self.agent_api: + for port_id in port_ids: + # This fetched router_info is guaranteed to be in_namespace. + router_info = self.agent_api.get_router_hosting_port(port_id) + if router_info: + if router_info in in_ns_ports: + in_ns_ports[router_info].append(port_id) + else: + in_ns_ports[router_info] = [port_id] + return list(in_ns_ports.items()) + + def _invoke_driver_for_sync_from_plugin(self, ctx, ports, firewall_group): + """Call driver to sync firewall group. + + Calls the FWaaS driver's delete_firewall_group method if firewall + group has status of PENDING_DELETE; calls driver's + update_firewall_group method for all other statuses. Both of these + methods are idempotent. + + :param ctx: RPC context + :param ports: IDs of ports associated with a firewall group + (set, list or tuple) + :param firewall_group: Dictionary describing the firewall group object + + """ + port_list = self._get_in_ns_ports(ports) + if firewall_group['status'] == nl_constants.PENDING_DELETE: + try: + self.fwaas_driver.delete_firewall_group( + self.conf.agent_mode, port_list, firewall_group) + self.fwplugin_rpc.firewall_group_deleted( + ctx, firewall_group['id']) + except fw_ext.FirewallInternalDriverError: + msg = ("FWaaS driver error on %(status)s " + "for firewall group: %(fwg_id)s") + LOG.exception(msg, {'status': firewall_group['status'], + 'fwg_id': firewall_group['id']}) + self.fwplugin_rpc.set_firewall_group_status( + ctx, firewall_group['id'], nl_constants.ERROR) + else: # PENDING_UPDATE, PENDING_CREATE, ... + + # Prepare firewall group status to return to plugin; may be + # overwritten if call to driver fails. + if firewall_group['admin_state_up']: + status = nl_constants.ACTIVE + else: + status = nl_constants.DOWN + + # Call the driver. + try: + self.fwaas_driver.update_firewall_group( + self.conf.agent_mode, port_list, firewall_group) + except fw_ext.FirewallInternalDriverError: + msg = ("FWaaS driver error on %(status)s for firewall " + "group: %(fwg_id)s") + LOG.exception(msg, {'status': firewall_group['status'], + 'fwg_id': firewall_group['id']}) + status = nl_constants.ERROR + + # Notify the plugin of firewall group's status. + self.fwplugin_rpc.set_firewall_group_status( + ctx, firewall_group['id'], status) + + def _process_router_update(self, updated_router): + """If a new or existing router in the local namespace is updated, + queries the plugin to get the firewall groups for the project in + question and then sees if the router has any ports for any firewall + group that is configured for that project. If so, installs firewall + group rules on the requested ports on this router. + """ + LOG.debug("Process router update, router_id: %s tenant: %s.", + updated_router['id'], updated_router['tenant_id']) + router_id = updated_router['id'] + if not self.agent_api.is_router_in_namespace(router_id): + return + + # Get the firewall groups for the new router's project. + # NOTE: Vernacular move from "tenant" to "project" doesn't yet appear + # as a key in router or firewall group objects. + ctx = context.Context('', updated_router['tenant_id']) + fwg_list = self.fwplugin_rpc.get_firewall_groups_for_project(ctx) + + if nl_constants.INTERFACE_KEY not in updated_router: + return + + # Apply a firewall group, as requested, to ports on the new router. + all_router_ports = set( + p['id'] for p in updated_router[nl_constants.INTERFACE_KEY] + ) + processed_ports = set() + for firewall_group in fwg_list: + if not self._has_port_insertion_fields(firewall_group): + continue + + ports_to_process = (set(firewall_group['add-port-ids'] + + firewall_group['del-port-ids']) & + all_router_ports) + # ensure no port in router is associated with the firewall group + if not ports_to_process: + continue + # A port can have at most one firewall group. + port_ids_to_exclude = ports_to_process & processed_ports + if port_ids_to_exclude: + LOG.warning("Port(s) %s is associated with " + "more than one firewall group(s).", + port_ids_to_exclude) + ports_to_process -= port_ids_to_exclude + self._invoke_driver_for_sync_from_plugin( + ctx, ports_to_process, firewall_group) + processed_ports |= ports_to_process + + def add_router(self, context, new_router): + """Handles agent restart and router add. Fetches firewall groups from + plugin and updates driver. + """ + if not self.fwaas_enabled: + return + + try: + self._process_router_update(new_router) + except Exception: + LOG.exception("FWaaS router add RPC info call failed for %s", + new_router['id']) + self.services_sync_needed = True + + def update_router(self, context, updated_router): + """Handles agent restart and router update. Fetches firewall groups + from plugin and updates driver. + """ + if not self.fwaas_enabled: + return + + try: + self._process_router_update(updated_router) + except Exception: + #TODO(njohnston): This repr should be replaced. + LOG.exception( + "FWaaS router update RPC info call failed for %s", + repr(updated_router)) + self.services_sync_needed = True + + def delete_router(self, context, new_router): + """Handles router deletion. There is basically nothing to do for this + in the context of FWaaS with an IPTables driver; the namespace will + already have been deleted, taking the IPTables rules with it. + """ + #TODO(njohnston): When another firewall driver is implemented, look at + # expanding this out so that the driver can handle deletion calls. + pass + + def update_network(self, context, data): + pass + + def process_services_sync(self, ctx): + """Syncs with plugin and applies the sync data. + """ + + if not self.services_sync_needed or not self.fwaas_enabled: + return + + try: + # Fetch from the plugin the list of projects with firewall groups. + project_ids = \ + self.fwplugin_rpc.get_projects_with_firewall_groups(ctx) + LOG.debug("Projects with firewall groups: %s", + ', '.join(project_ids)) + for project_id in project_ids: + ctx = context.Context('', project_id) + fwg_list = \ + self.fwplugin_rpc.get_firewall_groups_for_project(ctx) + for firewall_group in fwg_list: + if firewall_group['status'] == nl_constants.PENDING_DELETE: + self.delete_firewall_group(ctx, firewall_group, + self.host) + # No need to apply sync data for ACTIVE firewall group. + elif firewall_group['status'] != nl_constants.ACTIVE: + self.update_firewall_group(ctx, firewall_group, + self.host) + self.services_sync_needed = False + except Exception: + LOG.exception("Failed FWaaS process services sync.") + self.services_sync_needed = True + + @log_helpers.log_method_call + def create_firewall_group(self, context, firewall_group, host): + """Handles RPC from plugin to create a firewall group. + """ + + # Get the in-namespace ports to which to add the firewall group. + ports_for_fwg = self._get_firewall_group_ports(context, firewall_group) + if not ports_for_fwg: + return + + LOG.debug("Create firewall group %(fwg_id)s on ports: %(ports)s", + {'fwg_id': firewall_group['id'], + 'ports': ', '.join([p for ri_ports in ports_for_fwg + for p in ri_ports[1]])}) + + # Set firewall group status; will be overwritten if call to driver + # fails. + if firewall_group['admin_state_up']: + status = nl_constants.ACTIVE + else: + status = nl_constants.DOWN + + # Call the driver. + try: + self.fwaas_driver.create_firewall_group(self.conf.agent_mode, + ports_for_fwg, + firewall_group) + except fw_ext.FirewallInternalDriverError: + msg = ("FWaaS driver error in create_firewall_group " + "for firewall group: %(fwg_id)s") + LOG.exception(msg, {'fwg_id': firewall_group['id']}) + status = nl_constants.ERROR + + # Send firewall group's status to plugin. + try: + self.fwplugin_rpc.set_firewall_group_status(context, + firewall_group['id'], status) + except Exception: + msg = ("FWaaS RPC failure in create_firewall_group " + "for firewall group: %(fwg_id)s") + LOG.exception(msg, {'fwg_id': firewall_group['id']}) + self.services_sync_needed = True + + @log_helpers.log_method_call + def update_firewall_group(self, context, firewall_group, host): + """Handles RPC from plugin to update a firewall group. + """ + + # Initialize firewall group status. + status = "" + + # Get the list of in-namespace ports from which to delete the firewall + # group. + del_fwg_ports = self._get_firewall_group_ports( + context, firewall_group, to_delete=True, require_new_plugin=True) + add_fwg_ports = self._get_firewall_group_ports(context, firewall_group) + + port_ids = (firewall_group.get('del-port-ids') + + firewall_group.get('add-port-ids')) + + if port_ids and not (del_fwg_ports or add_fwg_ports): + LOG.debug("All ports are not router port." + "No need to update firewall driver.") + return + + # Remove firewall group from ports if requested. + if del_fwg_ports: + fw_ports = [p for ri_port in del_fwg_ports for p in ri_port[1]] + LOG.debug("Update (delete) firewall group %(fwg_id)s on ports: " + "%(ports)s", + {'fwg_id': firewall_group['id'], + 'ports': ', '.join(fw_ports)}) + + # Set firewall group's status; will be overwritten if call to + # driver fails. + + if firewall_group['admin_state_up']: + status = nl_constants.ACTIVE + if firewall_group['last-port']: + status = nl_constants.INACTIVE + else: + status = nl_constants.DOWN + + # Call the driver. + try: + self.fwaas_driver.delete_firewall_group(self.conf.agent_mode, + del_fwg_ports, + firewall_group) + except fw_ext.FirewallInternalDriverError: + msg = ("FWaaS driver error in update_firewall_group " + "(add) for firewall group: %s") + LOG.exception(msg, firewall_group['id']) + status = nl_constants.ERROR + + # Handle the add router and/or rule, policy, firewall group attribute + # updates. + if status not in (nl_constants.ERROR, nl_constants.INACTIVE): + if add_fwg_ports: + fw_ports = [p for ri_port in add_fwg_ports + for p in ri_port[1]] + LOG.debug("Update (create) firewall group %(fwg_id)s on " + "ports: %(ports)s", + {'fwg_id': firewall_group['id'], + 'ports': ', '.join(fw_ports)}) + + # Set firewall group status, which will be overwritten if call + # to driver fails. + if firewall_group['admin_state_up']: + status = nl_constants.ACTIVE + else: + status = nl_constants.DOWN + + # Call the driver. + try: + self.fwaas_driver.update_firewall_group( + self.conf.agent_mode, add_fwg_ports, + firewall_group) + except fw_ext.FirewallInternalDriverError: + msg = ("FWaaS driver error in update_firewall_group " + "for firewall group: %s") + LOG.exception(msg, firewall_group['id']) + status = nl_constants.ERROR + elif not status: + # if status not set by now, set it to INACTIVE + status = nl_constants.INACTIVE + + # Return status to plugin. + try: + self.fwplugin_rpc.set_firewall_group_status(context, + firewall_group['id'], status) + except Exception: + LOG.exception("FWaaS RPC failure in update_firewall_group " + "for firewall group: %s", firewall_group['id']) + self.services_sync_needed = True + + @log_helpers.log_method_call + def delete_firewall_group(self, context, firewall_group, host): + """Handles RPC from plugin to delete a firewall group. + """ + + ports_for_fwg = self._get_firewall_group_ports(context, firewall_group, + to_delete=True) + + if not ports_for_fwg: + return + + fw_ports = [p for ri_ports in ports_for_fwg for p in ri_ports[1]] + LOG.debug("Delete firewall group %(fwg_id)s on ports: %(ports)s", + {'fwg_id': firewall_group['id'], + 'ports': ', '.join(fw_ports)}) + + # Set the firewall group's status to return to plugin; status may be + # overwritten if call to driver fails. + if firewall_group['admin_state_up']: + status = nl_constants.ACTIVE + else: + status = nl_constants.DOWN + try: + self.fwaas_driver.delete_firewall_group(self.conf.agent_mode, + ports_for_fwg, + firewall_group) + # Call the driver. + except fw_ext.FirewallInternalDriverError: + LOG.exception("FWaaS driver error in delete_firewall_group " + "for firewall group: %s", firewall_group['id']) + status = nl_constants.ERROR + + # Notify plugin of deletion or return firewall group's status to + # plugin, as appropriate. + try: + if status in [nl_constants.ACTIVE, nl_constants.DOWN]: + self.fwplugin_rpc.firewall_group_deleted(context, + firewall_group['id']) + else: + self.fwplugin_rpc.set_firewall_group_status(context, + firewall_group['id'], status) + except Exception: + LOG.exception("FWaaS RPC failure in delete_firewall_group " + "for firewall group: %s", firewall_group['id']) + self.services_sync_needed = True + + def ha_state_change(self, context, data): + pass + + +class L3WithFWaaS(FWaaSL3AgentExtension): + + def __init__(self, conf=None): + if conf: + self.conf = conf + else: + self.conf = cfg.CONF + super(L3WithFWaaS, self).__init__(host=self.conf.host, conf=self.conf) diff --git a/neutron_fwaas/services/firewall/service_drivers/driver_api.py b/neutron_fwaas/services/firewall/service_drivers/driver_api.py new file mode 100644 index 000000000..eae09177f --- /dev/null +++ b/neutron_fwaas/services/firewall/service_drivers/driver_api.py @@ -0,0 +1,532 @@ +# Copyright (c) 2017 Juniper Networks, Inc. All rights reserved. +# 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. + +import abc +import copy + +import six + +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib import constants as nl_constants +from neutron_lib.plugins import directory +from oslo_log import log as logging + +from neutron_fwaas.common import fwaas_constants as const +from neutron_fwaas.db.firewall.v2 import firewall_db_v2 + + +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class FirewallDriver(object): + """Firewall v2 interface for driver + + That driver interface does not persist Firewall v2 data in any database. + The driver needs to do it by itself. + """ + + def __init__(self, service_plugin): + self.service_plugin = service_plugin + + @property + def _core_plugin(self): + return directory.get_plugin() + + def is_supported_l2_port(self, port): + return False + + def is_supported_l3_port(self, port): + return False + + # Firewall Group + @abc.abstractmethod + def create_firewall_group(self, context, firewall_group): + pass + + @abc.abstractmethod + def delete_firewall_group(self, context, id): + pass + + @abc.abstractmethod + def get_firewall_group(self, context, id, fields=None): + pass + + @abc.abstractmethod + def get_firewall_groups(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def update_firewall_group(self, context, id, firewall_group): + pass + + # Firewall Policy + @abc.abstractmethod + def create_firewall_policy(self, context, firewall_policy): + pass + + @abc.abstractmethod + def delete_firewall_policy(self, context, id): + pass + + @abc.abstractmethod + def get_firewall_policy(self, context, id, fields=None): + pass + + @abc.abstractmethod + def get_firewall_policies(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def update_firewall_policy(self, context, id, firewall_policy): + pass + + # Firewall Rule + @abc.abstractmethod + def create_firewall_rule(self, context, firewall_rule): + pass + + @abc.abstractmethod + def delete_firewall_rule(self, context, id): + pass + + @abc.abstractmethod + def get_firewall_rule(self, context, id, fields=None): + pass + + @abc.abstractmethod + def get_firewall_rules(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def update_firewall_rule(self, context, id, firewall_rule): + pass + + @abc.abstractmethod + def insert_rule(self, context, policy_id, rule_info): + pass + + @abc.abstractmethod + def remove_rule(self, context, policy_id, rule_info): + pass + + +@six.add_metaclass(abc.ABCMeta) +class FirewallDriverDBMixin(FirewallDriver): + """FirewallDriverDB mixin to provision the database on behalf of the driver + + That driver interface persists Firewall data in its database and forwards + the result to pre and post commit methods. + """ + + def __init__(self, *args, **kwargs): + super(FirewallDriverDBMixin, self).__init__(*args, **kwargs) + self.firewall_db = firewall_db_v2.FirewallPluginDb() + + @staticmethod + def _update_resource_status(context, resource_type, resource_dict): + with context.session.begin(subtransactions=True): + context.session.query(resource_type).\ + filter_by(id=resource_dict['id']).\ + update({'status': resource_dict['status']}) + + # Firewall Group + def create_firewall_group(self, context, firewall_group): + request_body = firewall_group + with context.session.begin(subtransactions=True): + firewall_group = self.firewall_db.create_firewall_group( + context, firewall_group) + self.create_firewall_group_precommit(context, firewall_group) + self._update_resource_status(context, firewall_db_v2.FirewallGroup, + firewall_group) + self.create_firewall_group_postcommit(context, firewall_group) + + payload = events.DBEventPayload(context=context, + resource_id=firewall_group['id'], + request_body=request_body, + states=(firewall_group,)) + registry.publish( + const.FIREWALL_GROUP, events.AFTER_CREATE, self, payload=payload) + return firewall_group + + @abc.abstractmethod + def create_firewall_group_precommit(self, context, firewall_group): + pass + + @abc.abstractmethod + def create_firewall_group_postcommit(self, context, firewall_group): + pass + + def delete_firewall_group(self, context, id): + firewall_group = self.firewall_db.get_firewall_group(context, id) + if firewall_group['status'] == nl_constants.PENDING_DELETE: + firewall_group['status'] = nl_constants.ERROR + self.delete_firewall_group_precommit(context, firewall_group) + if firewall_group['status'] != nl_constants.PENDING_DELETE: + # lets driver deleting firewall group later + self.firewall_db.delete_firewall_group(context, id) + self.delete_firewall_group_postcommit(context, firewall_group) + + payload = events.DBEventPayload(context=context, + resource_id=id, + states=(firewall_group,)) + registry.publish( + const.FIREWALL_GROUP, events.AFTER_DELETE, self, payload=payload) + + @abc.abstractmethod + def delete_firewall_group_precommit(self, context, firewall_group): + pass + + @abc.abstractmethod + def delete_firewall_group_postcommit(self, context, firewall_group): + pass + + def get_firewall_group(self, context, id, fields=None): + return self.firewall_db.get_firewall_group(context, id, fields=fields) + + def get_firewall_groups(self, context, filters=None, fields=None): + return self.firewall_db.get_firewall_groups(context, filters, fields) + + def update_firewall_group(self, context, id, firewall_group_delta): + old_firewall_group = self.firewall_db.get_firewall_group(context, id) + new_firewall_group = copy.deepcopy(old_firewall_group) + new_firewall_group.update(firewall_group_delta) + self.update_firewall_group_precommit(context, old_firewall_group, + new_firewall_group) + firewall_group_delta['status'] = new_firewall_group['status'] + firewall_group = self.firewall_db.update_firewall_group( + context, id, firewall_group_delta) + self.update_firewall_group_postcommit(context, old_firewall_group, + firewall_group) + + payload = events.DBEventPayload(context=context, + resource_id=id, + states=(old_firewall_group, + new_firewall_group)) + registry.publish( + const.FIREWALL_GROUP, events.AFTER_UPDATE, self, payload=payload) + + return firewall_group + + @abc.abstractmethod + def update_firewall_group_precommit(self, context, old_firewall_group, + new_firewall_group): + pass + + @abc.abstractmethod + def update_firewall_group_postcommit(self, context, old_firewall_group, + new_firewall_group): + pass + + # Firewall Policy + def create_firewall_policy(self, context, firewall_policy): + request_body = firewall_policy + with context.session.begin(subtransactions=True): + firewall_policy = self.firewall_db.create_firewall_policy( + context, firewall_policy) + self.create_firewall_policy_precommit(context, firewall_policy) + self.create_firewall_policy_postcommit(context, firewall_policy) + + payload = events.DBEventPayload(context=context, + resource_id=firewall_policy['id'], + request_body=request_body, + states=(firewall_policy,)) + registry.publish( + const.FIREWALL_POLICY, events.AFTER_CREATE, self, payload=payload) + return firewall_policy + + @abc.abstractmethod + def create_firewall_policy_precommit(self, context, firewall_policy): + pass + + @abc.abstractmethod + def create_firewall_policy_postcommit(self, context, firewall_policy): + pass + + def delete_firewall_policy(self, context, id): + firewall_policy = self.firewall_db.get_firewall_policy(context, id) + self.delete_firewall_policy_precommit(context, firewall_policy) + self.firewall_db.delete_firewall_policy(context, id) + self.delete_firewall_policy_postcommit(context, firewall_policy) + + payload = events.DBEventPayload(context=context, + resource_id=id, + states=(firewall_policy,)) + registry.publish( + const.FIREWALL_POLICY, events.AFTER_UPDATE, self, payload=payload) + + @abc.abstractmethod + def delete_firewall_policy_precommit(self, context, firewall_policy): + pass + + @abc.abstractmethod + def delete_firewall_policy_postcommit(self, context, firewall_policy): + pass + + def get_firewall_policy(self, context, id, fields=None): + return self.firewall_db.get_firewall_policy(context, id, fields) + + def get_firewall_policies(self, context, filters=None, fields=None): + return self.firewall_db.get_firewall_policies(context, filters, fields) + + def update_firewall_policy(self, context, id, firewall_policy_delta): + old_firewall_policy = self.firewall_db.get_firewall_policy(context, id) + new_firewall_policy = copy.deepcopy(old_firewall_policy) + new_firewall_policy.update(firewall_policy_delta) + self.update_firewall_policy_precommit(context, old_firewall_policy, + new_firewall_policy) + firewall_policy = self.firewall_db.update_firewall_policy( + context, id, firewall_policy_delta) + self.update_firewall_policy_postcommit(context, old_firewall_policy, + firewall_policy) + + payload = events.DBEventPayload(context=context, + resource_id=id, + states=(firewall_policy,)) + registry.publish( + const.FIREWALL_POLICY, events.AFTER_UPDATE, self, payload=payload) + return firewall_policy + + @abc.abstractmethod + def update_firewall_policy_precommit(self, context, old_firewall_policy, + new_firewall_policy): + pass + + @abc.abstractmethod + def update_firewall_policy_postcommit(self, context, old_firewall_policy, + new_firewall_policy): + pass + + # Firewall Rule + def create_firewall_rule(self, context, firewall_rule): + request_body = firewall_rule + with context.session.begin(subtransactions=True): + firewall_rule = self.firewall_db.create_firewall_rule( + context, firewall_rule) + self.create_firewall_rule_precommit(context, firewall_rule) + self.create_firewall_rule_postcommit(context, firewall_rule) + + payload = events.DBEventPayload(context=context, + resource_id=firewall_rule['id'], + request_body=request_body, + states=(firewall_rule,)) + registry.publish( + const.FIREWALL_RULE, events.AFTER_CREATE, self, payload=payload) + return firewall_rule + + @abc.abstractmethod + def create_firewall_rule_precommit(self, context, firewall_rule): + pass + + @abc.abstractmethod + def create_firewall_rule_postcommit(self, context, firewall_rule): + pass + + def delete_firewall_rule(self, context, id): + firewall_rule = self.firewall_db.get_firewall_rule(context, id) + self.delete_firewall_rule_precommit(context, firewall_rule) + self.firewall_db.delete_firewall_rule(context, id) + self.delete_firewall_rule_postcommit(context, firewall_rule) + + payload = events.DBEventPayload(context=context, + resource_id=id, + states=(firewall_rule,)) + registry.publish( + const.FIREWALL_RULE, events.AFTER_DELETE, self, payload=payload) + + @abc.abstractmethod + def delete_firewall_rule_precommit(self, context, firewall_rule): + pass + + @abc.abstractmethod + def delete_firewall_rule_postcommit(self, context, firewall_rule): + pass + + def get_firewall_rule(self, context, id, fields=None): + return self.firewall_db.get_firewall_rule(context, id, fields) + + def get_firewall_rules(self, context, filters=None, fields=None): + return self.firewall_db.get_firewall_rules(context, filters, fields) + + def update_firewall_rule(self, context, id, firewall_rule_delta): + old_firewall_rule = self.firewall_db.get_firewall_rule(context, id) + new_firewall_rule = copy.deepcopy(old_firewall_rule) + new_firewall_rule.update(firewall_rule_delta) + self.update_firewall_rule_precommit(context, old_firewall_rule, + new_firewall_rule) + firewall_rule = self.firewall_db.update_firewall_rule( + context, id, firewall_rule_delta) + self.update_firewall_rule_postcommit(context, old_firewall_rule, + firewall_rule) + + payload = events.DBEventPayload(context=context, + resource_id=id, + states=(firewall_rule,)) + registry.publish( + const.FIREWALL_RULE, events.AFTER_UPDATE, self, payload=payload) + + return firewall_rule + + @abc.abstractmethod + def update_firewall_rule_precommit(self, context, old_firewall_rule, + new_firewall_rule): + pass + + @abc.abstractmethod + def update_firewall_rule_postcommit(self, context, old_firewall_rule, + new_firewall_rule): + pass + + def insert_rule(self, context, policy_id, rule_info): + self.insert_rule_precommit(context, policy_id, rule_info) + firewall_policy = self.firewall_db.insert_rule(context, policy_id, + rule_info) + self.insert_rule_postcommit(context, policy_id, rule_info) + payload = events.DBEventPayload(context=context, + resource_id=policy_id, + states=(firewall_policy,)) + registry.publish( + const.FIREWALL_POLICY, events.AFTER_UPDATE, self, payload=payload) + + return firewall_policy + + @abc.abstractmethod + def insert_rule_precommit(self, context, policy_id, rule_info): + pass + + @abc.abstractmethod + def insert_rule_postcommit(self, context, policy_id, rule_info): + pass + + def remove_rule(self, context, policy_id, rule_info): + self.remove_rule_precommit(context, policy_id, rule_info) + firewall_policy = self.firewall_db.remove_rule(context, policy_id, + rule_info) + self.remove_rule_postcommit(context, policy_id, rule_info) + payload = events.DBEventPayload(context=context, + resource_id=policy_id, + states=(firewall_policy,)) + + registry.publish( + const.FIREWALL_POLICY, events.AFTER_UPDATE, self, payload=payload) + return firewall_policy + + @abc.abstractmethod + def remove_rule_precommit(self, context, policy_id, rule_info): + pass + + @abc.abstractmethod + def remove_rule_postcommit(self, context, policy_id, rule_info): + pass + + +class FirewallDriverDB(FirewallDriverDBMixin): + """FirewallDriverDBMixin interface for driver with database. + + Each firewall backend driver that needs a database persistency should + inherit from this driver. + It can overload needed methods from the following pre/postcommit methods. + Any exception raised during a precommit method will result in not having + related records in the databases. + """ + + #Firewal Group + def create_firewall_group_precommit(self, context, firewall_group): + pass + + def create_firewall_group_postcommit(self, context, firewall_group): + pass + + def update_firewall_group_precommit(self, context, old_firewall_group, + new_firewall_group): + pass + + def update_firewall_group_postcommit(self, context, old_firewall_group, + new_firewall_group): + pass + + def delete_firewall_group_precommit(self, context, firewall_group): + pass + + def delete_firewall_group_postcommit(self, context, firewall_group): + pass + + #Firewall Policy + def create_firewall_policy_precommit(self, context, firewall_policy): + pass + + def create_firewall_policy_postcommit(self, context, firewall_policy): + pass + + def update_firewall_policy_precommit(self, context, old_firewall_policy, + new_firewall_policy): + pass + + def update_firewall_policy_postcommit(self, context, old_firewall_policy, + new_firewall_policy): + pass + + def delete_firewall_policy_precommit(self, context, firewall_policy): + pass + + def delete_firewall_policy_postcommit(self, context, firewall_policy): + pass + + #Firewall Rule + def create_firewall_rule_precommit(self, context, firewall_rule): + pass + + def create_firewall_rule_postcommit(self, context, firewall_rule): + pass + + def update_firewall_rule_precommit(self, context, old_firewall_rule, + new_firewall_rule): + pass + + def update_firewall_rule_postcommit(self, context, old_firewall_rule, + new_firewall_rule): + pass + + def delete_firewall_rule_precommit(self, context, firewall_rule): + pass + + def delete_firewall_rule_postcommit(self, context, firewall_rule): + pass + + def insert_rule_precommit(self, context, policy_id, rule_info): + pass + + def insert_rule_postcommit(self, context, policy_id, rule_info): + pass + + def remove_rule_precommit(self, context, policy_id, rule_info): + pass + + def remove_rule_postcommit(self, context, policy_id, rule_info): + pass + + +@six.add_metaclass(abc.ABCMeta) +class FirewallDriverRPCMixin(object): + """FirewallAgent interface for driver with rpc callback listener. + + Each firewall backend driver that needs a rpc callback listener should + inherit from this driver. + """ + + @abc.abstractmethod + def start_rpc_listener(self): + pass diff --git a/neutron_fwaas/services/logapi/__init__.py b/neutron_fwaas/services/logapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/logapi/agents/__init__.py b/neutron_fwaas/services/logapi/agents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/logapi/agents/drivers/__init__.py b/neutron_fwaas/services/logapi/agents/drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/logapi/agents/drivers/iptables/__init__.py b/neutron_fwaas/services/logapi/agents/drivers/iptables/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/logapi/agents/drivers/iptables/driver.py b/neutron_fwaas/services/logapi/agents/drivers/iptables/driver.py new file mode 100644 index 000000000..417168358 --- /dev/null +++ b/neutron_fwaas/services/logapi/agents/drivers/iptables/driver.py @@ -0,0 +1,71 @@ +# Copyright (c) 2018 Fujitsu Limited. +# 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. + +from neutron.services.logapi.drivers import base +from neutron.services.logapi.drivers import manager +from neutron_lib.callbacks import resources +from neutron_lib.services.logapi import constants as log_const +from oslo_log import log as logging +from oslo_utils import importutils + +from neutron_fwaas.common import fwaas_constants +from neutron_fwaas.services.logapi.common import fwg_callback +from neutron_fwaas.services.logapi.common import port_callback +from neutron_fwaas.services.logapi import constants as fw_const +from neutron_fwaas.services.logapi.rpc import log_server as rpc_server + +LOG = logging.getLogger(__name__) + +DRIVER = None + +SUPPORTED_LOGGING_TYPES = [fw_const.FIREWALL_GROUP] + + +class IptablesLoggingDriver(base.DriverBase): + + @staticmethod + def create(): + return IptablesLoggingDriver( + name='iptables', + vif_types=[], + vnic_types=[], + supported_logging_types=SUPPORTED_LOGGING_TYPES, + requires_rpc=True) + + +def register(): + """Register iptables-based logging driver for FWaaS.""" + + global DRIVER + if not DRIVER: + DRIVER = IptablesLoggingDriver.create() + # Register RPC methods + if DRIVER.requires_rpc: + rpc_methods = [ + {resources.PORT: rpc_server.get_fwg_log_info_for_port}, + {log_const.LOG_RESOURCE: rpc_server. + get_fwg_log_info_for_log_resources} + ] + DRIVER.register_rpc_methods(fw_const.FIREWALL_GROUP, rpc_methods) + + # Trigger fwg validator + importutils.import_module('neutron_fwaas.services.logapi.fwg_validate') + # Register resource callback handler + manager.register( + fwaas_constants.FIREWALL_GROUP, fwg_callback.FirewallGroupCallBack) + # Register resource callback handler for Neutron ports + manager.register(resources.PORT, port_callback.NeutronPortCallBack) + + LOG.debug('FWaaS L3 Logging driver based iptables registered') diff --git a/neutron_fwaas/services/logapi/agents/drivers/iptables/log.py b/neutron_fwaas/services/logapi/agents/drivers/iptables/log.py new file mode 100644 index 000000000..0ceeba409 --- /dev/null +++ b/neutron_fwaas/services/logapi/agents/drivers/iptables/log.py @@ -0,0 +1,521 @@ +# Copyright (c) 2018 Fujitsu Limited. +# 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. + +from collections import defaultdict +import signal +import uuid + +from neutron.agent.linux import utils +from neutron.services.logapi.agent import log_extension as log_ext +from neutron_lib import constants +from neutron_lib.services.logapi import constants as log_const +from oslo_config import cfg +from oslo_log import formatters +from oslo_log import handlers +from oslo_log import log as logging + +from neutron_fwaas.privileged.netfilter_log import libnetfilter_log as libnflog + +LOG = logging.getLogger(__name__) + +UINT64_BITMASK = (1 << 64) - 1 + +MAX_INTF_NAME_LEN = 14 +INTERNAL_DEV_PREFIX = 'qr-' +SNAT_INT_DEV_PREFIX = 'sg-' +ROUTER_2_FIP_DEV_PREFIX = 'rfp-' + +IPTABLES_DIRECTION_DEVICE = { + constants.INGRESS_DIRECTION: 'i', + constants.EGRESS_DIRECTION: 'o' +} + + +def setup_logging(): + + log_file = cfg.CONF.network_log.local_output_log_base + if log_file: + from logging import handlers as watch_handler + log_file_handler = watch_handler.WatchedFileHandler(log_file) + log_file_handler.setLevel( + logging.DEBUG if cfg.CONF.debug else logging.INFO) + LOG.logger.addHandler(log_file_handler) + log_file_handler.setFormatter( + formatters.ContextFormatter( + fmt=cfg.CONF.logging_default_format_string, + datefmt=cfg.CONF.log_date_format)) + elif cfg.CONF.use_journal: + journal_handler = handlers.OSJournalHandler() + LOG.logger.addHandler(journal_handler) + else: + syslog_handler = handlers.OSSysLogHandler() + LOG.logger.addHandler(syslog_handler) + + +class LogPrefix(object): + """LogPrefix could be used as prefix in NFLOG rules + Each of a couple (port_id, event) has its own LogPrefix object + """ + + def __init__(self, port_id, event, project_id): + self.id = self._generate_prefix_id() + self.port_id = port_id + self.action = event + # A list of log objects that referenced to this prefix + self.log_object_refs = set() + self.project_id = project_id + + def __eq__(self, other): + return (self.id == other.id and + self.action == other.action and + self.port_id == other.port_id) + + def __hash__(self): + return hash(self.id) + + def _generate_prefix_id(self): + return uuid.uuid4().int & UINT64_BITMASK + + def add_log_obj_ref(self, log_id): + self.log_object_refs.add(log_id) + + def remove_log_obj_ref(self, log_id): + self.log_object_refs.discard(log_id) + + @property + def is_empty(self): + return not self.log_object_refs + + +class FWGPortLog(object): + """A firewall group port log per log_object""" + + def __init__(self, port_id, log_info): + self.port_id = port_id + self.log_id = log_info['id'] + self.project_id = log_info['project_id'] + self.event = log_info['event'] + + +class IptablesLoggingDriver(log_ext.LoggingDriver): + + SUPPORTED_LOGGING_TYPES = ['firewall_group'] + + def __init__(self, agent_api): + self.agent_api = agent_api + self.conf = cfg.CONF + self.rate_limit = self.conf.network_log.rate_limit + if self.rate_limit: + self.burst_limit = self.conf.network_log.burst_limit + self.ipt_mgr_list = defaultdict(dict) + # A list of fwg port logs that are being logged + self.fwg_port_logs = defaultdict(set) + # A list of prefixes that are being used in iptables + self.prefixes_table = {} + self.cleanup_table = defaultdict(set) + # Handle NFLOG processing + self.nflog_proc_map = {} + # A list of unused ports + self.unused_port_ids = set() + + def initialize(self, resource_rpc, **kwargs): + self.resource_rpc = resource_rpc + setup_logging() + self.log_app = libnflog.NFLogApp() + self.log_app.register_packet_handler(self.log_packet) + self.log_app.start() + + def log_packet(self, ev): + prefix = ev['prefix'] + pkt = ev['msg'] + prefix_entry = self._get_prefix_by_id(prefix) + if prefix_entry: + logs_id = [str(id) for id in prefix_entry.log_object_refs] + LOG.info("action=%s, project_id=%s, log_resource_ids=%s, port=%s, " + "pkt=%s", prefix_entry.action, + prefix_entry.project_id, logs_id, + prefix_entry.port_id, pkt) + else: + LOG.warning("Unknown cookie packet_in pkt=%s", pkt) + return 0 + + def _get_prefix(self, port_id, action): + if port_id in self.prefixes_table: + for prefix in self.prefixes_table[port_id]: + if prefix.action == action: + return prefix + return None + + def _get_prefix_by_id(self, prefix_id): + for port, prefixes in self.prefixes_table.items(): + for prefix in prefixes: + if str(prefix.id) == str(prefix_id): + return prefix + return None + + def _add_to_cleanup(self, port_id, prefix_id): + if port_id not in self.cleanup_table: + self.cleanup_table[port_id] = set() + self.cleanup_table[port_id].add(prefix_id) + + def _add_to_prefixes_table(self, port_id, prefix): + if port_id not in self.prefixes_table: + self.prefixes_table[port_id] = [] + self.prefixes_table[port_id].append(prefix) + + def _cleanup_nflog_process(self, router_info): + LOG.debug("Delete router_info %s", router_info) + if router_info in self.nflog_proc_map: + pid = self.nflog_proc_map[router_info] + try: + # A process started by a root helper will be running as + # root and need to be killed via the same helper. + LOG.debug('Trying to kill NFLOG process %d', pid) + utils.kill_process(pid, signal.SIGKILL, run_as_root=True) + del self.nflog_proc_map[router_info] + except Exception: + LOG.exception( + 'An error occurred while killing process %d', pid) + + def _cleanup_prefix_by_router(self, router_id): + + ipt_mgr_per_port = set() + for port_id in self.ipt_mgr_list[router_id]: + ipt_mgr = self.ipt_mgr_list[router_id][port_id] + ipt_mgr_per_port.add(ipt_mgr) + # Cleanup prefix + if port_id in self.prefixes_table: + for prefix in self.prefixes_table[port_id]: + self._add_to_cleanup(port_id, prefix.id) + del self.prefixes_table[port_id] + self.unused_port_ids.add(port_id) + return ipt_mgr_per_port + + def _cleanup_unused_ipt_mgrs(self): + + need_cleanup = set() + for port_id in self.unused_port_ids: + for router_id in self.ipt_mgr_list: + if port_id in self.ipt_mgr_list[router_id]: + del self.ipt_mgr_list[router_id][port_id] + if not self.ipt_mgr_list[router_id]: + need_cleanup.add(router_id) + + for router_id in need_cleanup: + del self.ipt_mgr_list[router_id] + + self.unused_port_ids.clear() + + def start_logging(self, context, **kwargs): + LOG.debug("Start logging: %s", str(kwargs)) + + for resource_type in self.SUPPORTED_LOGGING_TYPES: + router_info = kwargs.get('router_info') + if router_info: + # Handle router updated or L3 agent restart + router_id = router_info.router_id + internal_ports = router_info.internal_ports + self._create_firewall_group_log(context, resource_type, + ports=internal_ports, + router_id=router_id) + + # Start libnetfilter_log after router starting up + pid = libnflog.run_nflog(router_info.ns_name) + LOG.debug("NFLOG process ID %s for router %s has started", + pid, router_info.router_id) + self.nflog_proc_map[router_id] = pid + else: + # Handle the log request + self._create_firewall_group_log(context, resource_type, + **kwargs) + + def stop_logging(self, context, **kwargs): + LOG.debug("Stop logging: %s", str(kwargs)) + + # Delete router + router_info = kwargs.get('router_info') + if router_info: + self._cleanup_nflog_process(router_info) + + if kwargs.get('log_resources'): + # Handle incoming log request + self._delete_firewall_group_log(context, **kwargs) + + def _create_firewall_group_log(self, context, resource_type, **kwargs): + ports = kwargs.get('ports') + log_resources = kwargs.get('log_resources') + applied_ipt_mgrs = set() + logs_info = [] + + port_ids = [] + # Get log objects from database via RPC + if ports: + port_ids = [port['id'] for port in ports] + logs_info = self.resource_rpc. \ + get_sg_log_info_for_port(context, resource_type, + port_id=port_ids) + elif log_resources: + logs_info = self.resource_rpc.\ + get_sg_log_info_for_log_resources(context, resource_type, + log_resources=log_resources) + # Handle logs_info + for log_info in logs_info: + log_id = log_info['id'] + old_fwg_port_logs = self.fwg_port_logs.get(log_id, []) + new_ports_log = log_info.get('ports_log') + self.fwg_port_logs[log_id] = set() + for port in new_ports_log: + self._add_fwg_port_log(log_id, port, log_info) + + for port in old_fwg_port_logs: + if port.port_id not in new_ports_log: + # Remove port not bound by log_id + self._cleanup_prefixes_table(port.port_id, log_id) + + for fwg_port_log in self.fwg_port_logs[log_id]: + self._setup_chains(applied_ipt_mgrs, fwg_port_log) + + router_id = kwargs.get("router_id") + if router_id: + if not port_ids: + ipt_mgrs = self._cleanup_prefix_by_router(router_id) + applied_ipt_mgrs.update(ipt_mgrs) + + for port_id in port_ids: + try: + ipt_mgr = self.ipt_mgr_list[router_id][port_id] + applied_ipt_mgrs.add(ipt_mgr) + except KeyError: + pass + + # Clean up NFLOG rules + self._cleanup_nflog_rules(applied_ipt_mgrs) + + # Apply NFLOG rules into iptables managers + for ipt_mgr in applied_ipt_mgrs: + LOG.debug('Apply NFLOG rules in namespace %s', ipt_mgr.namespace) + ipt_mgr.defer_apply_off() + + # Clean up unused iptables managers from ports + self._cleanup_unused_ipt_mgrs() + + def _cleanup_prefixes_table(self, port_id, log_id): + + # Each a port has at most 2 prefix + for index in [1, 0]: + try: + prefix = self.prefixes_table[port_id][index] + prefix.remove_log_obj_ref(log_id) + if prefix.is_empty: + self._add_to_cleanup(port_id, prefix.id) + self.prefixes_table[port_id].remove(prefix) + except Exception: + pass + + if port_id in self.prefixes_table: + if not self.prefixes_table[port_id]: + del self.prefixes_table[port_id] + self.unused_port_ids.add(port_id) + + def _cleanup_nflog_rules(self, applied_ipt_mgrs): + for port_id, prefix_ids in self.cleanup_table.items(): + ipt_mgr = self._get_ipt_mgr_by_port(port_id) + for prefix_id in prefix_ids: + self._clear_rules_from_tag_v4v6(ipt_mgr, tag=prefix_id) + applied_ipt_mgrs.add(ipt_mgr) + self.cleanup_table.clear() + + def _delete_firewall_group_log(self, context, **kwargs): + log_resources = kwargs.get('log_resources') + applied_ipt_mgrs = set() + + for log_resource in log_resources: + log_id = log_resource.get('id') + fwg_port_logs = self.fwg_port_logs[log_id] + for port in fwg_port_logs: + self._cleanup_prefixes_table(port.port_id, log_id) + del self.fwg_port_logs[log_id] + + # Clean NFLOG rules: + self._cleanup_nflog_rules(applied_ipt_mgrs) + + # Apply NFLOG rules into iptables managers + for ipt_mgr in applied_ipt_mgrs: + ipt_mgr.defer_apply_off() + + # Clean up unused iptables managers + self._cleanup_unused_ipt_mgrs() + + def _get_if_prefix(self, agent_mode, router): + """Get the if prefix from router""" + if not router.router.get('distributed'): + return INTERNAL_DEV_PREFIX + + if agent_mode == 'dvr_snat': + return SNAT_INT_DEV_PREFIX + + if router.rtr_fip_connect: + return ROUTER_2_FIP_DEV_PREFIX + + def _get_intf_name(self, port_id): + agent_mode = self.conf.agent_mode + router = self.agent_api.get_router_hosting_port(port_id) + if_prefix = self._get_if_prefix(agent_mode, router) + return (if_prefix + port_id)[:constants.LINUX_DEV_LEN] + + def _get_ipt_mgr_by_port(self, port_id): + + router = self.agent_api.get_router_hosting_port(port_id) + if router: + try: + ipt_mgr = self.ipt_mgr_list[router.router_id][port_id] + return ipt_mgr + except KeyError: + pass + + ipt_mgr = router.iptables_manager + self.ipt_mgr_list[router.router_id][port_id] = ipt_mgr + return ipt_mgr + + for router_id in self.ipt_mgr_list: + if port_id in self.ipt_mgr_list[router_id]: + return self.ipt_mgr_list[router_id][port_id] + + def _setup_chains(self, applied_ipt_mgrs, fwg_port_log): + # Add NFLOG rules by log event + event = fwg_port_log.event + if event in [log_const.ACCEPT_EVENT, log_const.ALL_EVENT]: + self._add_nflog_rules_accepted(applied_ipt_mgrs, fwg_port_log) + if event in [log_const.DROP_EVENT, log_const.ALL_EVENT]: + self._add_log_rules_dropped(applied_ipt_mgrs, fwg_port_log) + + def _add_nflog_rules_accepted(self, applied_ipt_mgrs, fwg_port_log): + """Add NFLOG rules to the accepted chain into iptables""" + action = log_const.ACCEPT_EVENT + port_id = fwg_port_log.port_id + prefix = self._get_prefix(port_id, action) + if not prefix: + # Generate a new prefix from port and action + project_id = fwg_port_log.project_id + prefix = LogPrefix(port_id, action, project_id) + self._add_to_prefixes_table(port_id, prefix) + + # Get iptables manager from router port + ipt_mgr = self._get_ipt_mgr_by_port(port_id) + if ipt_mgr: + applied_ipt_mgrs.add(ipt_mgr) + + device = self._get_intf_name(port_id) + + # Add the NFLOG rules to the dropped chain into iptables + ipv4_rules, ipv6_rules = \ + self._generate_nflog_rules_v4v6(device, prefix=prefix.id) + self._add_rules_to_chain_v4v6(ipt_mgr, + 'accepted', ipv4_rules, ipv6_rules, + wrap=True, top=True, tag=prefix.id) + + prefix.add_log_obj_ref(fwg_port_log.log_id) + + def _add_log_rules_dropped(self, applied_ipt_mgrs, fwg_port_log): + """Add NFLOG rules to the dropped chain into iptables""" + + action = log_const.DROP_EVENT + port_id = fwg_port_log.port_id + prefix = self._get_prefix(port_id, action) + if not prefix: + # Generate a new prefix from port and action + project_id = fwg_port_log.project_id + prefix = LogPrefix(port_id, action, project_id) + self._add_to_prefixes_table(port_id, prefix) + device = self._get_intf_name(port_id) + + # Get iptables manager from router port + ipt_mgr = self._get_ipt_mgr_by_port(port_id) + if ipt_mgr: + applied_ipt_mgrs.add(ipt_mgr) + + # Add the NFLOG rules to the dropped chain into iptables + ipv4_rules, ipv6_rules = \ + self._generate_nflog_rules_v4v6(device, prefix=prefix.id) + self._add_rules_to_chain_v4v6(ipt_mgr, + 'dropped', ipv4_rules, ipv6_rules, + wrap=True, top=True, tag=prefix.id) + # Add the NFLOG rules to the rejected chain in iptables + self._add_rules_to_chain_v4v6(ipt_mgr, + 'rejected', ipv4_rules, ipv6_rules, + wrap=True, top=True, tag=prefix.id) + prefix.add_log_obj_ref(fwg_port_log.log_id) + + def _add_rules_to_chain_v4v6(self, ipt_mgr, chain_name, v4_rules, v6_rules, + wrap=False, comment=None, + top=False, tag=None): + """Add rules to filter table in iptables and ip6tables""" + + for rule in v4_rules: + ipt_mgr.ipv4['filter'].add_rule(chain_name, rule, wrap=wrap, + comment=comment, top=top, tag=tag) + for rule in v6_rules: + ipt_mgr.ipv6['filter'].add_rule(chain_name, rule, wrap=wrap, + comment=comment, top=top, tag=tag) + + def _add_fwg_port_log(self, log_id, port_id, log_info): + + self.fwg_port_logs[log_id].add(FWGPortLog(port_id, log_info)) + + # Add log ID into the corresponding LogPrefix object + if log_info['event'] == log_const.ALL_EVENT: + events = [log_const.ACCEPT_EVENT, log_const.DROP_EVENT] + else: + events = [log_info['event']] + for event in events: + prefix = self._get_prefix(port_id, event) + if prefix: + prefix.add_log_obj_ref(log_id) + + def _generate_nflog_rules_v4v6(self, device, prefix): + iptables_rules = [] + added_rules = set() + for direction in constants.VALID_DIRECTIONS: + args = self._generate_iptables_args(direction, device, prefix) + rule = ' '.join(args) + if rule in added_rules: + # Since these rules are already added to iptables, + # so we ignore them here + continue + added_rules.add(rule) + iptables_rules.append(rule) + LOG.debug("iptables-nflog-rules %r", iptables_rules) + return iptables_rules, iptables_rules + + def _generate_iptables_args(self, direction, device, prefix=None): + + direction_config = ['-%s %s' % + (IPTABLES_DIRECTION_DEVICE[direction], device)] + match_rule = [] + if self.rate_limit: + match_rule += ['-m', 'limit', '--limit', '%s/s' % self.rate_limit] + if self.burst_limit: + match_rule += ['--limit-burst %s' % self.burst_limit] + target = ['-j', 'NFLOG'] + if prefix: + target += ['--nflog-prefix', '%s' % prefix] + + args = direction_config + match_rule + target + return args + + def _clear_rules_from_tag_v4v6(self, ipt_mgt, tag): + """Remove rules from filter table in iptables and ip6tables""" + ipt_mgt.ipv4['filter'].clear_rules_by_tag(tag) + ipt_mgt.ipv6['filter'].clear_rules_by_tag(tag) diff --git a/neutron_fwaas/services/logapi/agents/l3/__init__.py b/neutron_fwaas/services/logapi/agents/l3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/logapi/agents/l3/fwg_log.py b/neutron_fwaas/services/logapi/agents/l3/fwg_log.py new file mode 100644 index 000000000..82dc647ad --- /dev/null +++ b/neutron_fwaas/services/logapi/agents/l3/fwg_log.py @@ -0,0 +1,41 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +from neutron.services.logapi.agent.l3 import base +from neutron.services.logapi.agent import log_extension as log_ext +from neutron.services.logapi.rpc import agent as agent_rpc +from neutron_lib.agent import l3_extension + +#TODO(annp) move to neutron-lib +FIREWALL_LOG_DRIVER_NAME = 'fwaas_v2_log' + + +class FWaaSL3LoggingExtension(base.L3LoggingExtensionBase, + l3_extension.L3AgentExtension): + + def initialize(self, connection, driver_type): + """Initialize L3 logging agent extension""" + + fw_log_cls = self._load_driver_cls( + log_ext.LOGGING_DRIVERS_NAMESPACE, FIREWALL_LOG_DRIVER_NAME) + self.log_driver = fw_log_cls(self.agent_api) + self.resource_rpc = agent_rpc.LoggingApiStub() + self._register_rpc_consumers() + self.log_driver.initialize(self.resource_rpc) + + def update_network(self, context, data): + # TODO(zhouhenglc) remove at base.L3LoggingExtensionBase support + # update_network + pass diff --git a/neutron_fwaas/services/logapi/common/__init__.py b/neutron_fwaas/services/logapi/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/logapi/common/fwg_callback.py b/neutron_fwaas/services/logapi/common/fwg_callback.py new file mode 100644 index 000000000..f7a463345 --- /dev/null +++ b/neutron_fwaas/services/logapi/common/fwg_callback.py @@ -0,0 +1,61 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +from neutron.objects import ports as port_objects +from neutron.services.logapi.drivers import manager +from neutron_lib.callbacks import events +from neutron_lib import constants as nl_const +from neutron_lib.services.logapi import constants as log_const + +from neutron_fwaas.services.logapi.common import log_db_api + + +class FirewallGroupCallBack(manager.ResourceCallBackBase): + + def handle_event(self, resource, event, trigger, **kwargs): + payload = kwargs.get('payload') + context = payload.context + ports_delta = [] + if event == events.AFTER_CREATE: + # Update log when a new firewall group is created with ports + ports_delta = payload.latest_state['ports'] + + elif event == events.AFTER_UPDATE: + old_ports = payload.states[0]['ports'] + new_ports = payload.states[1]['ports'] + + # Check whether port is updated from firewall group or not + ports_delta = \ + set(new_ports).symmetric_difference(set(old_ports)) + + if self.need_to_notify(context, ports_delta): + self.trigger_logging(context, payload.resource_id, ports_delta) + + def trigger_logging(self, context, fwg_id, ports_delta): + log_resources = log_db_api.get_logs_for_fwg( + context, fwg_id, ports_delta) + if log_resources: + self.resource_push_api( + log_const.RESOURCE_UPDATE, context, log_resources) + + def need_to_notify(self, context, ports): + notify = False + for port_id in ports: + port = port_objects.Port.get_object(context, id=port_id) + device_owner = port.get('device_owner', '') + if device_owner in nl_const.ROUTER_INTERFACE_OWNERS: + notify = True + break + return notify diff --git a/neutron_fwaas/services/logapi/common/log_db_api.py b/neutron_fwaas/services/logapi/common/log_db_api.py new file mode 100644 index 000000000..d188413c3 --- /dev/null +++ b/neutron_fwaas/services/logapi/common/log_db_api.py @@ -0,0 +1,228 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +from neutron.objects.logapi import logging_resource as log_object +from neutron.objects import ports as port_objects +from neutron_lib import constants as nl_const +from neutron_lib.plugins import directory + +from neutron_fwaas.common import fwaas_constants +from neutron_fwaas.services.logapi import constants + +fw_plugin_db = None + + +# TODO(longkb): We will support L2 port. Currently, this method returns only +# router ports. +def _get_ports_being_logged(context, log_obj): + """Return a list of ports being logged with a given log_obj""" + + target_id = log_obj['target_id'] + resource_id = log_obj['resource_id'] + + # If 'target_id' (port_id) is specified in a log_obj + if target_id: + fwg_id = fw_plugin_db.get_fwg_attached_to_port(context, target_id) + if fwg_id: + port_ids = [target_id] + else: + port_ids = [] + # If 'resource_id' (fwg_id) is specified in a log_obj + elif resource_id: + port_ids = \ + fw_plugin_db.get_ports_in_firewall_group(context, resource_id) + # Both 'resource_id' and 'target_id' aren't specified in a log_resource + else: + tenant_id = log_obj['project_id'] + port_ids = fw_plugin_db.get_fwg_ports_in_tenant(context, tenant_id) + + filtered_port_ids = [] + for port_id in port_ids: + port = port_objects.Port.get_object(context, id=port_id) + device_owner = port.get('device_owner', '') + # TODO(longkb): L2 ports will be supported in the future + # Check whether a port is router port or not. + if device_owner in nl_const.ROUTER_INTERFACE_OWNERS: + # Checking port status + if port.get('status') == nl_const.PORT_STATUS_ACTIVE: + # Check whether a port is attached to firewall group or not + fwg = fw_plugin_db.get_fwg_attached_to_port(context, port_id) + if fwg: + filtered_port_ids.append(port_id) + return filtered_port_ids + + +def _make_log_info_dict(log_obj, port_ids): + log_info = { + 'id': log_obj['id'], + 'ports_log': port_ids, + 'event': log_obj['event'], + 'project_id': log_obj['project_id'] + } + return log_info + + +def get_logs_for_port(context, port_id): + """Return a list of log_resources bound to a given port_id""" + + global fw_plugin_db + if not fw_plugin_db: + fw_plugin = directory.get_plugin(fwaas_constants.FIREWALL_V2) + + # NOTE(longkb): check whether fw plugin was loaded or not. + if not fw_plugin: + return [] + fw_plugin_db = fw_plugin.driver.firewall_db + + logs_bounded = [] + port = port_objects.Port.get_object(context, id=port_id) + + if not port: + return logs_bounded + # Ignore if a given port_id is not belong to router port + device_owner = port.get('device_owner', '') + if device_owner not in nl_const.ROUTER_INTERFACE_OWNERS: + return logs_bounded + + # Ignore if a given port does not attach to any fwg + fwg_id = fw_plugin_db.get_fwg_attached_to_port(context, port_id) + if not fwg_id: + return logs_bounded + + project_id = port['project_id'] + log_objs = log_object.Log.get_objects( + context, project_id=project_id, + resource_type=constants.FIREWALL_GROUP, enabled=True) + + for log_obj in log_objs: + if log_obj.resource_id == fwg_id: + logs_bounded.append(log_obj) + elif log_obj.target_id == port['id']: + logs_bounded.append(log_obj) + elif not log_obj.target_id and not log_obj.resource_id: + logs_bounded.append(log_obj) + return logs_bounded + + +def get_logs_for_fwg(context, fwg_id, ports_delta): + """Return a list of log_resources bound to a firewall group""" + + global fw_plugin_db + if not fw_plugin_db: + fw_plugin = directory.get_plugin(fwaas_constants.FIREWALL_V2) + + # NOTE(longkb): check whether fw plugin was loaded or not. + if not fw_plugin: + return [] + fw_plugin_db = fw_plugin.driver.firewall_db + + project_id = context.tenant_id + log_objs = log_object.Log.get_objects( + context, project_id=project_id, + resource_type=constants.FIREWALL_GROUP, enabled=True) + + log_resources = [] + for log_obj in log_objs: + if log_obj.resource_id == fwg_id: + log_resources.append(log_obj) + elif log_obj.target_id in ports_delta: + log_resources.append(log_obj) + elif not log_obj.resource_id and not log_obj.target_id: + log_resources.append(log_obj) + return log_resources + + +def get_fwg_log_info_for_port(context, port_ids): + """Return a list of firewall group log info for a given port + The list has format as below: + + [ + { + 'event': u'ALL', + 'id': '733e0499-e69e-4106-a84a-635fbc5fbbc0', + 'project_id': u'46f70361-ba71-4bd0-9769-3573fd227c4b', + 'ports_log': + [ + port1_id, + port2_id, + ] + }, + ] + :param context: current running context information + :param port_ids: list of ports which needed to get firewall group log info + + """ + + global fw_plugin_db + if not fw_plugin_db: + fw_plugin = directory.get_plugin(fwaas_constants.FIREWALL_V2) + + # NOTE(longkb): check whether fw plugin was loaded or not. + if not fw_plugin: + return [] + fw_plugin_db = fw_plugin.driver.firewall_db + + logs_info = [] + log_bounds = set() + for port_id in port_ids: + log_objs = get_logs_for_port(context, port_id) + if log_objs: + log_bounds |= set(log_objs) + if log_bounds: + for log_resource in log_bounds: + port_ids = _get_ports_being_logged(context, log_resource) + log_info = _make_log_info_dict(log_resource, port_ids) + logs_info.append(log_info) + return logs_info + + +def get_fwg_log_info_for_log_resources(context, log_resources): + """Return a list of firewall group log info for list of log_resources + The list has format as below: + + [ + { + 'event': u'ALL', + 'id': '733e0499-e69e-4106-a84a-635fbc5fbbc0', + 'project_id': u'46f70361-ba71-4bd0-9769-3573fd227c4b', + 'ports_log': + [ + port1_id, + port2_id, + ] + }, + ] + :param context: current running context information + :param log_resources: list of log_resources, which needed to get firewall + groups log info + + """ + + global fw_plugin_db + if not fw_plugin_db: + fw_plugin = directory.get_plugin(fwaas_constants.FIREWALL_V2) + + # NOTE(longkb): check whether fw plugin was loaded or not. + if not fw_plugin: + return [] + fw_plugin_db = fw_plugin.driver.firewall_db + + logs_info = [] + for log_resource in log_resources: + ports_id = _get_ports_being_logged(context, log_resource) + log_info = _make_log_info_dict(log_resource, ports_id) + logs_info.append(log_info) + + return logs_info diff --git a/neutron_fwaas/services/logapi/common/port_callback.py b/neutron_fwaas/services/logapi/common/port_callback.py new file mode 100644 index 000000000..768a983a1 --- /dev/null +++ b/neutron_fwaas/services/logapi/common/port_callback.py @@ -0,0 +1,40 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +from neutron.services.logapi.drivers import manager +from neutron_lib.callbacks import events +from neutron_lib import constants as nl_const +from neutron_lib.services.logapi import constants as log_const + +from neutron_fwaas.services.logapi.common import log_db_api + + +class NeutronPortCallBack(manager.ResourceCallBackBase): + + def handle_event(self, resource, event, trigger, payload): + if event == events.AFTER_UPDATE: + context = payload.context + original_port = payload.states[0] + port = payload.states[1] + + if port['device_owner'] in nl_const.ROUTER_INTERFACE_OWNERS: + if original_port['status'] != port['status']: + self.trigger_logging(context, port) + + def trigger_logging(self, context, port): + log_resources = log_db_api.get_logs_for_port(context, port['id']) + if log_resources: + self.resource_push_api( + log_const.RESOURCE_UPDATE, context, log_resources) diff --git a/neutron_fwaas/services/logapi/constants.py b/neutron_fwaas/services/logapi/constants.py new file mode 100644 index 000000000..fc82767b0 --- /dev/null +++ b/neutron_fwaas/services/logapi/constants.py @@ -0,0 +1,21 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + + +# Firewall group logging resource type +FIREWALL_GROUP = 'firewall_group' + +# Target logging resource type +TARGET_RESOURCE = 'port which is associated with the firewall group' diff --git a/neutron_fwaas/services/logapi/exceptions.py b/neutron_fwaas/services/logapi/exceptions.py new file mode 100644 index 000000000..c7af753c0 --- /dev/null +++ b/neutron_fwaas/services/logapi/exceptions.py @@ -0,0 +1,34 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +from neutron._i18n import _ +from neutron_lib import exceptions as n_exc + +# TODO(annp or longkb): move to neutron-lib + + +class FWGIsNotReadyForLogging(n_exc.InvalidInput): + message = _("Firewall group %(fwg_id)s is not ready for logging " + "because of %(fwg_status)s status.") + + +class TargetResourceNotAssociated(n_exc.InvalidInput): + message = _("Target resource %(target_id)s is not associated with " + "any firewall group.") + + +class PortIsNotReadyForLogging(n_exc.InvalidInput): + message = _("Target resource %(target_id)s is not ready for logging " + "because of %(port_status)s status.") diff --git a/neutron_fwaas/services/logapi/fwg_validate.py b/neutron_fwaas/services/logapi/fwg_validate.py new file mode 100644 index 000000000..ea3f3b0e0 --- /dev/null +++ b/neutron_fwaas/services/logapi/fwg_validate.py @@ -0,0 +1,125 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +from neutron.objects import ports +from neutron.services.logapi.common import exceptions as log_exc +from neutron.services.logapi.common import validators +from neutron_lib import constants as nl_const +from neutron_lib.plugins import directory +from sqlalchemy.orm import exc as orm_exc + +from neutron_fwaas.common import fwaas_constants +from neutron_fwaas.services.logapi import constants as log_const +from neutron_fwaas.services.logapi import exceptions as fwg_log_exc + +fwg_plugin = None + + +def _check_fwg(context, fwg_id): + try: + fwg = fwg_plugin.get_firewall_group(context, id=fwg_id) + except orm_exc.NoResultFound: + raise log_exc.ResourceNotFound(resource_id=fwg_id) + + if fwg['status'] != nl_const.ACTIVE: + raise fwg_log_exc.FWGIsNotReadyForLogging( + fwg_id=fwg_id, fwg_status=fwg['status']) + + +def _check_fwg_port(context, port_id): + + # Checking port exists + port = ports.Port.get_object(context, id=port_id) + if not port: + raise log_exc.TargetResourceNotFound(target_id=port_id) + + device_owner = port.get('device_owner', '') + # Checking supported firewall group logging for vm port + if device_owner.startswith(nl_const.DEVICE_OWNER_COMPUTE_PREFIX): + if not validators.validate_log_type_for_port( + log_const.FIREWALL_GROUP, port): + raise log_exc.LoggingTypeNotSupported( + log_type=log_const.FIREWALL_GROUP, + port_id=port_id) + # Checking supported firewall group for router interface, DVR interface, + # and HA replicated interface + elif device_owner not in nl_const.ROUTER_INTERFACE_OWNERS: + raise log_exc.LoggingTypeNotSupported( + log_type=log_const.FIREWALL_GROUP, port_id=port_id) + + # Checking port status + port_status = port.get('status') + if port_status != nl_const.PORT_STATUS_ACTIVE: + raise fwg_log_exc.PortIsNotReadyForLogging(target_id=port_id, + port_status=port_status) + + # Checking whether router port or vm port binding with any firewall group + fwg_id = fwg_plugin.driver.firewall_db.get_fwg_attached_to_port( + context, port_id=port_id) + + if not fwg_id: + raise fwg_log_exc.TargetResourceNotAssociated(target_id=port_id) + + fwg = fwg_plugin.get_firewall_group(context, id=fwg_id) + + if fwg['status'] != nl_const.ACTIVE: + raise fwg_log_exc.FWGIsNotReadyForLogging(fwg_id=fwg_id, + fwg_status=fwg['status']) + + +def _check_target_resource_bound_fwg(context, fwg_id, target_id): + ports = fwg_plugin.driver.firewall_db.get_ports_in_firewall_group( + context=context, firewall_group_id=fwg_id) + if target_id not in ports: + raise log_exc.InvalidResourceConstraint( + resource=log_const.FIREWALL_GROUP, + resource_id=fwg_id, + target_resource=log_const.TARGET_RESOURCE, + target_id=target_id) + + +@validators.ResourceValidateRequest.register(log_const.FIREWALL_GROUP) +def validate_firewall_group_request(context, log_data): + """Validate a log request + + This method validates log request is satisfied or not. + + A ResourceNotFound will be raised if resource_id in log_data not exists or + a TargetResourceNotFound will be raised if target_id in log_data not + exists. Beside, FWGIsNotReadyForLogging will be raised in the case of + queried firewall group is not in ACTIVE state. PortIsNotReadyForLogging + exception will be raised if port is not in ACTIVE status. Besides, + TargetResourceNotAssociated exception will be raised if a given port does + not have any firewall group attach to. This method will also raise a + LoggingTypeNotSupported, if there is no log_driver supporting for + resource_type in log_data. + + In addition, if log_data specify both resource_id and target_id. A + InvalidResourceConstraint will be raised if there is no constraint between + resource_id and target_id. + + """ + + global fwg_plugin + if not fwg_plugin: + fwg_plugin = directory.get_plugin(fwaas_constants.FIREWALL_V2) + resource_id = log_data.get('resource_id') + target_id = log_data.get('target_id') + if resource_id and target_id: + _check_target_resource_bound_fwg(context, resource_id, target_id) + if resource_id: + _check_fwg(context, resource_id) + if target_id: + _check_fwg_port(context, target_id) diff --git a/neutron_fwaas/services/logapi/rpc/__init__.py b/neutron_fwaas/services/logapi/rpc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/logapi/rpc/log_server.py b/neutron_fwaas/services/logapi/rpc/log_server.py new file mode 100644 index 000000000..ca5e2e244 --- /dev/null +++ b/neutron_fwaas/services/logapi/rpc/log_server.py @@ -0,0 +1,30 @@ +# Copyright (C) 2018 Fujitsu Limited +# 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. + +from neutron_fwaas.services.logapi.common import log_db_api + + +# Use this when register log driver with +# "register_rpc_methods" function +def get_fwg_log_info_for_port(context, port_id): + return log_db_api.get_fwg_log_info_for_port(context, port_id) + + +# Use this when register log driver with +# "register_rpc_methods" function +def get_fwg_log_info_for_log_resources(context, log_resources): + return log_db_api.get_fwg_log_info_for_log_resources( + context, + log_resources) diff --git a/neutron_fwaas/tests/__init__.py b/neutron_fwaas/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/base.py b/neutron_fwaas/tests/base.py new file mode 100644 index 000000000..10b2490b9 --- /dev/null +++ b/neutron_fwaas/tests/base.py @@ -0,0 +1,21 @@ +# Copyright 2014 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. +# + +from neutron.tests import base as n_base + + +class BaseTestCase(n_base.BaseTestCase): + pass diff --git a/neutron_fwaas/tests/contrib/README b/neutron_fwaas/tests/contrib/README new file mode 100644 index 000000000..a73d75af9 --- /dev/null +++ b/neutron_fwaas/tests/contrib/README @@ -0,0 +1,3 @@ +The files in this directory are intended for use by the +Neutron infra jobs that run the various functional test +suites in the gate. diff --git a/neutron_fwaas/tests/contrib/filters.template b/neutron_fwaas/tests/contrib/filters.template new file mode 100644 index 000000000..78189d760 --- /dev/null +++ b/neutron_fwaas/tests/contrib/filters.template @@ -0,0 +1,20 @@ +# neutron-rootwrap command filters to support functional testing. It +# is NOT intended to be used outside of a test environment. +# +# This file should be owned by (and only-writeable by) the root user + +[Filters] +# '$BASE_PATH' is intended to be replaced with the expected tox path +# (e.g. /opt/stack/new/neutron/.tox/dsvm-functional) by the neutron +# functional jenkins job. This ensures that tests can kill the +# processes that they launch with their containing tox environment's +# python. +kill_tox_python: KillFilter, root, $BASE_PATH/bin/python, -9 + +# enable ping from namespace +ping_filter: CommandFilter, ping, root + +# enable curl from namespace +curl_filter: CommandFilter, curl, root +tee_filter: CommandFilter, tee, root +tee_kill: KillFilter, root, tee, -9 diff --git a/neutron_fwaas/tests/contrib/functional-testing.filters b/neutron_fwaas/tests/contrib/functional-testing.filters new file mode 100644 index 000000000..23fbc9b73 --- /dev/null +++ b/neutron_fwaas/tests/contrib/functional-testing.filters @@ -0,0 +1,10 @@ +# neutron-rootwrap command filters to support functional testing. It +# is NOT intended to be used outside of a test environment. +# +# This file should be owned by (and only-writeable by) the root user + +[Filters] +# enable ping from namespace +ping_filter: CommandFilter, ping, root +ping6_filter: CommandFilter, ping6, root +ping_kill: KillFilter, root, ping, -2 diff --git a/neutron_fwaas/tests/contrib/gate_hook.sh b/neutron_fwaas/tests/contrib/gate_hook.sh new file mode 100644 index 000000000..4ce09dd83 --- /dev/null +++ b/neutron_fwaas/tests/contrib/gate_hook.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -ex + +VENV=${1:-"dsvm-functional"} + +GATE_DEST=$BASE/new +FWAAS_PATH=$GATE_DEST/neutron-fwaas +DEVSTACK_PATH=$GATE_DEST/devstack + + +case $VENV in + "dsvm-functional"|"dsvm-fullstack") + # The following need to be set before sourcing + # configure_for_fwaas_func_testing. + GATE_STACK_USER=stack + PROJECT_NAME=neutron-fwaas + IS_GATE=True + + source $FWAAS_PATH/tools/configure_for_fwaas_func_testing.sh + + configure_host_for_func_testing + if is_ubuntu || is_suse; then + install_package libnetfilter-log1 + elif is_fedora; then + install_package libnetfilter-log + fi + ;; +esac diff --git a/neutron_fwaas/tests/contrib/gate_hook_tempest.sh b/neutron_fwaas/tests/contrib/gate_hook_tempest.sh new file mode 100755 index 000000000..99f1afe35 --- /dev/null +++ b/neutron_fwaas/tests/contrib/gate_hook_tempest.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +set -ex + +FWAAS_VERSION=$1 + +GATE_DEST=$BASE/new +GATE_HOOKS=$GATE_DEST/neutron-fwaas/neutron_fwaas/tests/contrib/hooks +DEVSTACK_PATH=$GATE_DEST/devstack +LOCAL_CONF=$DEVSTACK_PATH/late-local.conf +DSCONF=/tmp/devstack-tools/bin/dsconf + +# Install devstack-tools used to produce local.conf; we can't rely on +# test-requirements.txt because the gate hook is triggered before neutron-fwaas +# is installed +sudo -H pip install virtualenv +virtualenv /tmp/devstack-tools +/tmp/devstack-tools/bin/pip install -U devstack-tools==0.4.0 + +# Inject config from hook into localrc +function load_rc_hook { + local hook="$1" + local tmpfile + local config + tmpfile=$(tempfile) + config=$(cat $GATE_HOOKS/$hook) + echo "[[local|localrc]]" > $tmpfile + $DSCONF setlc_raw $tmpfile "$config" + $DSCONF merge_lc $LOCAL_CONF $tmpfile + rm -f $tmpfile +} + +LOCAL_CONF=$DEVSTACK_PATH/local.conf +load_rc_hook api_extensions-base +load_rc_hook api_extensions-${FWAAS_VERSION} + +export DEVSTACK_LOCALCONF=$(cat $LOCAL_CONF) +$BASE/new/devstack-gate/devstack-vm-gate.sh diff --git a/neutron_fwaas/tests/contrib/hooks/api_extensions-base b/neutron_fwaas/tests/contrib/hooks/api_extensions-base new file mode 100644 index 000000000..02be81851 --- /dev/null +++ b/neutron_fwaas/tests/contrib/hooks/api_extensions-base @@ -0,0 +1 @@ +NETWORK_API_EXTENSIONS=agent,binding,dhcp_agent_scheduler,external-net,ext-gw-mode,extra_dhcp_opts,quotas,router,security-group,subnet_allocation,network-ip-availability,auto-allocated-topology,timestamp_core,tag,service-type,rbac-policies,standard-attr-description,pagination,sorting,project-id diff --git a/neutron_fwaas/tests/contrib/hooks/api_extensions-legacy b/neutron_fwaas/tests/contrib/hooks/api_extensions-legacy new file mode 100644 index 000000000..98c9f4adc --- /dev/null +++ b/neutron_fwaas/tests/contrib/hooks/api_extensions-legacy @@ -0,0 +1 @@ +NETWORK_API_EXTENSIONS+=,fwaas,fwaasrouterinsertion diff --git a/neutron_fwaas/tests/contrib/hooks/api_extensions-v1 b/neutron_fwaas/tests/contrib/hooks/api_extensions-v1 new file mode 100644 index 000000000..98c9f4adc --- /dev/null +++ b/neutron_fwaas/tests/contrib/hooks/api_extensions-v1 @@ -0,0 +1 @@ +NETWORK_API_EXTENSIONS+=,fwaas,fwaasrouterinsertion diff --git a/neutron_fwaas/tests/contrib/hooks/api_extensions-v2 b/neutron_fwaas/tests/contrib/hooks/api_extensions-v2 new file mode 100644 index 000000000..7a7e7f950 --- /dev/null +++ b/neutron_fwaas/tests/contrib/hooks/api_extensions-v2 @@ -0,0 +1 @@ +NETWORK_API_EXTENSIONS+=,fwaas_v2 diff --git a/neutron_fwaas/tests/contrib/hooks/iptables_verify b/neutron_fwaas/tests/contrib/hooks/iptables_verify new file mode 100644 index 000000000..72cbd1aeb --- /dev/null +++ b/neutron_fwaas/tests/contrib/hooks/iptables_verify @@ -0,0 +1,4 @@ +[[post-config|/etc/neutron/neutron.conf]] + +[AGENT] +debug_iptables_rules=True diff --git a/neutron_fwaas/tests/contrib/post_test_hook.sh b/neutron_fwaas/tests/contrib/post_test_hook.sh new file mode 100644 index 000000000..fabac5bf4 --- /dev/null +++ b/neutron_fwaas/tests/contrib/post_test_hook.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +set -xe + +FWAAS_DIR="$BASE/new/neutron-fwaas" +NEUTRON_DIR="$BASE/new/neutron" +TEMPEST_DIR="$BASE/new/tempest" +SCRIPTS_DIR="/usr/os-testr-env/bin" + +venv=${1:-"dsvm-functional"} + +function generate_testr_results { + # Give job user rights to access tox logs + sudo -H -u $owner chmod o+rw . + sudo -H -u $owner chmod o+rw -R .stestr + if [ -f ".stestr/0" ] ; then + .tox/$venv/bin/subunit-1to2 < .stestr/0 > ./stestr.subunit + $SCRIPTS_DIR/subunit2html ./stestr.subunit testr_results.html + gzip -9 ./stestr.subunit + gzip -9 ./testr_results.html + sudo mv ./*.gz /opt/stack/logs/ + fi +} + +function dsvm_functional_prep_func { + : +} + +if [[ "$venv" == dsvm-functional* ]] +then + owner=stack + sudo_env= + # Set owner permissions according to job's requirements. + cd $FWAAS_DIR + sudo chown -R $owner:stack $FWAAS_DIR + sudo chown -R $owner:stack $NEUTRON_DIR + # Prep the environment according to job's requirements. + $prep_func + + # Run tests + echo "Running neutron-fwaas $venv test suite" + set +e + sudo -H -u $owner $sudo_env tox -e $venv + testr_exit_code=$? + set -e + + # Collect and parse results + generate_testr_results + exit $testr_exit_code +fi diff --git a/neutron_fwaas/tests/fullstack/README b/neutron_fwaas/tests/fullstack/README new file mode 100644 index 000000000..cf1b7111f --- /dev/null +++ b/neutron_fwaas/tests/fullstack/README @@ -0,0 +1 @@ +Please see neutron/TESTING.rst for more information about what Fullstack tests are. diff --git a/neutron_fwaas/tests/fullstack/__init__.py b/neutron_fwaas/tests/fullstack/__init__.py new file mode 100644 index 000000000..5c53d8822 --- /dev/null +++ b/neutron_fwaas/tests/fullstack/__init__.py @@ -0,0 +1,16 @@ +# 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 neutron.common import eventlet_utils + + +eventlet_utils.monkey_patch() diff --git a/neutron_fwaas/tests/fullstack/base.py b/neutron_fwaas/tests/fullstack/base.py new file mode 100644 index 000000000..f8894a57d --- /dev/null +++ b/neutron_fwaas/tests/fullstack/base.py @@ -0,0 +1,67 @@ +# Copyright 2015 Red Hat, 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 os + +from neutron_lib.tests import tools +from oslo_config import cfg + +from neutron.tests import base as tests_base +from neutron.tests.fullstack.resources import client as client_resource +from neutron.tests.unit import testlib_api + + +# This is the directory from which infra fetches log files for fullstack tests +DEFAULT_LOG_DIR = os.path.join('/opt/stack/logs/neutron-fwaas/', + 'dsvm-fullstack-logs') + + +class BaseFullStackTestCase(testlib_api.MySQLTestCaseMixin, + testlib_api.SqlTestCase): + """Base test class for full-stack tests.""" + + BUILD_WITH_MIGRATIONS = True + + def setUp(self, environment): + super(BaseFullStackTestCase, self).setUp() + + tests_base.setup_test_logging( + cfg.CONF, DEFAULT_LOG_DIR, '%s.txt' % self.get_name()) + + # NOTE(zzzeek): the opportunistic DB fixtures have built for + # us a per-test (or per-process) database. Set the URL of this + # database in CONF as the full stack tests need to actually run a + # neutron server against this database. + _orig_db_url = cfg.CONF.database.connection + cfg.CONF.set_override( + 'connection', str(self.engine.url), group='database') + self.addCleanup( + cfg.CONF.set_override, + "connection", _orig_db_url, group="database" + ) + + # NOTE(ihrachys): seed should be reset before environment fixture below + # since the latter starts services that may rely on generated port + # numbers + tools.reset_random_seed() + self.environment = environment + self.environment.test_name = self.get_name() + self.useFixture(self.environment) + self.client = self.environment.neutron_server.client + self.safe_client = self.useFixture( + client_resource.ClientFixture(self.client)) + + def get_name(self): + class_name, test_name = self.id().split(".")[-2:] + return "%s.%s" % (class_name, test_name) diff --git a/neutron_fwaas/tests/fullstack/resources/__init__.py b/neutron_fwaas/tests/fullstack/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/fullstack/resources/client.py b/neutron_fwaas/tests/fullstack/resources/client.py new file mode 100644 index 000000000..d97675fd0 --- /dev/null +++ b/neutron_fwaas/tests/fullstack/resources/client.py @@ -0,0 +1,247 @@ +# Copyright (c) 2015 Thales Services SAS +# +# 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 functools + +import netaddr + +import fixtures +from neutron_lib import constants +from neutronclient.common import exceptions + +from neutron.common import utils +from neutron.extensions import portbindings + + +def _safe_method(f): + @functools.wraps(f) + def delete(*args, **kwargs): + try: + return f(*args, **kwargs) + except exceptions.NotFound: + pass + return delete + + +class ClientFixture(fixtures.Fixture): + """Manage and cleanup neutron resources.""" + + def __init__(self, client): + super(ClientFixture, self).__init__() + self.client = client + + def _create_resource(self, resource_type, spec): + create = getattr(self.client, 'create_%s' % resource_type) + delete = getattr(self.client, 'delete_%s' % resource_type) + + body = {resource_type: spec} + resp = create(body=body) + data = resp[resource_type] + self.addCleanup(_safe_method(delete), data['id']) + return data + + def create_router(self, tenant_id, name=None, ha=False, + external_network=None): + resource_type = 'router' + + name = name or utils.get_rand_name(prefix=resource_type) + spec = {'tenant_id': tenant_id, 'name': name, 'ha': ha} + if external_network: + spec['external_gateway_info'] = {"network_id": external_network} + + return self._create_resource(resource_type, spec) + + def create_network(self, tenant_id, name=None, external=False): + resource_type = 'network' + + name = name or utils.get_rand_name(prefix=resource_type) + spec = {'tenant_id': tenant_id, 'name': name} + spec['router:external'] = external + return self._create_resource(resource_type, spec) + + def create_subnet(self, tenant_id, network_id, + cidr, gateway_ip=None, name=None, enable_dhcp=True, + ipv6_address_mode='slaac', ipv6_ra_mode='slaac'): + resource_type = 'subnet' + + name = name or utils.get_rand_name(prefix=resource_type) + ip_version = netaddr.IPNetwork(cidr).version + spec = {'tenant_id': tenant_id, 'network_id': network_id, 'name': name, + 'cidr': cidr, 'enable_dhcp': enable_dhcp, + 'ip_version': ip_version} + if ip_version == constants.IP_VERSION_6: + spec['ipv6_address_mode'] = ipv6_address_mode + spec['ipv6_ra_mode'] = ipv6_ra_mode + + if gateway_ip: + spec['gateway_ip'] = gateway_ip + + return self._create_resource(resource_type, spec) + + def create_port(self, tenant_id, network_id, hostname=None, + qos_policy_id=None, **kwargs): + spec = { + 'network_id': network_id, + 'tenant_id': tenant_id, + } + spec.update(kwargs) + if hostname is not None: + spec[portbindings.HOST_ID] = hostname + if qos_policy_id: + spec['qos_policy_id'] = qos_policy_id + return self._create_resource('port', spec) + + def create_floatingip(self, tenant_id, floating_network_id, + fixed_ip_address, port_id): + spec = { + 'floating_network_id': floating_network_id, + 'tenant_id': tenant_id, + 'fixed_ip_address': fixed_ip_address, + 'port_id': port_id + } + + return self._create_resource('floatingip', spec) + + def add_router_interface(self, router_id, subnet_id): + body = {'subnet_id': subnet_id} + router_interface_info = self.client.add_interface_router( + router=router_id, body=body) + self.addCleanup(_safe_method(self.client.remove_interface_router), + router=router_id, body=body) + return router_interface_info + + def create_qos_policy(self, tenant_id, name, description, shared): + policy = self.client.create_qos_policy( + body={'policy': {'name': name, + 'description': description, + 'shared': shared, + 'tenant_id': tenant_id}}) + + def detach_and_delete_policy(): + qos_policy_id = policy['policy']['id'] + ports_with_policy = self.client.list_ports( + qos_policy_id=qos_policy_id)['ports'] + for port in ports_with_policy: + self.client.update_port( + port['id'], + body={'port': {'qos_policy_id': None}}) + self.client.delete_qos_policy(qos_policy_id) + + # NOTE: We'll need to add support for detaching from network once + # create_network() supports qos_policy_id. + self.addCleanup(_safe_method(detach_and_delete_policy)) + + return policy['policy'] + + def create_bandwidth_limit_rule(self, tenant_id, qos_policy_id, limit=None, + burst=None): + rule = {'tenant_id': tenant_id} + if limit: + rule['max_kbps'] = limit + if burst: + rule['max_burst_kbps'] = burst + rule = self.client.create_bandwidth_limit_rule( + policy=qos_policy_id, + body={'bandwidth_limit_rule': rule}) + + self.addCleanup(_safe_method(self.client.delete_bandwidth_limit_rule), + rule['bandwidth_limit_rule']['id'], + qos_policy_id) + + return rule['bandwidth_limit_rule'] + + def create_dscp_marking_rule(self, tenant_id, qos_policy_id, dscp_mark=0): + rule = {'tenant_id': tenant_id} + if dscp_mark: + rule['dscp_mark'] = dscp_mark + rule = self.client.create_dscp_marking_rule( + policy=qos_policy_id, + body={'dscp_marking_rule': rule}) + + self.addCleanup(_safe_method(self.client.delete_dscp_marking_rule), + rule['dscp_marking_rule']['id'], + qos_policy_id) + + return rule['dscp_marking_rule'] + + def create_trunk(self, tenant_id, port_id, name=None, + admin_state_up=None, sub_ports=None): + """Create a trunk via API. + + :param tenant_id: ID of the tenant. + :param port_id: Parent port of trunk. + :param name: Name of the trunk. + :param admin_state_up: Admin state of the trunk. + :param sub_ports: List of subport dictionaries in format + {'port_id': , + 'segmentation_type': 'vlan', + 'segmentation_id': } + + :return: Dictionary with trunk's data returned from Neutron API. + """ + spec = { + 'port_id': port_id, + 'tenant_id': tenant_id, + } + if name is not None: + spec['name'] = name + if sub_ports is not None: + spec['sub_ports'] = sub_ports + if admin_state_up is not None: + spec['admin_state_up'] = admin_state_up + + trunk = self.client.create_trunk({'trunk': spec})['trunk'] + + if sub_ports: + self.addCleanup( + _safe_method(self.trunk_remove_subports), + tenant_id, trunk['id'], trunk['sub_ports']) + self.addCleanup(_safe_method(self.client.delete_trunk), trunk['id']) + + return trunk + + def trunk_add_subports(self, tenant_id, trunk_id, sub_ports): + """Add subports to the trunk. + + :param tenant_id: ID of the tenant. + :param trunk_id: ID of the trunk. + :param sub_ports: List of subport dictionaries to be added in format + {'port_id': , + 'segmentation_type': 'vlan', + 'segmentation_id': } + """ + spec = { + 'tenant_id': tenant_id, + 'sub_ports': sub_ports, + } + trunk = self.client.trunk_add_subports(trunk_id, spec) + + sub_ports_to_remove = [ + sub_port for sub_port in trunk['sub_ports'] + if sub_port in sub_ports] + self.addCleanup( + _safe_method(self.trunk_remove_subports), tenant_id, trunk_id, + sub_ports_to_remove) + + def trunk_remove_subports(self, tenant_id, trunk_id, sub_ports): + """Remove subports from the trunk. + + :param trunk_id: ID of the trunk. + :param sub_ports: List of subport port IDs. + """ + spec = { + 'tenant_id': tenant_id, + 'sub_ports': sub_ports, + } + return self.client.trunk_remove_subports(trunk_id, spec) diff --git a/neutron_fwaas/tests/fullstack/resources/config.py b/neutron_fwaas/tests/fullstack/resources/config.py new file mode 100644 index 000000000..60b2114ef --- /dev/null +++ b/neutron_fwaas/tests/fullstack/resources/config.py @@ -0,0 +1,290 @@ +# Copyright 2015 Red Hat, 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 tempfile + +import fixtures +from neutron_lib import constants + +from neutron.common import utils +from neutron.plugins.ml2.extensions import qos as qos_ext +from neutron.tests.common import config_fixtures +from neutron.tests.common.exclusive_resources import port +from neutron.tests.common import helpers as c_helpers + + +class ConfigFixture(fixtures.Fixture): + """A fixture that holds an actual Neutron configuration. + + Note that 'self.config' is intended to only be updated once, during + the constructor, so if this fixture is re-used (setUp is called twice), + then the dynamic configuration values won't change. The correct usage + is initializing a new instance of the class. + """ + def __init__(self, env_desc, host_desc, temp_dir, base_filename): + super(ConfigFixture, self).__init__() + self.config = config_fixtures.ConfigDict() + self.env_desc = env_desc + self.host_desc = host_desc + self.temp_dir = temp_dir + self.base_filename = base_filename + + def _setUp(self): + cfg_fixture = config_fixtures.ConfigFileFixture( + self.base_filename, self.config, self.temp_dir) + self.useFixture(cfg_fixture) + self.filename = cfg_fixture.filename + + +class NeutronConfigFixture(ConfigFixture): + + def __init__(self, env_desc, host_desc, temp_dir, + connection, rabbitmq_environment): + super(NeutronConfigFixture, self).__init__( + env_desc, host_desc, temp_dir, base_filename='neutron.conf') + + service_plugins = ['router', 'trunk'] + if env_desc.qos: + service_plugins.append('qos') + + self.config.update({ + 'DEFAULT': { + 'host': self._generate_host(), + 'state_path': self._generate_state_path(self.temp_dir), + 'api_paste_config': self._generate_api_paste(), + 'core_plugin': 'ml2', + 'service_plugins': ','.join(service_plugins), + 'auth_strategy': 'noauth', + 'debug': 'True', + 'transport_url': + 'rabbit://%(user)s:%(password)s@%(host)s:5672/%(vhost)s' % + {'user': rabbitmq_environment.user, + 'password': rabbitmq_environment.password, + 'host': rabbitmq_environment.host, + 'vhost': rabbitmq_environment.vhost}, + }, + 'database': { + 'connection': connection, + }, + 'oslo_concurrency': { + 'lock_path': '$state_path/lock', + }, + 'oslo_policy': { + 'policy_file': self._generate_policy_json(), + }, + }) + + def _setUp(self): + self.config['DEFAULT'].update({ + 'bind_port': self.useFixture( + port.ExclusivePort(constants.PROTO_NAME_TCP)).port + }) + super(NeutronConfigFixture, self)._setUp() + + def _generate_host(self): + return utils.get_rand_name(prefix='host-') + + def _generate_state_path(self, temp_dir): + # Assume that temp_dir will be removed by the caller + self.state_path = tempfile.mkdtemp(prefix='state_path', dir=temp_dir) + return self.state_path + + def _generate_api_paste(self): + return c_helpers.find_sample_file('api-paste.ini') + + def _generate_policy_json(self): + return c_helpers.find_sample_file('policy.json') + + +class ML2ConfigFixture(ConfigFixture): + + def __init__(self, env_desc, host_desc, temp_dir, tenant_network_types): + super(ML2ConfigFixture, self).__init__( + env_desc, host_desc, temp_dir, base_filename='ml2_conf.ini') + + mechanism_drivers = self.env_desc.mech_drivers + if self.env_desc.l2_pop: + mechanism_drivers += ',l2population' + + self.config.update({ + 'ml2': { + 'tenant_network_types': tenant_network_types, + 'mechanism_drivers': mechanism_drivers, + }, + 'ml2_type_vlan': { + 'network_vlan_ranges': 'physnet1:1000:2999', + }, + 'ml2_type_gre': { + 'tunnel_id_ranges': '1:1000', + }, + 'ml2_type_vxlan': { + 'vni_ranges': '1001:2000', + }, + }) + + if env_desc.qos: + self.config['ml2']['extension_drivers'] =\ + qos_ext.QOS_EXT_DRIVER_ALIAS + + +class OVSConfigFixture(ConfigFixture): + + def __init__(self, env_desc, host_desc, temp_dir, local_ip): + super(OVSConfigFixture, self).__init__( + env_desc, host_desc, temp_dir, + base_filename='openvswitch_agent.ini') + + self.tunneling_enabled = self.env_desc.tunneling_enabled + self.config.update({ + 'ovs': { + 'local_ip': local_ip, + 'integration_bridge': self._generate_integration_bridge(), + 'of_interface': host_desc.of_interface, + 'ovsdb_interface': host_desc.ovsdb_interface, + }, + 'securitygroup': { + 'firewall_driver': 'noop', + }, + 'agent': { + 'l2_population': str(self.env_desc.l2_pop), + 'arp_responder': str(self.env_desc.arp_responder), + } + }) + + if self.tunneling_enabled: + self.config['agent'].update({ + 'tunnel_types': self.env_desc.network_type}) + self.config['ovs'].update({ + 'tunnel_bridge': self._generate_tunnel_bridge(), + 'int_peer_patch_port': self._generate_int_peer(), + 'tun_peer_patch_port': self._generate_tun_peer()}) + else: + self.config['ovs']['bridge_mappings'] = ( + self._generate_bridge_mappings()) + + if env_desc.qos: + self.config['agent']['extensions'] = 'qos' + + def _setUp(self): + if self.config['ovs']['of_interface'] == 'native': + self.config['ovs'].update({ + 'of_listen_port': self.useFixture( + port.ExclusivePort(constants.PROTO_NAME_TCP)).port + }) + super(OVSConfigFixture, self)._setUp() + + def _generate_bridge_mappings(self): + return 'physnet1:%s' % utils.get_rand_device_name(prefix='br-eth') + + def _generate_integration_bridge(self): + return utils.get_rand_device_name(prefix='br-int') + + def _generate_tunnel_bridge(self): + return utils.get_rand_device_name(prefix='br-tun') + + def _generate_int_peer(self): + return utils.get_rand_device_name(prefix='patch-tun') + + def _generate_tun_peer(self): + return utils.get_rand_device_name(prefix='patch-int') + + def get_br_int_name(self): + return self.config.ovs.integration_bridge + + def get_br_phys_name(self): + return self.config.ovs.bridge_mappings.split(':')[1] + + def get_br_tun_name(self): + return self.config.ovs.tunnel_bridge + + +class LinuxBridgeConfigFixture(ConfigFixture): + + def __init__(self, env_desc, host_desc, temp_dir, local_ip, + physical_device_name): + super(LinuxBridgeConfigFixture, self).__init__( + env_desc, host_desc, temp_dir, + base_filename="linuxbridge_agent.ini" + ) + self.config.update({ + 'VXLAN': { + 'enable_vxlan': str(self.env_desc.tunneling_enabled), + 'local_ip': local_ip, + 'l2_population': str(self.env_desc.l2_pop), + } + }) + if env_desc.qos: + self.config.update({ + 'AGENT': { + 'extensions': 'qos' + } + }) + if self.env_desc.tunneling_enabled: + self.config.update({ + 'LINUX_BRIDGE': { + 'bridge_mappings': self._generate_bridge_mappings( + physical_device_name + ) + } + }) + else: + self.config.update({ + 'LINUX_BRIDGE': { + 'physical_interface_mappings': + self._generate_bridge_mappings( + physical_device_name + ) + } + }) + + def _generate_bridge_mappings(self, device_name): + return 'physnet1:%s' % device_name + + +class L3ConfigFixture(ConfigFixture): + + def __init__(self, env_desc, host_desc, temp_dir, integration_bridge=None): + super(L3ConfigFixture, self).__init__( + env_desc, host_desc, temp_dir, base_filename='l3_agent.ini') + if host_desc.l2_agent_type == constants.AGENT_TYPE_OVS: + self._prepare_config_with_ovs_agent(integration_bridge) + elif host_desc.l2_agent_type == constants.AGENT_TYPE_LINUXBRIDGE: + self._prepare_config_with_linuxbridge_agent() + self.config['DEFAULT'].update({ + 'debug': 'True', + 'test_namespace_suffix': self._generate_namespace_suffix(), + }) + + def _prepare_config_with_ovs_agent(self, integration_bridge): + self.config.update({ + 'DEFAULT': { + 'interface_driver': ('neutron.agent.linux.interface.' + 'OVSInterfaceDriver'), + 'ovs_integration_bridge': integration_bridge, + } + }) + + def _prepare_config_with_linuxbridge_agent(self): + self.config.update({ + 'DEFAULT': { + 'interface_driver': ('neutron.agent.linux.interface.' + 'BridgeInterfaceDriver'), + } + }) + + def _generate_external_bridge(self): + return utils.get_rand_device_name(prefix='br-ex') + + def _generate_namespace_suffix(self): + return utils.get_rand_name(prefix='test') diff --git a/neutron_fwaas/tests/fullstack/resources/environment.py b/neutron_fwaas/tests/fullstack/resources/environment.py new file mode 100644 index 000000000..454502a7f --- /dev/null +++ b/neutron_fwaas/tests/fullstack/resources/environment.py @@ -0,0 +1,362 @@ +# Copyright 2015 Red Hat, 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 fixtures +from neutron_lib import constants +from neutronclient.common import exceptions as nc_exc +from oslo_config import cfg + +from neutron.agent.linux import ip_lib +from neutron.common import utils as common_utils +from neutron.plugins.ml2.drivers.linuxbridge.agent import \ + linuxbridge_neutron_agent as lb_agent +from neutron.tests.common.exclusive_resources import ip_address +from neutron.tests.common.exclusive_resources import ip_network +from neutron.tests.common import net_helpers +from neutron.tests.fullstack.resources import config +from neutron.tests.fullstack.resources import process + + +class EnvironmentDescription(object): + """A set of characteristics of an environment setup. + + Does the setup, as a whole, support tunneling? How about l2pop? + """ + def __init__(self, network_type='vxlan', l2_pop=True, qos=False, + mech_drivers='openvswitch,linuxbridge', arp_responder=False): + self.network_type = network_type + self.l2_pop = l2_pop + self.qos = qos + self.network_range = None + self.mech_drivers = mech_drivers + self.arp_responder = arp_responder + + @property + def tunneling_enabled(self): + return self.network_type in ('vxlan', 'gre') + + +class HostDescription(object): + """A set of characteristics of an environment Host. + + What agents should the host spawn? What mode should each agent operate + under? + """ + def __init__(self, l3_agent=False, of_interface='ovs-ofctl', + ovsdb_interface='vsctl', + l2_agent_type=constants.AGENT_TYPE_OVS): + self.l2_agent_type = l2_agent_type + self.l3_agent = l3_agent + self.of_interface = of_interface + self.ovsdb_interface = ovsdb_interface + + +class Host(fixtures.Fixture): + """The Host class models a physical host running agents, all reporting with + the same hostname. + + OpenStack installers or administrators connect compute nodes to the + physical tenant network by connecting the provider bridges to their + respective physical NICs. Or, if using tunneling, by configuring an + IP address on the appropriate physical NIC. The Host class does the same + with the connect_* methods. + + TODO(amuller): Add start/stop/restart methods that will start/stop/restart + all of the agents on this host. Add a kill method that stops all agents + and disconnects the host from other hosts. + """ + + def __init__(self, env_desc, host_desc, + test_name, neutron_config, + central_data_bridge, central_external_bridge): + self.env_desc = env_desc + self.host_desc = host_desc + self.test_name = test_name + self.neutron_config = neutron_config + self.central_data_bridge = central_data_bridge + self.central_external_bridge = central_external_bridge + self.host_namespace = None + self.agents = {} + # we need to cache already created "per network" bridges if linuxbridge + # agent is used on host: + self.network_bridges = {} + + def _setUp(self): + self.local_ip = self.allocate_local_ip() + + if self.host_desc.l2_agent_type == constants.AGENT_TYPE_OVS: + self.setup_host_with_ovs_agent() + elif self.host_desc.l2_agent_type == constants.AGENT_TYPE_LINUXBRIDGE: + self.setup_host_with_linuxbridge_agent() + if self.host_desc.l3_agent: + self.l3_agent = self.useFixture( + process.L3AgentFixture( + self.env_desc, self.host_desc, + self.test_name, + self.neutron_config, + self.l3_agent_cfg_fixture)) + + def setup_host_with_ovs_agent(self): + agent_cfg_fixture = config.OVSConfigFixture( + self.env_desc, self.host_desc, self.neutron_config.temp_dir, + self.local_ip) + self.useFixture(agent_cfg_fixture) + + br_phys = self.useFixture( + net_helpers.OVSBridgeFixture( + agent_cfg_fixture.get_br_phys_name())).bridge + if self.env_desc.tunneling_enabled: + self.useFixture( + net_helpers.OVSBridgeFixture( + agent_cfg_fixture.get_br_tun_name())).bridge + self.connect_to_internal_network_via_tunneling() + else: + self.connect_to_internal_network_via_vlans(br_phys) + + self.ovs_agent = self.useFixture( + process.OVSAgentFixture( + self.env_desc, self.host_desc, + self.test_name, self.neutron_config, agent_cfg_fixture)) + + if self.host_desc.l3_agent: + self.l3_agent_cfg_fixture = self.useFixture( + config.L3ConfigFixture( + self.env_desc, self.host_desc, + self.neutron_config.temp_dir, + self.ovs_agent.agent_cfg_fixture.get_br_int_name())) + + def setup_host_with_linuxbridge_agent(self): + #First we need to provide connectivity for agent to prepare proper + #bridge mappings in agent's config: + self.host_namespace = self.useFixture( + net_helpers.NamespaceFixture(prefix="host-") + ).name + + self.connect_namespace_to_control_network() + + agent_cfg_fixture = config.LinuxBridgeConfigFixture( + self.env_desc, self.host_desc, + self.neutron_config.temp_dir, + self.local_ip, + physical_device_name=self.host_port.name + ) + self.useFixture(agent_cfg_fixture) + + self.linuxbridge_agent = self.useFixture( + process.LinuxBridgeAgentFixture( + self.env_desc, self.host_desc, + self.test_name, self.neutron_config, agent_cfg_fixture, + namespace=self.host_namespace + ) + ) + + if self.host_desc.l3_agent: + self.l3_agent_cfg_fixture = self.useFixture( + config.L3ConfigFixture( + self.env_desc, self.host_desc, + self.neutron_config.temp_dir)) + + def _connect_ovs_port(self, cidr_address): + ovs_device = self.useFixture( + net_helpers.OVSPortFixture( + bridge=self.central_data_bridge, + namespace=self.host_namespace)).port + # NOTE: This sets an IP address on the host's root namespace + # which is cleaned up when the device is deleted. + ovs_device.addr.add(cidr_address) + return ovs_device + + def connect_namespace_to_control_network(self): + self.host_port = self._connect_ovs_port( + common_utils.ip_to_cidr(self.local_ip, 24) + ) + self.host_port.link.set_up() + + def connect_to_internal_network_via_tunneling(self): + veth_1, veth_2 = self.useFixture( + net_helpers.VethFixture()).ports + + # NOTE: This sets an IP address on the host's root namespace + # which is cleaned up when the device is deleted. + veth_1.addr.add(common_utils.ip_to_cidr(self.local_ip, 32)) + + veth_1.link.set_up() + veth_2.link.set_up() + + def connect_to_internal_network_via_vlans(self, host_data_bridge): + # If using VLANs as a segmentation device, it's needed to connect + # a provider bridge to a centralized, shared bridge. + net_helpers.create_patch_ports( + self.central_data_bridge, host_data_bridge) + + def connect_to_external_network(self, host_external_bridge): + net_helpers.create_patch_ports( + self.central_external_bridge, host_external_bridge) + + def allocate_local_ip(self): + if not self.env_desc.network_range: + return str(self.useFixture( + ip_address.ExclusiveIPAddress( + '240.0.0.1', '240.255.255.254')).address) + return str(self.useFixture( + ip_address.ExclusiveIPAddress( + str(self.env_desc.network_range[2]), + str(self.env_desc.network_range[-2]))).address) + + def get_bridge(self, network_id): + if "ovs" in self.agents.keys(): + return self.ovs_agent.br_int + elif "linuxbridge" in self.agents.keys(): + bridge = self.network_bridges.get(network_id, None) + if not bridge: + br_prefix = lb_agent.LinuxBridgeManager.get_bridge_name( + network_id) + bridge = self.useFixture( + net_helpers.LinuxBridgeFixture( + prefix=br_prefix, + namespace=self.host_namespace, + prefix_is_full_name=True)).bridge + self.network_bridges[network_id] = bridge + return bridge + + @property + def hostname(self): + return self.neutron_config.config.DEFAULT.host + + @property + def l3_agent(self): + return self.agents['l3'] + + @l3_agent.setter + def l3_agent(self, agent): + self.agents['l3'] = agent + + @property + def ovs_agent(self): + return self.agents['ovs'] + + @ovs_agent.setter + def ovs_agent(self, agent): + self.agents['ovs'] = agent + + @property + def linuxbridge_agent(self): + return self.agents['linuxbridge'] + + @linuxbridge_agent.setter + def linuxbridge_agent(self, agent): + self.agents['linuxbridge'] = agent + + +class Environment(fixtures.Fixture): + """Represents a deployment topology. + + Environment is a collection of hosts. It starts a Neutron server + and a parametrized number of Hosts, each a collection of agents. + The Environment accepts a collection of HostDescription, each describing + the type of Host to create. + """ + + def __init__(self, env_desc, hosts_desc): + """ + :param env_desc: An EnvironmentDescription instance. + :param hosts_desc: A list of HostDescription instances. + """ + + super(Environment, self).__init__() + self.env_desc = env_desc + self.hosts_desc = hosts_desc + self.hosts = [] + + def wait_until_env_is_up(self): + common_utils.wait_until_true(self._processes_are_ready) + + def _processes_are_ready(self): + try: + running_agents = self.neutron_server.client.list_agents()['agents'] + agents_count = sum(len(host.agents) for host in self.hosts) + return len(running_agents) == agents_count + except nc_exc.NeutronClientException: + return False + + def _create_host(self, host_desc): + temp_dir = self.useFixture(fixtures.TempDir()).path + neutron_config = config.NeutronConfigFixture( + self.env_desc, host_desc, temp_dir, + cfg.CONF.database.connection, self.rabbitmq_environment) + self.useFixture(neutron_config) + + return self.useFixture( + Host(self.env_desc, + host_desc, + self.test_name, + neutron_config, + self.central_data_bridge, + self.central_external_bridge)) + + def _setUp(self): + self.temp_dir = self.useFixture(fixtures.TempDir()).path + + #we need this bridge before rabbit and neutron service will start + self.central_data_bridge = self.useFixture( + net_helpers.OVSBridgeFixture('cnt-data')).bridge + self.central_external_bridge = self.useFixture( + net_helpers.OVSBridgeFixture('cnt-ex')).bridge + + #Get rabbitmq address (and cnt-data network) + rabbitmq_ip_address = self._configure_port_for_rabbitmq() + self.rabbitmq_environment = self.useFixture( + process.RabbitmqEnvironmentFixture(host=rabbitmq_ip_address) + ) + + plugin_cfg_fixture = self.useFixture( + config.ML2ConfigFixture( + self.env_desc, self.hosts_desc, self.temp_dir, + self.env_desc.network_type)) + neutron_cfg_fixture = self.useFixture( + config.NeutronConfigFixture( + self.env_desc, None, self.temp_dir, + cfg.CONF.database.connection, self.rabbitmq_environment)) + self.neutron_server = self.useFixture( + process.NeutronServerFixture( + self.env_desc, None, + self.test_name, neutron_cfg_fixture, plugin_cfg_fixture)) + + self.hosts = [self._create_host(desc) for desc in self.hosts_desc] + + self.wait_until_env_is_up() + + def _configure_port_for_rabbitmq(self): + self.env_desc.network_range = self._get_network_range() + if not self.env_desc.network_range: + return "127.0.0.1" + rabbitmq_ip = str(self.env_desc.network_range[1]) + rabbitmq_port = ip_lib.IPDevice(self.central_data_bridge.br_name) + rabbitmq_port.addr.add(common_utils.ip_to_cidr(rabbitmq_ip, 24)) + rabbitmq_port.link.set_up() + + return rabbitmq_ip + + def _get_network_range(self): + #NOTE(slaweq): We need to choose IP address on which rabbitmq will be + # available because LinuxBridge agents are spawned in their own + # namespaces and need to know where the rabbitmq server is listening. + # For ovs agent it is not necessary because agents are spawned in + # globalscope together with rabbitmq server so default localhost + # address is fine for them + for desc in self.hosts_desc: + if desc.l2_agent_type == constants.AGENT_TYPE_LINUXBRIDGE: + return self.useFixture( + ip_network.ExclusiveIPNetwork( + "240.0.0.0", "240.255.255.255", "24")).network diff --git a/neutron_fwaas/tests/fullstack/resources/machine.py b/neutron_fwaas/tests/fullstack/resources/machine.py new file mode 100644 index 000000000..e9eaecc1e --- /dev/null +++ b/neutron_fwaas/tests/fullstack/resources/machine.py @@ -0,0 +1,168 @@ +# Copyright 2015 Red Hat, 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 itertools + +import netaddr + +from neutron_lib import constants + +from neutron.agent.linux import ip_lib +from neutron.common import utils +from neutron.extensions import portbindings as pbs +from neutron.tests.common import machine_fixtures +from neutron.tests.common import net_helpers + + +class FakeFullstackMachinesList(list): + """A list of items implementing the FakeFullstackMachine interface.""" + + def block_until_all_boot(self): + for vm in self: + vm.block_until_boot() + + def ping_all(self): + # Generate an iterable of all unique pairs. For example: + # itertools.combinations(range(3), 2) results in: + # ((0, 1), (0, 2), (1, 2)) + for vm_1, vm_2 in itertools.combinations(self, 2): + vm_1.block_until_ping(vm_2.ip) + + +class FakeFullstackMachine(machine_fixtures.FakeMachineBase): + + def __init__(self, host, network_id, tenant_id, safe_client, + neutron_port=None, bridge_name=None): + super(FakeFullstackMachine, self).__init__() + self.host = host + self.tenant_id = tenant_id + self.network_id = network_id + self.safe_client = safe_client + self.neutron_port = neutron_port + self.bridge_name = bridge_name + + def _setUp(self): + super(FakeFullstackMachine, self)._setUp() + + self.bridge = self._get_bridge() + + if not self.neutron_port: + self.neutron_port = self.safe_client.create_port( + network_id=self.network_id, + tenant_id=self.tenant_id, + hostname=self.host.hostname) + mac_address = self.neutron_port['mac_address'] + hybrid_plug = self.neutron_port[pbs.VIF_DETAILS].get( + pbs.OVS_HYBRID_PLUG, False) + + self.bind_port_if_needed() + self.port = self.useFixture( + net_helpers.PortFixture.get( + self.bridge, self.namespace, mac_address, + self.neutron_port['id'], hybrid_plug)).port + + for fixed_ip in self.neutron_port['fixed_ips']: + self._configure_ipaddress(fixed_ip) + + def bind_port_if_needed(self): + if self.neutron_port[pbs.VIF_TYPE] == pbs.VIF_TYPE_UNBOUND: + self.safe_client.client.update_port( + self.neutron_port['id'], + {'port': {pbs.HOST_ID: self.host.hostname}}) + self.addCleanup(self.safe_client.client.update_port, + self.neutron_port['id'], + {'port': {pbs.HOST_ID: ''}}) + + def _get_bridge(self): + if self.bridge_name is None: + return self.host.get_bridge(self.network_id) + agent_type = self.host.host_desc.l2_agent_type + if agent_type == constants.AGENT_TYPE_OVS: + new_bridge = self.useFixture( + net_helpers.OVSTrunkBridgeFixture(self.bridge_name)).bridge + else: + raise NotImplementedError( + "Support for %s agent is not implemented." % agent_type) + + return new_bridge + + def _configure_ipaddress(self, fixed_ip): + if (netaddr.IPAddress(fixed_ip['ip_address']).version == + constants.IP_VERSION_6): + # v6Address/default_route is auto-configured. + self._ipv6 = fixed_ip['ip_address'] + else: + self._ip = fixed_ip['ip_address'] + subnet_id = fixed_ip['subnet_id'] + subnet = self.safe_client.client.show_subnet(subnet_id) + prefixlen = netaddr.IPNetwork(subnet['subnet']['cidr']).prefixlen + self._ip_cidr = '%s/%s' % (self._ip, prefixlen) + + # TODO(amuller): Support DHCP + self.port.addr.add(self.ip_cidr) + + self.gateway_ip = subnet['subnet']['gateway_ip'] + if self.gateway_ip: + net_helpers.set_namespace_gateway(self.port, self.gateway_ip) + + @property + def ipv6(self): + return self._ipv6 + + @property + def ip(self): + return self._ip + + @property + def ip_cidr(self): + return self._ip_cidr + + def block_until_boot(self): + utils.wait_until_true( + lambda: (self.safe_client.client.show_port(self.neutron_port['id']) + ['port']['status'] == 'ACTIVE'), + sleep=3) + + def destroy(self): + """Destroy this fake machine. + + This should simulate deletion of a vm. It doesn't call cleanUp(). + """ + self.safe_client.client.update_port( + self.neutron_port['id'], + {'port': {pbs.HOST_ID: ''}} + ) + # All associated vlan interfaces are deleted too + self.bridge.delete_port(self.port.name) + + ip_wrap = ip_lib.IPWrapper(self.namespace) + ip_wrap.netns.delete(self.namespace) + + +class FakeFullstackTrunkMachine(FakeFullstackMachine): + def __init__(self, trunk, *args, **kwargs): + super(FakeFullstackTrunkMachine, self).__init__(*args, **kwargs) + self.trunk = trunk + + def add_vlan_interface(self, mac_address, ip_address, segmentation_id): + """Add VLAN interface to VM's namespace. + + :param mac_address: MAC address to be set on VLAN interface. + :param ip_address: The IPNetwork instance containing IP address + assigned to the interface. + :param segmentation_id: VLAN tag added to the interface. + """ + net_helpers.create_vlan_interface( + self.namespace, self.port.name, mac_address, ip_address, + segmentation_id) diff --git a/neutron_fwaas/tests/fullstack/resources/process.py b/neutron_fwaas/tests/fullstack/resources/process.py new file mode 100644 index 000000000..0f98c888e --- /dev/null +++ b/neutron_fwaas/tests/fullstack/resources/process.py @@ -0,0 +1,235 @@ +# Copyright 2015 Red Hat, 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 datetime +from distutils import spawn +import os +import signal + +import fixtures +from neutronclient.common import exceptions as nc_exc +from neutronclient.v2_0 import client + +from neutron.agent.common import async_process +from neutron.agent.linux import utils +from neutron.common import utils as common_utils +from neutron.tests import base +from neutron.tests.common import net_helpers +from neutron.tests.fullstack import base as fullstack_base + + +class ProcessFixture(fixtures.Fixture): + def __init__(self, test_name, process_name, exec_name, config_filenames, + namespace=None, kill_signal=signal.SIGKILL): + super(ProcessFixture, self).__init__() + self.test_name = test_name + self.process_name = process_name + self.exec_name = exec_name + self.config_filenames = config_filenames + self.process = None + self.kill_signal = kill_signal + self.namespace = namespace + + def _setUp(self): + self.start() + self.addCleanup(self.stop) + + def start(self): + test_name = base.sanitize_log_path(self.test_name) + + log_dir = os.path.join(fullstack_base.DEFAULT_LOG_DIR, test_name) + common_utils.ensure_dir(log_dir) + + timestamp = datetime.datetime.now().strftime("%Y-%m-%d--%H-%M-%S-%f") + log_file = "%s--%s.log" % (self.process_name, timestamp) + cmd = [spawn.find_executable(self.exec_name), + '--log-dir', log_dir, + '--log-file', log_file] + for filename in self.config_filenames: + cmd += ['--config-file', filename] + run_as_root = bool(self.namespace) + self.process = async_process.AsyncProcess( + cmd, run_as_root=run_as_root, namespace=self.namespace + ) + self.process.start(block=True) + + def stop(self): + self.process.stop(block=True, kill_signal=self.kill_signal) + + +class RabbitmqEnvironmentFixture(fixtures.Fixture): + + def __init__(self, host="127.0.0.1"): + super(RabbitmqEnvironmentFixture, self).__init__() + self.host = host + + def _setUp(self): + self.user = common_utils.get_rand_name(prefix='user') + self.password = common_utils.get_rand_name(prefix='pass') + self.vhost = common_utils.get_rand_name(prefix='vhost') + + self._execute('add_user', self.user, self.password) + self.addCleanup(self._execute, 'delete_user', self.user) + + self._execute('add_vhost', self.vhost) + self.addCleanup(self._execute, 'delete_vhost', self.vhost) + + self._execute('set_permissions', '-p', self.vhost, self.user, + '.*', '.*', '.*') + + def _execute(self, *args): + cmd = ['rabbitmqctl'] + cmd.extend(args) + utils.execute(cmd, run_as_root=True) + + +class NeutronServerFixture(fixtures.Fixture): + + NEUTRON_SERVER = "neutron-server" + + def __init__(self, env_desc, host_desc, + test_name, neutron_cfg_fixture, plugin_cfg_fixture): + super(NeutronServerFixture, self).__init__() + self.env_desc = env_desc + self.host_desc = host_desc + self.test_name = test_name + self.neutron_cfg_fixture = neutron_cfg_fixture + self.plugin_cfg_fixture = plugin_cfg_fixture + + def _setUp(self): + config_filenames = [self.neutron_cfg_fixture.filename, + self.plugin_cfg_fixture.filename] + + self.process_fixture = self.useFixture(ProcessFixture( + test_name=self.test_name, + process_name=self.NEUTRON_SERVER, + exec_name=self.NEUTRON_SERVER, + config_filenames=config_filenames, + kill_signal=signal.SIGTERM)) + + common_utils.wait_until_true(self.server_is_live) + + def server_is_live(self): + try: + self.client.list_networks() + return True + except nc_exc.NeutronClientException: + return False + + @property + def client(self): + url = ("http://127.0.0.1:%s" % + self.neutron_cfg_fixture.config.DEFAULT.bind_port) + return client.Client(auth_strategy="noauth", endpoint_url=url) + + +class OVSAgentFixture(fixtures.Fixture): + + NEUTRON_OVS_AGENT = "neutron-openvswitch-agent" + + def __init__(self, env_desc, host_desc, + test_name, neutron_cfg_fixture, agent_cfg_fixture): + super(OVSAgentFixture, self).__init__() + self.env_desc = env_desc + self.host_desc = host_desc + self.test_name = test_name + self.neutron_cfg_fixture = neutron_cfg_fixture + self.neutron_config = self.neutron_cfg_fixture.config + self.agent_cfg_fixture = agent_cfg_fixture + self.agent_config = agent_cfg_fixture.config + + def _setUp(self): + self.br_int = self.useFixture( + net_helpers.OVSBridgeFixture( + self.agent_cfg_fixture.get_br_int_name())).bridge + + config_filenames = [self.neutron_cfg_fixture.filename, + self.agent_cfg_fixture.filename] + + self.process_fixture = self.useFixture(ProcessFixture( + test_name=self.test_name, + process_name=self.NEUTRON_OVS_AGENT, + exec_name=spawn.find_executable( + 'ovs_agent.py', + path=os.path.join(base.ROOTDIR, 'common', 'agents')), + config_filenames=config_filenames, + kill_signal=signal.SIGTERM)) + + +class LinuxBridgeAgentFixture(fixtures.Fixture): + + NEUTRON_LINUXBRIDGE_AGENT = "neutron-linuxbridge-agent" + + def __init__(self, env_desc, host_desc, test_name, + neutron_cfg_fixture, agent_cfg_fixture, + namespace=None): + super(LinuxBridgeAgentFixture, self).__init__() + self.env_desc = env_desc + self.host_desc = host_desc + self.test_name = test_name + self.neutron_cfg_fixture = neutron_cfg_fixture + self.neutron_config = self.neutron_cfg_fixture.config + self.agent_cfg_fixture = agent_cfg_fixture + self.agent_config = agent_cfg_fixture.config + self.namespace = namespace + + def _setUp(self): + config_filenames = [self.neutron_cfg_fixture.filename, + self.agent_cfg_fixture.filename] + + self.process_fixture = self.useFixture( + ProcessFixture( + test_name=self.test_name, + process_name=self.NEUTRON_LINUXBRIDGE_AGENT, + exec_name=self.NEUTRON_LINUXBRIDGE_AGENT, + config_filenames=config_filenames, + namespace=self.namespace + ) + ) + + +class L3AgentFixture(fixtures.Fixture): + + NEUTRON_L3_AGENT = "neutron-l3-agent" + + def __init__(self, env_desc, host_desc, test_name, + neutron_cfg_fixture, l3_agent_cfg_fixture, + namespace=None): + super(L3AgentFixture, self).__init__() + self.env_desc = env_desc + self.host_desc = host_desc + self.test_name = test_name + self.neutron_cfg_fixture = neutron_cfg_fixture + self.l3_agent_cfg_fixture = l3_agent_cfg_fixture + self.namespace = namespace + + def _setUp(self): + self.plugin_config = self.l3_agent_cfg_fixture.config + + config_filenames = [self.neutron_cfg_fixture.filename, + self.l3_agent_cfg_fixture.filename] + self.process_fixture = self.useFixture( + ProcessFixture( + test_name=self.test_name, + process_name=self.NEUTRON_L3_AGENT, + exec_name=spawn.find_executable( + 'l3_agent.py', + path=os.path.join(base.ROOTDIR, 'common', 'agents')), + config_filenames=config_filenames, + namespace=self.namespace + ) + ) + + def get_namespace_suffix(self): + return self.plugin_config.DEFAULT.test_namespace_suffix diff --git a/neutron_fwaas/tests/fullstack/test_l3_agent.py b/neutron_fwaas/tests/fullstack/test_l3_agent.py new file mode 100644 index 000000000..58d1ea02c --- /dev/null +++ b/neutron_fwaas/tests/fullstack/test_l3_agent.py @@ -0,0 +1,183 @@ +# Copyright 2015 Red Hat, 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 functools + +import netaddr + +from neutron_lib import constants +from oslo_utils import uuidutils + +from neutron.agent.l3 import agent as l3_agent +from neutron.agent.l3 import namespaces +from neutron.agent.linux import ip_lib +from neutron.common import utils as common_utils +from neutron.tests.common.exclusive_resources import ip_network +from neutron.tests.common import machine_fixtures +from neutron.tests.fullstack import base +from neutron.tests.fullstack.resources import environment +from neutron.tests.fullstack.resources import machine +from neutron.tests.unit import testlib_api + +load_tests = testlib_api.module_load_tests + + +class TestL3Agent(base.BaseFullStackTestCase): + + def _create_external_network_and_subnet(self, tenant_id): + network = self.safe_client.create_network( + tenant_id, name='public', external=True) + cidr = self.useFixture( + ip_network.ExclusiveIPNetwork( + "240.0.0.0", "240.255.255.255", "24")).network + subnet = self.safe_client.create_subnet( + tenant_id, network['id'], cidr, + enable_dhcp=False) + return network, subnet + + def block_until_port_status_active(self, port_id): + def is_port_status_active(): + port = self.client.show_port(port_id) + return port['port']['status'] == 'ACTIVE' + common_utils.wait_until_true(lambda: is_port_status_active(), sleep=1) + + def _create_net_subnet_and_vm(self, tenant_id, subnet_cidrs, host, router): + network = self.safe_client.create_network(tenant_id) + for cidr in subnet_cidrs: + # For IPv6 subnets, enable_dhcp should be set to true. + enable_dhcp = (netaddr.IPNetwork(cidr).version == + constants.IP_VERSION_6) + subnet = self.safe_client.create_subnet( + tenant_id, network['id'], cidr, enable_dhcp=enable_dhcp) + + router_interface_info = self.safe_client.add_router_interface( + router['id'], subnet['id']) + self.block_until_port_status_active( + router_interface_info['port_id']) + + vm = self.useFixture( + machine.FakeFullstackMachine( + host, network['id'], tenant_id, self.safe_client)) + vm.block_until_boot() + return vm + + +class TestLegacyL3Agent(TestL3Agent): + + def setUp(self): + host_descriptions = [ + environment.HostDescription(l3_agent=True), + environment.HostDescription()] + env = environment.Environment( + environment.EnvironmentDescription( + network_type='vlan', l2_pop=False), + host_descriptions) + super(TestLegacyL3Agent, self).setUp(env) + + def _get_namespace(self, router_id): + return namespaces.build_ns_name(l3_agent.NS_PREFIX, router_id) + + def _assert_namespace_exists(self, ns_name): + ip = ip_lib.IPWrapper(ns_name) + common_utils.wait_until_true(lambda: ip.netns.exists(ns_name)) + + def test_namespace_exists(self): + tenant_id = uuidutils.generate_uuid() + + router = self.safe_client.create_router(tenant_id) + network = self.safe_client.create_network(tenant_id) + subnet = self.safe_client.create_subnet( + tenant_id, network['id'], '20.0.0.0/24', gateway_ip='20.0.0.1') + self.safe_client.add_router_interface(router['id'], subnet['id']) + + namespace = "%s@%s" % ( + self._get_namespace(router['id']), + self.environment.hosts[0].l3_agent.get_namespace_suffix(), ) + self._assert_namespace_exists(namespace) + + def test_east_west_traffic(self): + tenant_id = uuidutils.generate_uuid() + router = self.safe_client.create_router(tenant_id) + + vm1 = self._create_net_subnet_and_vm( + tenant_id, ['20.0.0.0/24', '2001:db8:aaaa::/64'], + self.environment.hosts[0], router) + vm2 = self._create_net_subnet_and_vm( + tenant_id, ['21.0.0.0/24', '2001:db8:bbbb::/64'], + self.environment.hosts[1], router) + + vm1.block_until_ping(vm2.ip) + # Verify ping6 from vm2 to vm1 IPv6 Address + vm2.block_until_ping(vm1.ipv6) + + def test_snat_and_floatingip(self): + # This function creates external network and boots an extrenal vm + # on it with gateway ip and connected to central_external_bridge. + # Later it creates a tenant vm on tenant network, with tenant router + # connected to tenant network and external network. + # To test snat and floatingip, try ping between tenant and external vms + tenant_id = uuidutils.generate_uuid() + ext_net, ext_sub = self._create_external_network_and_subnet(tenant_id) + external_vm = self.useFixture( + machine_fixtures.FakeMachine( + self.environment.central_external_bridge, + common_utils.ip_to_cidr(ext_sub['gateway_ip'], 24))) + + router = self.safe_client.create_router(tenant_id, + external_network=ext_net['id']) + vm = self._create_net_subnet_and_vm( + tenant_id, ['20.0.0.0/24'], + self.environment.hosts[1], router) + + # ping external vm to test snat + vm.block_until_ping(external_vm.ip) + + fip = self.safe_client.create_floatingip( + tenant_id, ext_net['id'], vm.ip, vm.neutron_port['id']) + + # ping floating ip from external vm + external_vm.block_until_ping(fip['floating_ip_address']) + + +class TestHAL3Agent(base.BaseFullStackTestCase): + + def setUp(self): + host_descriptions = [ + environment.HostDescription(l3_agent=True) for _ in range(2)] + env = environment.Environment( + environment.EnvironmentDescription( + network_type='vxlan', l2_pop=True), + host_descriptions) + super(TestHAL3Agent, self).setUp(env) + + def _is_ha_router_active_on_one_agent(self, router_id): + agents = self.client.list_l3_agent_hosting_routers(router_id) + return ( + agents['agents'][0]['ha_state'] != agents['agents'][1]['ha_state']) + + def test_ha_router(self): + # TODO(amuller): Test external connectivity before and after a + # failover, see: https://review.openstack.org/#/c/196393/ + + tenant_id = uuidutils.generate_uuid() + router = self.safe_client.create_router(tenant_id, ha=True) + agents = self.client.list_l3_agent_hosting_routers(router['id']) + self.assertEqual(2, len(agents['agents']), + 'HA router must be scheduled to both nodes') + + common_utils.wait_until_true( + functools.partial( + self._is_ha_router_active_on_one_agent, + router['id']), + timeout=90) diff --git a/neutron_fwaas/tests/fullstack/utils.py b/neutron_fwaas/tests/fullstack/utils.py new file mode 100644 index 000000000..5a0f9623c --- /dev/null +++ b/neutron_fwaas/tests/fullstack/utils.py @@ -0,0 +1,24 @@ +# 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. + + +def get_ovs_interface_scenarios(): + return [ + ('openflow-cli_ovsdb-cli', {'of_interface': 'ovs-ofctl', + 'ovsdb_interface': 'vsctl'}), + ('openflow-native_ovsdb-cli', {'of_interface': 'native', + 'ovsdb_interface': 'vsctl'}), + ('openflow-cli_ovsdb-native', {'of_interface': 'ovs-ofctl', + 'ovsdb_interface': 'native'}), + ('openflow-native_ovsdb-native', {'of_interface': 'native', + 'ovsdb_interface': 'native'}), + ] diff --git a/neutron_fwaas/tests/functional/__init__.py b/neutron_fwaas/tests/functional/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/functional/db/__init__.py b/neutron_fwaas/tests/functional/db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/functional/db/test_migrations.py b/neutron_fwaas/tests/functional/db/test_migrations.py new file mode 100644 index 000000000..9b3fcb3f0 --- /dev/null +++ b/neutron_fwaas/tests/functional/db/test_migrations.py @@ -0,0 +1,100 @@ +# 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 alembic import script as alembic_script +from neutron.db.migration.alembic_migrations import external +from neutron.db.migration import cli as migration +from neutron.tests.functional.db import test_migrations +from neutron.tests.unit import testlib_api +from oslo_config import cfg +import sqlalchemy + +from neutron_fwaas.db.models import head + +# EXTERNAL_TABLES should contain all names of tables that are not related to +# current repo. +EXTERNAL_TABLES = set(external.TABLES) +# Model moved to vendor repo +EXTERNAL_TABLES.update({'cisco_firewall_associations'}) +EXTERNAL_TABLES.update({'firewall_router_associations'}) + +VERSION_TABLE = 'alembic_version_fwaas' + + +class _TestModelsMigrationsFWaaS(test_migrations._TestModelsMigrations): + + def db_sync(self, engine): + cfg.CONF.set_override('connection', engine.url, group='database') + for conf in migration.get_alembic_configs(): + self.alembic_config = conf + self.alembic_config.neutron_config = cfg.CONF + migration.do_alembic_command(conf, 'upgrade', 'heads') + + def get_metadata(self): + return head.get_metadata() + + def include_object(self, object_, name, type_, reflected, compare_to): + if type_ == 'table' and (name.startswith('alembic') or + name == VERSION_TABLE or + name in EXTERNAL_TABLES): + return False + if type_ == 'index' and reflected and name.startswith("idx_autoinc_"): + return False + return True + + +class TestModelsMigrationsMysql(testlib_api.MySQLTestCaseMixin, + _TestModelsMigrationsFWaaS, + testlib_api.SqlTestCaseLight): + pass + + +class TestModelsMigrationsPostgresql(testlib_api.PostgreSQLTestCaseMixin, + _TestModelsMigrationsFWaaS, + testlib_api.SqlTestCaseLight): + pass + + +class TestSanityCheck(testlib_api.SqlTestCaseLight): + BUILD_SCHEMA = False + + def setUp(self): + super(TestSanityCheck, self).setUp() + + for conf in migration.get_alembic_configs(): + self.alembic_config = conf + self.alembic_config.neutron_config = cfg.CONF + + def _drop_table(self, table): + with self.engine.begin() as conn: + table.drop(conn) + + def test_check_sanity_f24e0d5e5bff(self): + current_revision = "f24e0d5e5bff" + fwg_port_association = sqlalchemy.Table( + 'firewall_group_port_associations_v2', sqlalchemy.MetaData(), + sqlalchemy.Column('firewall_group_id', sqlalchemy.String(36)), + sqlalchemy.Column('port_id', sqlalchemy.String(36))) + + with self.engine.connect() as conn: + fwg_port_association.create(conn) + self.addCleanup(self._drop_table, fwg_port_association) + conn.execute(fwg_port_association.insert(), [ + {'firewall_group_id': '1234', 'port_id': '12345'}, + {'firewall_group_id': '12343', 'port_id': '12345'} + ]) + script_dir = alembic_script.ScriptDirectory.from_config( + self.alembic_config) + script = script_dir.get_revision(current_revision).module + self.assertRaises( + script.DuplicatePortRecordinFirewallGroupPortAssociation, + script.check_sanity, conn) diff --git a/neutron_fwaas/tests/functional/privileged/__init__.py b/neutron_fwaas/tests/functional/privileged/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/functional/privileged/test_dummy.py b/neutron_fwaas/tests/functional/privileged/test_dummy.py new file mode 100644 index 000000000..cdcb0ff6b --- /dev/null +++ b/neutron_fwaas/tests/functional/privileged/test_dummy.py @@ -0,0 +1,24 @@ +# Copyright (c) 2017 Thales Services SAS +# 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. + +from neutron.tests.functional import base + +from neutron_fwaas.privileged.tests.functional import dummy + + +class DummyTest(base.BaseSudoTestCase): + + def test_dummy(self): + dummy.dummy() diff --git a/neutron_fwaas/tests/functional/privileged/test_netlink_lib.py b/neutron_fwaas/tests/functional/privileged/test_netlink_lib.py new file mode 100644 index 000000000..5d34f8bac --- /dev/null +++ b/neutron_fwaas/tests/functional/privileged/test_netlink_lib.py @@ -0,0 +1,188 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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. + +from neutron.agent.linux import utils as linux_utils +from neutron.tests.common import net_helpers +from neutron.tests.functional import base as functional_base +from oslo_log import log as logging + +import neutron_fwaas.privileged.netlink_lib as nl_lib + +LOG = logging.getLogger(__name__) + + +def check_nf_conntrack_ipv6_is_loaded(): + try: + output = linux_utils.execute(['lsmod']) + except RuntimeError: + msg = "Failed execute command lsmod!" + raise RuntimeError(msg) + if 'nf_conntrack' in output: + return True + return False + + +def _create_entries(namespace, conntrack_cmds): + for cmd in conntrack_cmds: + exec_cmd = ['ip', 'netns', 'exec', namespace] + cmd + try: + linux_utils.execute(exec_cmd, + run_as_root=True, + check_exit_code=True, + extra_ok_codes=[1], + privsep_exec=True) + except RuntimeError: + raise Exception('Error while creating entry') + + +class NetlinkLibTestCase(functional_base.BaseSudoTestCase): + """Functional test for netlink_lib: List, delete, flush conntrack entries. + + For each function, first we add a specific namespace, then create real + conntrack entries. netlink_lib function will do list, delete and flush + these entries. This class will test this netlink_lib function work + as expected. + """ + + CONNTRACK_CMDS = ( + ['conntrack', '-I', '-p', 'tcp', + '-s', '1.1.1.1', '-d', '2.2.2.2', + '--sport', '1', '--dport', '2', + '--state', 'ESTABLISHED', '--timeout', '1234'], + ['conntrack', '-I', '-p', 'udp', + '-s', '1.1.1.1', '-d', '2.2.2.2', + '--sport', '1', '--dport', '2', + '--timeout', '1234'], + ['conntrack', '-I', '-p', 'icmp', + '-s', '1.1.1.1', '-d', '2.2.2.2', + '--icmp-type', '8', '--icmp-code', '0', '--icmp-id', '3333', + '--timeout', '1234'], + ['conntrack', '-I', '-p', 'icmp', + '-s', '1.1.1.1', '-d', '2.2.2.2', + '--icmp-type', '8', '--icmp-code', '0', '--icmp-id', '3333', + '--timeout', '1234'], + ) + + def test_list_entries(self): + namespace = self.useFixture(net_helpers.NamespaceFixture()).name + _create_entries(namespace, self.CONNTRACK_CMDS) + expected = ( + (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333), + (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2'), + (4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2') + ) + entries_list = nl_lib.list_entries(namespace=namespace) + self.assertEqual(expected, entries_list) + + def _delete_entry(self, delete_entries, remain_entries): + namespace = self.useFixture(net_helpers.NamespaceFixture()).name + _create_entries(namespace, self.CONNTRACK_CMDS) + nl_lib.delete_entries(namespace=namespace, entries=delete_entries) + entries_list = nl_lib.list_entries(namespace) + self.assertEqual(remain_entries, entries_list) + + def test_delete_icmp_entry(self): + icmp_entry = [(4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333)] + remain_entries = ( + (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2'), + (4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2'), + ) + self._delete_entry(icmp_entry, remain_entries) + + def test_delete_tcp_entry(self): + tcp_entry = [(4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2')] + remain_entries = ( + (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333), + (4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2') + ) + self._delete_entry(tcp_entry, remain_entries) + + def test_delete_udp_entry(self): + udp_entry = [(4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2')] + remain_entries = ( + (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333), + (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2') + ) + self._delete_entry(udp_entry, remain_entries) + + def test_delete_multiple_entries(self): + delete_entries = ( + (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333), + (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2'), + (4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2') + ) + remain_entries = () + self._delete_entry(delete_entries, remain_entries) + + def test_flush_entries(self): + namespace = self.useFixture(net_helpers.NamespaceFixture()).name + _create_entries(namespace, self.CONNTRACK_CMDS) + nl_lib.flush_entries(namespace) + entries_list = nl_lib.list_entries(namespace) + self.assertEqual((), entries_list) + + +class NetlinkLibTestCaseIPv6(functional_base.BaseSudoTestCase): + + CONNTRACK_CMDS = ( + ['conntrack', '-I', '-p', 'icmp', + '-s', '1.1.1.1', '-d', '2.2.2.2', + '--icmp-type', '8', '--icmp-code', '0', '--icmp-id', '3333', + '--timeout', '1234'], + ['conntrack', '-I', '-p', 'icmpv6', + '-s', '10::10', '-d', '20::20', + '--icmpv6-type', '128', '--icmpv6-code', '0', '--icmpv6-id', '3456', + '--timeout', '1234'], + ) + + def setUp(self): + super(NetlinkLibTestCaseIPv6, self).setUp() + if not check_nf_conntrack_ipv6_is_loaded(): + self.skipTest( + "nf_conntrack_ipv6 module wasn't loaded. Please load" + "this module into your system if you want to use " + "netlink conntrack with ipv6" + ) + + def test_list_entries(self): + namespace = self.useFixture(net_helpers.NamespaceFixture()).name + _create_entries(namespace, self.CONNTRACK_CMDS) + expected = ( + (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333), + (6, 'icmpv6', 128, 0, '10::10', '20::20', 3456), + ) + entries_list = nl_lib.list_entries(namespace=namespace) + self.assertEqual(expected, entries_list) + + def _delete_entry(self, delete_entries, remain_entries): + namespace = self.useFixture(net_helpers.NamespaceFixture()).name + _create_entries(namespace, self.CONNTRACK_CMDS) + nl_lib.delete_entries(namespace=namespace, entries=delete_entries) + entries_list = nl_lib.list_entries(namespace) + self.assertEqual(remain_entries, entries_list) + + def test_delete_icmpv6_entry(self): + icmp_entry = [(6, 'icmpv6', 128, 0, '10::10', '20::20', 3456)] + remain_entries = ( + (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', 3333), + ) + self._delete_entry(icmp_entry, remain_entries) + + def test_flush_entries(self): + namespace = self.useFixture(net_helpers.NamespaceFixture()).name + _create_entries(namespace, self.CONNTRACK_CMDS) + nl_lib.flush_entries(namespace) + entries_list = nl_lib.list_entries(namespace) + self.assertEqual((), entries_list) diff --git a/neutron_fwaas/tests/functional/privileged/test_utils.py b/neutron_fwaas/tests/functional/privileged/test_utils.py new file mode 100644 index 000000000..3d62d4473 --- /dev/null +++ b/neutron_fwaas/tests/functional/privileged/test_utils.py @@ -0,0 +1,53 @@ +# Copyright (c) 2017 Thales Services SAS +# 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. + +from neutron.agent.linux import ip_lib +from neutron.common import utils as neutron_utils +from neutron.tests.common import net_helpers +from neutron.tests.functional import base + +from neutron_fwaas.privileged.tests.functional import utils + + +class InNamespaceTest(base.BaseSudoTestCase): + + def setUp(self): + super(InNamespaceTest, self).setUp() + self.namespace = self.useFixture(net_helpers.NamespaceFixture()).name + + ip = ip_lib.IPWrapper() + root_dev_name = neutron_utils.get_rand_device_name() + netns_dev_name = neutron_utils.get_rand_device_name() + self.root_dev, self.netns_dev = ip.add_veth( + root_dev_name, netns_dev_name, namespace2=self.namespace) + self.addCleanup(self.root_dev.link.delete) + + def test_in_namespace(self): + before, observed, after = utils.get_in_namespace_interfaces( + self.namespace) + expected = ['lo', self.netns_dev.name] + self.assertItemsEqual(expected, observed) + # Other tests can create/delete devices, so we just checks + # self.root_dev_name is included in the root namespace result. + self.assertIn(self.root_dev.name, before) + self.assertIn(self.root_dev.name, after) + + def test_in_no_namespace(self): + before, observed, after = utils.get_in_namespace_interfaces(None) + # Other tests can create/delete devices, so we just checks + # self.root_dev_name is included in the root namespace result. + self.assertIn(self.root_dev.name, observed) + self.assertIn(self.root_dev.name, before) + self.assertIn(self.root_dev.name, after) diff --git a/neutron_fwaas/tests/functional/services/__init__.py b/neutron_fwaas/tests/functional/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/functional/services/logapi/__init__.py b/neutron_fwaas/tests/functional/services/logapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/functional/services/logapi/agents/__init__.py b/neutron_fwaas/tests/functional/services/logapi/agents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/functional/services/logapi/agents/drivers/__init__.py b/neutron_fwaas/tests/functional/services/logapi/agents/drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/functional/services/logapi/agents/drivers/iptables/__init__.py b/neutron_fwaas/tests/functional/services/logapi/agents/drivers/iptables/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/functional/services/logapi/agents/drivers/iptables/test_log.py b/neutron_fwaas/tests/functional/services/logapi/agents/drivers/iptables/test_log.py new file mode 100644 index 000000000..cf82dfdd9 --- /dev/null +++ b/neutron_fwaas/tests/functional/services/logapi/agents/drivers/iptables/test_log.py @@ -0,0 +1,568 @@ +# Copyright (c) 2018 Fujitsu Limited. +# 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. + +import time + +import mock +from neutron.agent.l3 import l3_agent_extension_api as l3_ext_api +from neutron.agent.linux import utils as linux_utils +from neutron.tests.functional.agent.l3 import framework +from neutron_lib import constants +from neutron_lib import context as neutron_context +from neutron_lib.services.logapi import constants as log_const +from oslo_config import cfg +from oslo_log import log as logging + +from neutron_fwaas.services.logapi.agents.drivers.iptables import log + +LOG = logging.getLogger(__name__) + +FAKE_LOG_ID = 'fake_log_id' +FAKE_PROJECT_ID = 'fake_project_id' +FAKE_RESOURCE_TYPE = 'firewall_group' + +# Default chain name +ACCEPTED_CHAIN = 'accepted' +DROPPED_CHAIN = 'dropped' +REJECTED_CHAIN = 'rejected' + +ACCEPT = 'ACCEPT' +DROP = 'DROP' +REJECT = 'REJECT' +ALL = 'ALL' + +CHAIN_NAME_POSTFIX_MAP = { + ACCEPT: ACCEPTED_CHAIN, + DROP: DROPPED_CHAIN, + REJECT: REJECTED_CHAIN +} + +FWAAS_V2_LOG_OPTS = [ + cfg.StrOpt('extensions', default=['fwaas_v2', 'fwaas_v2_log']), +] + +AGENT_MODE_OPTS = [ + cfg.StrOpt('agent_mode', default='legacy', + choices=['legacy', 'dvr', 'dvr_snat', 'dvr_no_external']), +] + + +class FWLoggingTestBase(framework.L3AgentTestFramework): + + def setUp(self): + super(FWLoggingTestBase, self).setUp() + self.conf.register_opts(FWAAS_V2_LOG_OPTS, 'fwaas') + self.conf.register_opts(AGENT_MODE_OPTS, group='DEFAULT') + self._set_agent_mode(self.conf) + self.if_prefix = 'qr-' + + self.context = neutron_context.get_admin_context() + self.context.tenant_id = FAKE_PROJECT_ID + self.resource_rpc = mock.patch( + 'neutron.services.logapi.rpc.agent.LoggingApiStub').start() + # Initialize logging driver + self.log_driver = self._initialize_iptables_log() + # Prepare router_info + self._prepare_router_info(n_ports=2) + + def _prepare_router_info(self, n_ports=0): + router_data = self.generate_router_info(enable_ha=False, + num_internal_ports=n_ports) + + self.router_info = self.manage_router(self.agent, router_data) + self.log_driver.agent_api._router_info = { + self.router_info.router_id: self.router_info + } + + def _initialize_iptables_log(self): + self.agent_api = l3_ext_api.L3AgentExtensionAPI({}, None) + log_driver = log.IptablesLoggingDriver(self.agent_api) + log_driver.initialize(self.resource_rpc) + log_driver.conf = self.conf + return log_driver + + def _refresh_logging_config(self, ipt_mgr): + # Reset configuration for the next testing EVENT + self.log_driver.ipt_mgr_list.clear() + self.log_driver.fwg_port_logs.clear() + self.log_driver.prefixes_table.clear() + self.log_driver.cleanup_table.clear() + self.log_driver.nflog_proc_map.clear() + self.log_driver.unused_port_ids.clear() + # Empty default chains + self._empty_default_chains_v4v6(ipt_mgr=ipt_mgr) + + def _set_agent_mode(self, cfg, agent_mode='legacy'): + cfg.agent_mode = agent_mode + + def _config_default_chains_v4v6(self, ipt_mgr): + # Config default chains in iptables and ip6tables + for action, chain in CHAIN_NAME_POSTFIX_MAP.items(): + v4rules_in_chain = \ + ipt_mgr.get_chain("filter", chain, ip_version=4) + if not v4rules_in_chain: + ipt_mgr.ipv4['filter'].add_chain(chain) + ipt_mgr.ipv4['filter'].add_rule(chain, '-j %s' % action) + + v6rules_in_chain = \ + ipt_mgr.get_chain("filter", chain, ip_version=6) + if not v6rules_in_chain: + ipt_mgr.ipv6['filter'].add_chain(chain) + ipt_mgr.ipv6['filter'].add_rule(chain, '-j %s' % action) + + def _empty_default_chains_v4v6(self, ipt_mgr): + # Empty default chains in iptables and ip6tables + for action, chain in CHAIN_NAME_POSTFIX_MAP.items(): + ipt_mgr.ipv4['filter'].empty_chain(chain=chain) + ipt_mgr.ipv6['filter'].empty_chain(chain=chain) + + def _fake_log_resource(self, tenant_id, resource_id=None, + target_id=None, event='ALL', enabled=True): + log_resource = { + 'id': FAKE_LOG_ID, + 'name': 'fake_log_name', + 'resource_type': FAKE_RESOURCE_TYPE, + 'project_id': tenant_id, + 'event': event, + 'enabled': True} + if resource_id: + log_resource['resource_id'] = resource_id + if target_id: + log_resource['target_id'] = target_id + if not enabled: + log_resource['enabled'] = enabled + return log_resource + + def _fake_log_info(self, log_id, port_ids, event='ALL'): + return { + 'event': event, + 'id': log_id, + 'project_id': FAKE_PROJECT_ID, + 'ports_log': port_ids + } + + def _get_expected_nflog_rule(self, wrap_name, if_prefix, logs_info): + # Generate an expected NFLOG rules from given log_info + rules = set() + limit = 'limit --limit %s/sec --limit-burst %s' % \ + (self.log_driver.rate_limit, self.log_driver.burst_limit) + + accept_chain = wrap_name + '-' + ACCEPTED_CHAIN + drop_chain = wrap_name + '-' + DROPPED_CHAIN + reject_chain = wrap_name + '-' + REJECTED_CHAIN + for log_info in logs_info: + event = log_info['event'] + ports_log = log_info['ports_log'] + + for port_id in ports_log: + device = (if_prefix + port_id)[:constants.LINUX_DEV_LEN] + if event in [ACCEPT, ALL]: + # Generate iptables rules for ACCEPT action + prefix = self._get_log_prefix(port_id, ACCEPT) + rules.add('-A %s -i %s -m %s -j NFLOG --nflog-prefix %s' + % (accept_chain, device, limit, prefix.id)) + rules.add('-A %s -o %s -m %s -j NFLOG --nflog-prefix %s' + % (accept_chain, device, limit, prefix.id)) + + if event in [DROP, ALL]: + # Generate iptables rules for DROP action + prefix = self._get_log_prefix(port_id, DROP) + rules.add('-A %s -i %s -m %s -j NFLOG --nflog-prefix %s' + % (drop_chain, device, limit, prefix.id)) + rules.add('-A %s -o %s -m %s -j NFLOG --nflog-prefix %s' + % (drop_chain, device, limit, prefix.id)) + + # Generate iptables rules for REJECT action + rules.add('-A %s -i %s -m %s -j NFLOG --nflog-prefix %s' + % (reject_chain, device, limit, prefix.id)) + rules.add('-A %s -o %s -m %s -j NFLOG --nflog-prefix %s' + % (reject_chain, device, limit, prefix.id)) + return rules + + def _get_log_prefix(self, port_id, action): + prefix_table = self.log_driver.prefixes_table + if port_id in prefix_table: + for prefix in prefix_table[port_id]: + if prefix.action == action: + return prefix + return None + + def _get_nflog_entries(self, namespace, table='iptables', chain_name=None): + # Get NFLOG entries from iptables and ip6tables + exec_cmd = ['ip', 'netns', 'exec', namespace, table, '-S'] + if chain_name: + exec_cmd += [chain_name] + while True: + try: + output = linux_utils.execute(exec_cmd, + run_as_root=True, + check_exit_code=True, + extra_ok_codes=[1], + privsep_exec=True) + nflog_rules = [rule for rule in output.splitlines() + if 'NFLOG' in rule] + return nflog_rules + except RuntimeError: + time.sleep(1) + + def assert_logging_results(self, ipt_mgr, log_info): + # Comparing between expected NFLOG rules and NFLOG rules from iptables + v4_rules = v6_rules = self._get_expected_nflog_rule( + ipt_mgr.wrap_name, self.if_prefix, log_info) + + v4_actual = self._get_nflog_entries(ipt_mgr.namespace, + table='iptables') + v6_actual = self._get_nflog_entries(ipt_mgr.namespace, + table='ip6tables') + + self.assertEqual(sorted(v4_rules), sorted(v4_actual)) + self.assertEqual(sorted(v6_rules), sorted(v6_actual)) + + def run_start_logging(self, ipt_mgr, log_info, **kwargs): + # Run start logging function with a give log_info + router_info = kwargs.get('router_info') + log_resources = kwargs.get('log_resources') + + self._config_default_chains_v4v6(ipt_mgr) + if router_info: + with mock.patch.object(self.resource_rpc, + 'get_sg_log_info_for_port', + return_value=log_info): + self.log_driver.start_logging(self.context, + router_info=router_info) + elif log_resources: + with mock.patch.object(self.resource_rpc, + 'get_sg_log_info_for_log_resources', + return_value=log_info): + self.log_driver.start_logging(self.context, + log_resources=log_resources) + + +class FWLoggingTestCase(FWLoggingTestBase): + + def test_start_logging_when_l3_starting(self): + # Get router information + ipt_mgr = self.router_info.iptables_manager + port_ids = [port['id'] for port in self.router_info.internal_ports] + + for event in log_const.LOG_EVENTS: + # Test start_logging with single log resource + f_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=event) + ] + self.run_start_logging(ipt_mgr, + log_info=f_log_info, + router_info=self.router_info) + + # Test start_logging with multiple log resources + f_log_info = [ + self._fake_log_info(log_id='fake_log_id_1', + port_ids=[port_ids[0]], + event=event), + self._fake_log_info(log_id='fake_log_id_2', + port_ids=[port_ids[1]], + event=event) + ] + self.run_start_logging(ipt_mgr, + log_info=f_log_info, + router_info=self.router_info) + + self.assert_logging_results(ipt_mgr, f_log_info) + self._refresh_logging_config(ipt_mgr=ipt_mgr) + + def test_start_logging_when_create_log(self): + # Get router information + ipt_mgr = self.router_info.iptables_manager + port_ids = [port['id'] for port in self.router_info.internal_ports] + + for event in log_const.LOG_EVENTS: + log_resources = [self._fake_log_resource(FAKE_PROJECT_ID, + event=event)] + f_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=event) + ] + self.run_start_logging(ipt_mgr, + log_info=f_log_info, + log_resources=log_resources) + + self.assert_logging_results(ipt_mgr, f_log_info) + self._refresh_logging_config(ipt_mgr=ipt_mgr) + + def test_start_logging_when_add_router_port(self): + ipt_mgr = self.router_info.iptables_manager + + for event in log_const.LOG_EVENTS: + port_ids = [port['id'] for port in self.router_info.internal_ports] + + # Making log_info when there is only one port + added_port_id = port_ids.pop() + initial_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=event) + ] + # Make log_info with new adding port + port_ids.append(added_port_id) + add_port_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=event) + ] + + self._config_default_chains_v4v6(ipt_mgr) + with mock.patch.object(self.resource_rpc, + 'get_sg_log_info_for_port', + side_effect=[initial_log_info, + add_port_log_info]): + # Start logging with a single port as normal to get initial + # NFLOG rules into iptables + self.log_driver.start_logging(self.context, + router_info=self.router_info) + # Start logging with the new port + self.log_driver.start_logging(self.context, + router_info=self.router_info) + + self.assert_logging_results(ipt_mgr, add_port_log_info) + self._refresh_logging_config(ipt_mgr=ipt_mgr) + + def test_start_logging_when_remove_port(self): + ipt_mgr = self.router_info.iptables_manager + + for event in log_const.LOG_EVENTS: + port_ids = [port['id'] for port in self.router_info.internal_ports] + + # Making log_info when there are two ports on router + initial_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=event) + ] + # Make log_info when a port is removed from router + port_ids.pop() + remove_port_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=event) + ] + + self._config_default_chains_v4v6(ipt_mgr) + with mock.patch.object(self.resource_rpc, + 'get_sg_log_info_for_port', + side_effect=[initial_log_info, + remove_port_log_info]): + # Start logging with a single port as normal to get initial + # NFLOG rules into iptables + self.log_driver.start_logging(self.context, + router_info=self.router_info) + # Start logging with the new port + self.log_driver.start_logging(self.context, + router_info=self.router_info) + + self.assert_logging_results(ipt_mgr, remove_port_log_info) + self._refresh_logging_config(ipt_mgr=ipt_mgr) + + def test_start_logging_when_attach_port_to_fwg(self): + ipt_mgr = self.router_info.iptables_manager + + for event in log_const.LOG_EVENTS: + port_ids = [port['id'] for port in self.router_info.internal_ports] + + # Making log_info when there is only one port + attached_port_id = port_ids.pop() + initial_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=event) + ] + # Make log_info with a new port that attached to fwg + port_ids.append(attached_port_id) + + attached_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=event) + ] + log_resources = [ + self._fake_log_resource(FAKE_PROJECT_ID, + resource_id=attached_port_id, + event=event) + ] + + self._config_default_chains_v4v6(ipt_mgr) + with mock.patch.object(self.resource_rpc, + 'get_sg_log_info_for_port', + return_value=initial_log_info): + with mock.patch.object(self.resource_rpc, + 'get_sg_log_info_for_log_resources', + return_value=attached_log_info): + # Start logging with a single port as normal to get initial + # NFLOG rules into iptables + self.log_driver.start_logging(self.context, + router_info=self.router_info) + # Start logging with the new port attach to fwg + self.log_driver.start_logging(self.context, + log_resources=log_resources) + + self.assert_logging_results(ipt_mgr, attached_log_info) + self._refresh_logging_config(ipt_mgr=ipt_mgr) + + def test_start_logging_when_detach_port_from_fwg(self): + ipt_mgr = self.router_info.iptables_manager + + for event in log_const.LOG_EVENTS: + port_ids = [port['id'] for port in self.router_info.internal_ports] + + # Making log_info when there are two ports attached to fwg + initial_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=event) + ] + # Make log_info when a port is detached from fwg + detached_port_id = port_ids.pop() + detached_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=event) + ] + log_resources = [ + self._fake_log_resource(FAKE_PROJECT_ID, + resource_id=detached_port_id, + event=event) + ] + + self._config_default_chains_v4v6(ipt_mgr) + with mock.patch.object(self.resource_rpc, + 'get_sg_log_info_for_port', + return_value=initial_log_info): + with mock.patch.object(self.resource_rpc, + 'get_sg_log_info_for_log_resources', + return_value=detached_log_info): + # Start logging with a single port as normal to get initial + # NFLOG rules into iptables + self.log_driver.start_logging(self.context, + router_info=self.router_info) + + # Start logging with the new port attach to fwg + self.log_driver.start_logging(self.context, + log_resources=log_resources) + + self.assert_logging_results(ipt_mgr, detached_log_info) + self._refresh_logging_config(ipt_mgr=ipt_mgr) + + def test_start_logging_when_enable_router(self): + ipt_mgr = self.router_info.iptables_manager + port_ids = [port['id'] for port in self.router_info.internal_ports] + for event in log_const.LOG_EVENTS: + # Log info to initialize NFLOG rules + f_log_info = [ + self._fake_log_info(log_id=FAKE_LOG_ID, + port_ids=port_ids, + event=ALL) + ] + # Initialize NFLOG rules with start_logging + self.run_start_logging(ipt_mgr, + log_info=f_log_info, + router_info=self.router_info) + # Fake disable router by running stop_logging with router_info + self.log_driver.stop_logging( + self.context, router_info=self.router_info.router_id) + # Fake enable router by running start_logging with router_info + self.log_driver.start_logging(self.context, + router_info=self.router_info) + + self.assert_logging_results(ipt_mgr, f_log_info) + self._refresh_logging_config(ipt_mgr=ipt_mgr) + + def test_stop_logging_when_delete_log(self): + ipt_mgr = self.router_info.iptables_manager + + for event in log_const.LOG_EVENTS: + port_ids = [port['id'] for port in self.router_info.internal_ports] + + # Initialize log_info to start logging + log_info_1 = self._fake_log_info(log_id='fake_log_id_1', + port_ids=port_ids, + event=event) + log_info_2 = self._fake_log_info(log_id='fake_log_id_2', + port_ids=[port_ids[0]], + event=event) + initial_log_info = [ + log_info_1, + log_info_2 + ] + + self._config_default_chains_v4v6(ipt_mgr) + with mock.patch.object(self.resource_rpc, + 'get_sg_log_info_for_port', + return_value=initial_log_info): + # Start logging to get initial NFLOG rules + self.log_driver.start_logging(self.context, + router_info=self.router_info) + + # Stop logging by deleting fake_log_id_1 + deleted_log_1 = [{'id': 'fake_log_id_1'}] + self.log_driver.stop_logging(self.context, + log_resources=deleted_log_1) + self.assert_logging_results(ipt_mgr, [log_info_2]) + + # Stop logging by deleting fake_log_id_2 + deleted_log_2 = [{'id': 'fake_log_id_2'}] + self.log_driver.stop_logging(self.context, + log_resources=deleted_log_2) + self.assert_logging_results(ipt_mgr, []) + + self._refresh_logging_config(ipt_mgr=ipt_mgr) + + def test_stop_logging_when_delete_log_with_event_combination(self): + ipt_mgr = self.router_info.iptables_manager + + port_ids = [port['id'] for port in self.router_info.internal_ports] + + # Initial log_info to start logging + log_info_1 = self._fake_log_info(log_id='accept_log_id', + port_ids=port_ids, + event=ACCEPT) + log_info_2 = self._fake_log_info(log_id='all_log_id', + port_ids=[port_ids[0]], + event=ALL) + initial_log_info = [ + log_info_1, + log_info_2 + ] + + self._config_default_chains_v4v6(ipt_mgr) + with mock.patch.object(self.resource_rpc, + 'get_sg_log_info_for_port', + return_value=initial_log_info): + # Start logging to get initial NFLOG rules + self.log_driver.start_logging(self.context, + router_info=self.router_info) + + # Stop logging by deleting accept_log_id + accepted_log = [{'id': 'accept_log_id'}] + self.log_driver.stop_logging(self.context, + log_resources=accepted_log) + self.assert_logging_results(ipt_mgr, [log_info_2]) + + # Stop logging by deleting all_log_id + all_log = [{'id': 'all_log_id'}] + self.log_driver.stop_logging(self.context, + log_resources=all_log) + self.assert_logging_results(ipt_mgr, []) diff --git a/neutron_fwaas/tests/unit/__init__.py b/neutron_fwaas/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/cmd/__init__.py b/neutron_fwaas/tests/unit/cmd/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/cmd/upgrade_checks/__init__.py b/neutron_fwaas/tests/unit/cmd/upgrade_checks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/cmd/upgrade_checks/test_checks.py b/neutron_fwaas/tests/unit/cmd/upgrade_checks/test_checks.py new file mode 100644 index 000000000..41b47d2a2 --- /dev/null +++ b/neutron_fwaas/tests/unit/cmd/upgrade_checks/test_checks.py @@ -0,0 +1,46 @@ +# Copyright 2019 Red Hat Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslo_config import cfg +from oslo_upgradecheck.upgradecheck import Code + +from neutron_fwaas.cmd.upgrade_checks import checks +from neutron_fwaas.tests import base + + +class TestChecks(base.BaseTestCase): + + def setUp(self): + super(TestChecks, self).setUp() + self.checks = checks.Checks() + + def test_get_checks_list(self): + self.assertIsInstance(self.checks.get_checks(), list) + + def test_fwaas_v1_check_sucess(self): + cfg.CONF.set_override('service_plugins', ['l3', 'qos']) + check_result = checks.Checks.fwaas_v1_check(mock.Mock()) + self.assertEqual(Code.SUCCESS, check_result.code) + + def test_fwaas_v1_check_warning(self): + plugins_to_check = [ + ['l3', 'firewall', 'qos'], + ['l3', + 'neutron_fwaas.services.firewall.fwaas_plugin:FirewallPlugin', + 'qos']] + for plugins in plugins_to_check: + cfg.CONF.set_override('service_plugins', plugins) + check_result = checks.Checks.fwaas_v1_check(mock.Mock()) + self.assertEqual(Code.FAILURE, check_result.code) diff --git a/neutron_fwaas/tests/unit/db/__init__.py b/neutron_fwaas/tests/unit/db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/db/firewall/__init__.py b/neutron_fwaas/tests/unit/db/firewall/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/db/firewall/v2/__init__.py b/neutron_fwaas/tests/unit/db/firewall/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/db/firewall/v2/test_firewall_db_v2.py b/neutron_fwaas/tests/unit/db/firewall/v2/test_firewall_db_v2.py new file mode 100644 index 000000000..41c21db5d --- /dev/null +++ b/neutron_fwaas/tests/unit/db/firewall/v2/test_firewall_db_v2.py @@ -0,0 +1,1739 @@ +# Copyright (c) 2016 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. + +import mock +import six +import testtools +import webob.exc + +from neutron_lib import constants as nl_constants +from neutron_lib.exceptions import firewall_v2 as f_exc +from oslo_config import cfg +from oslo_utils import uuidutils + +from neutron_fwaas.common import fwaas_constants as constants +from neutron_fwaas.tests.unit.services.firewall import test_fwaas_plugin_v2 + + +class TestFirewallDBPluginV2(test_fwaas_plugin_v2.FirewallPluginV2TestCase): + + def setUp(self): + super(TestFirewallDBPluginV2, self).setUp() + self.db = self.plugin.driver.firewall_db + + def test_get_policy_ordered_rules(self): + with self.firewall_rule(name='alone'), \ + self.firewall_rule(name='fwr1') as fwr1, \ + self.firewall_rule(name='fwr3') as fwr3, \ + self.firewall_rule(name='fwr2') as fwr2: + fwrs = fwr1, fwr2, fwr3 + expected_ids = [fwr['firewall_rule']['id'] for fwr in fwrs] + with self.firewall_policy(firewall_rules=expected_ids) as fwp: + ctx = self._get_admin_context() + fwp_id = fwp['firewall_policy']['id'] + observeds = self.db._get_policy_ordered_rules(ctx, fwp_id) + observed_ids = [r['id'] for r in observeds] + self.assertEqual(expected_ids, observed_ids) + + def test_create_firewall_policy(self): + name = "firewall_policy1" + attrs = self._get_test_firewall_policy_attrs(name) + + with self.firewall_policy(name=name, shared=self.SHARED, + firewall_rules=None, audited=self.AUDITED + ) as firewall_policy: + for k, v in six.iteritems(attrs): + self.assertEqual(v, firewall_policy['firewall_policy'][k]) + + def test_create_firewall_policy_with_rules(self): + name = "firewall_policy1" + attrs = self._get_test_firewall_policy_attrs(name) + + with self.firewall_rule(name='fwr1') as fwr1, \ + self.firewall_rule(name='fwr2') as fwr2, \ + self.firewall_rule(name='fwr3') as fwr3: + fr = [fwr1, fwr2, fwr3] + fw_rule_ids = [r['firewall_rule']['id'] for r in fr] + attrs['firewall_rules'] = fw_rule_ids + with self.firewall_policy(name=name, shared=self.SHARED, + firewall_rules=fw_rule_ids, + audited=self.AUDITED) as fwp: + for k, v in six.iteritems(attrs): + self.assertEqual(v, fwp['firewall_policy'][k]) + + def test_create_firewall_policy_with_previously_associated_rule(self): + with self.firewall_rule() as fwr: + fw_rule_ids = [fwr['firewall_rule']['id']] + with self.firewall_policy(firewall_rules=fw_rule_ids): + with self.firewall_policy(shared=self.SHARED, + firewall_rules=fw_rule_ids) as fwp2: + self.assertEqual( + fwr['firewall_rule']['id'], + fwp2['firewall_policy']['firewall_rules'][0]) + + def test_show_firewall_policy(self): + name = "firewall_policy1" + attrs = self._get_test_firewall_policy_attrs(name) + + with self.firewall_policy(name=name, shared=self.SHARED, + firewall_rules=None, + audited=self.AUDITED) as fwp: + res = self._show_req('firewall_policies', + fwp['firewall_policy']['id']) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_policy'][k]) + + def test_list_firewall_policies(self): + with self.firewall_policy(name='fwp1', description='fwp') as fwp1, \ + self.firewall_policy(name='fwp2', description='fwp') as fwp2, \ + self.firewall_policy(name='fwp3', description='fwp') as fwp3: + fw_policies = [fwp1, fwp2, fwp3] + self._test_list_resources('firewall_policy', + fw_policies, + query_params='description=fwp') + + def test_update_firewall_policy(self): + name = "new_firewall_policy1" + attrs = self._get_test_firewall_policy_attrs(name, audited=False) + + with self.firewall_policy(shared=self.SHARED, firewall_rules=None, + audited=self.AUDITED) as fwp: + data = {'firewall_policy': {'name': name}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = self.deserialize(self.fmt, req.get_response(self.ext_api)) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_policy'][k]) + + def _test_update_firewall_policy(self, with_audited): + with self.firewall_policy(name='firewall_policy1', description='fwp', + audited=self.AUDITED) as fwp: + attrs = self._get_test_firewall_policy_attrs(audited=with_audited) + data = {'firewall_policy': + {'description': 'fw_p1'}} + if with_audited: + data['firewall_policy']['audited'] = 'True' + + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + attrs['description'] = 'fw_p1' + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_policy'][k]) + + def test_update_firewall_policy_set_audited_false(self): + self._test_update_firewall_policy(with_audited=False) + + def test_update_firewall_policy_with_audited_set_true(self): + self._test_update_firewall_policy(with_audited=True) + + def test_update_firewall_policy_with_rules(self): + attrs = self._get_test_firewall_policy_attrs() + + with self.firewall_rule(name='fwr1') as fwr1, \ + self.firewall_rule(name='fwr2') as fwr2, \ + self.firewall_rule(name='fwr3') as fwr3: + with self.firewall_policy() as fwp: + fr = [fwr1, fwr2, fwr3] + fw_rule_ids = [r['firewall_rule']['id'] for r in fr] + attrs['firewall_rules'] = fw_rule_ids + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + attrs['audited'] = False + attrs['firewall_rules'] = sorted(attrs['firewall_rules']) + # TODO(sridar): set it so that the ordering is maintained + res['firewall_policy']['firewall_rules'] = sorted( + res['firewall_policy']['firewall_rules']) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_policy'][k]) + + def test_update_firewall_policy_replace_rules(self): + attrs = self._get_test_firewall_policy_attrs() + + with self.firewall_rule(name='fwr1') as fwr1, \ + self.firewall_rule(name='fwr2') as fwr2, \ + self.firewall_rule(name='fwr3') as fwr3, \ + self.firewall_rule(name='fwr4') as fwr4: + frs = [fwr1, fwr2, fwr3, fwr4] + fr1 = frs[0:2] + fr2 = frs[2:4] + with self.firewall_policy() as fwp: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr1] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + req.get_response(self.ext_api) + + fw_rule_ids = [r['firewall_rule']['id'] for r in fr2] + attrs['firewall_rules'] = fw_rule_ids + new_data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', new_data, + fwp['firewall_policy']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + attrs['audited'] = False + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_policy'][k]) + + @testtools.skip('bug/1614673') + def test_update_firewall_policy_reorder_rules(self): + attrs = self._get_test_firewall_policy_attrs() + + with self.firewall_rule(name='fwr1') as fwr1, \ + self.firewall_rule(name='fwr2') as fwr2, \ + self.firewall_rule(name='fwr3') as fwr3, \ + self.firewall_rule(name='fwr4') as fwr4: + fr = [fwr1, fwr2, fwr3, fwr4] + with self.firewall_policy() as fwp: + fw_rule_ids = [fr[2]['firewall_rule']['id'], + fr[3]['firewall_rule']['id']] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + req.get_response(self.ext_api) + # shuffle the rules, add more rules + fw_rule_ids = [fr[1]['firewall_rule']['id'], + fr[3]['firewall_rule']['id'], + fr[2]['firewall_rule']['id'], + fr[0]['firewall_rule']['id']] + attrs['firewall_rules'] = fw_rule_ids + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + rules = [] + for rule_id in fw_rule_ids: + res = self._show_req('firewall_rules', rule_id) + rules.append(res['firewall_rule']) + self.assertEqual(1, rules[0]['position']) + self.assertEqual(fr[1]['firewall_rule']['id'], rules[0]['id']) + self.assertEqual(2, rules[1]['position']) + self.assertEqual(fr[3]['firewall_rule']['id'], rules[1]['id']) + self.assertEqual(3, rules[2]['position']) + self.assertEqual(fr[2]['firewall_rule']['id'], rules[2]['id']) + self.assertEqual(4, rules[3]['position']) + self.assertEqual(fr[0]['firewall_rule']['id'], rules[3]['id']) + + def test_update_firewall_policy_with_non_existing_rule(self): + attrs = self._get_test_firewall_policy_attrs() + + with self.firewall_rule(name='fwr1') as fwr1, \ + self.firewall_rule(name='fwr2') as fwr2: + fr = [fwr1, fwr2] + with self.firewall_policy() as fwp: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr] + # appending non-existent rule + fw_rule_ids.append(uuidutils.generate_uuid()) + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = req.get_response(self.ext_api) + # check that the firewall_rule was not found + self.assertEqual(404, res.status_int) + # check if none of the rules got added to the policy + res = self._show_req('firewall_policies', + fwp['firewall_policy']['id']) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_policy'][k]) + + def test_update_shared_firewall_policy_with_nonshared_rule(self): + with self.firewall_rule(name='fwr1', shared=False) as fr: + with self.firewall_policy() as fwp: + fw_rule_ids = [fr['firewall_rule']['id']] + # update shared policy with nonshared rule + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPNotFound.code, res.status_int) + + def test_update_firewall_policy_with_shared_attr_nonshared_rule(self): + with self.firewall_rule(name='fwr1', shared=False) as fr: + with self.firewall_policy(shared=False) as fwp: + fw_rule_ids = [fr['firewall_rule']['id']] + # update shared policy with shared attr and nonshared rule + data = {'firewall_policy': {'shared': self.SHARED, + 'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPNotFound.code, res.status_int) + + def test_update_firewall_policy_with_shared_attr_exist_unshared_rule(self): + with self.firewall_rule(name='fwr1', shared=False) as fwr: + fwr_ids = [fwr['firewall_rule']['id']] + with self.firewall_policy(shared=False, + firewall_rules=fwr_ids) as fwp: + # Update policy with shared attr + data = {'firewall_policy': {'shared': self.SHARED}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) + + def test_update_firewall_policy_with_shared_and_shared_rules(self): + with self.firewall_rule(name='fwr1', shared=self.SHARED) as fwr: + fwr_ids = [fwr['firewall_rule']['id']] + with self.firewall_policy(shared=False, + firewall_rules=fwr_ids) as fwp: + # Update policy with shared attr + data = {'firewall_policy': {'shared': self.SHARED}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPOk.code, res.status_int) + + def test_update_firewall_policy_assoc_with_other_tenant_firewall(self): + with self.firewall_policy(shared=self.SHARED, + tenant_id='tenant1') as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group(ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id): + data = {'firewall_policy': {'shared': False}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) + + def test_update_firewall_policy_from_shared_to_unshared(self): + with self.firewall_policy(shared=True) as fwp: + # update policy with public attr + data = {'firewall_policy': {'shared': False}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPOk.code, res.status_int) + + def test_update_from_shared_to_unshared_associated_as_ingress_fwp(self): + with self.firewall_policy(shared=True, tenant_id='here') as fwp: + # update policy with public attr + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group(tenant_id='another', + ingress_firewall_policy_id=fwp_id): + data = {'firewall_policy': {'shared': False}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) + + def test_update_from_shared_to_unshared_associated_as_egress_fwp(self): + with self.firewall_policy(shared=True, tenant_id='here') as fwp: + # update policy with public attr + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group(tenant_id='another', + egress_firewall_policy_id=fwp_id): + data = {'firewall_policy': {'shared': False}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) + + def test_update_from_shared_to_unshared_associated_as_ingress_egress(self): + with self.firewall_policy(shared=True, tenant_id='here') as fwp: + # update policy with public attr + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group(tenant_id='another', + egress_firewall_policy_id=fwp_id, + ingress_firewall_policy_id=fwp_id): + data = {'firewall_policy': {'shared': False}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) + + def test_update_default_fwg_policy(self): + """ + Make sure that neither admin nor non-admin can update policy + associated with default firewall group + """ + ctx_admin = self._get_admin_context() + ctx_nonadmin = self._get_nonadmin_context() + for ctx in [ctx_admin, ctx_nonadmin]: + self._build_default_fwg(ctx=ctx) + policies = self._list_req('firewall_policies') + for p in policies: + data = {'firewall_policy': + {'firewall_rules': []}} + req = self.new_update_request('firewall_policies', + data, p['id']) + res = req.get_response(self.ext_api) + self.assertEqual(409, res.status_int) + + def test_delete_firewall_policy(self): + ctx = self._get_admin_context() + with self.firewall_policy(do_delete=False) as fwp: + fwp_id = fwp['firewall_policy']['id'] + req = self.new_delete_request('firewall_policies', fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(204, res.status_int) + self.assertRaises(f_exc.FirewallPolicyNotFound, + self.plugin.get_firewall_policy, + ctx, fwp_id) + + @testtools.skip('bug/1614673') + def test_delete_firewall_policy_with_rule(self): + ctx = self._get_admin_context() + attrs = self._get_test_firewall_policy_attrs() + with self.firewall_policy(do_delete=False) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_rule(name='fwr1') as fr: + fr_id = fr['firewall_rule']['id'] + fw_rule_ids = [fr_id] + attrs['firewall_rules'] = fw_rule_ids + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + req.get_response(self.ext_api) + fw_rule = self.plugin.get_firewall_rule(ctx, fr_id) + self.assertEqual(fwp_id, fw_rule['ingress_firewall_policy_id']) + req = self.new_delete_request('firewall_policies', fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(204, res.status_int) + self.assertRaises(f_exc.FirewallPolicyNotFound, + self.plugin.get_firewall_policy, + ctx, fwp_id) + fw_rule = self.plugin.get_firewall_rule(ctx, fr_id) + self.assertIsNone(fw_rule['ingress_firewall_policy_id']) + + def test_delete_firewall_policy_with_firewall_group_association(self): + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + ingress_firewall_policy_id=fwp_id, + admin_state_up=self.ADMIN_STATE_UP): + req = self.new_delete_request('firewall_policies', fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(409, res.status_int) + + def test_create_firewall_rule(self): + attrs = self._get_test_firewall_rule_attrs() + + with self.firewall_rule() as firewall_rule: + for k, v in six.iteritems(attrs): + self.assertEqual(v, firewall_rule['firewall_rule'][k]) + + attrs['source_port'] = None + attrs['destination_port'] = None + with self.firewall_rule(source_port=None, + destination_port=None) as firewall_rule: + for k, v in six.iteritems(attrs): + self.assertEqual(v, firewall_rule['firewall_rule'][k]) + + attrs['source_port'] = '10000' + attrs['destination_port'] = '80' + with self.firewall_rule(source_port=10000, + destination_port=80) as firewall_rule: + for k, v in six.iteritems(attrs): + self.assertEqual(v, firewall_rule['firewall_rule'][k]) + + attrs['source_port'] = '10000' + attrs['destination_port'] = '80' + with self.firewall_rule(source_port='10000', + destination_port='80') as firewall_rule: + for k, v in six.iteritems(attrs): + self.assertEqual(v, firewall_rule['firewall_rule'][k]) + + def test_create_firewall_src_port_illegal_range(self): + attrs = self._get_test_firewall_rule_attrs() + attrs['source_port'] = '65535:1024' + attrs['expected_res_status'] = 400 + self._create_firewall_rule(self.fmt, **attrs) + + def test_create_firewall_dest_port_illegal_range(self): + attrs = self._get_test_firewall_rule_attrs() + attrs['destination_port'] = '65535:1024' + attrs['expected_res_status'] = 400 + self._create_firewall_rule(self.fmt, **attrs) + + def test_create_firewall_rule_icmp_with_port(self): + attrs = self._get_test_firewall_rule_attrs() + attrs['protocol'] = 'icmp' + attrs['expected_res_status'] = 400 + self._create_firewall_rule(self.fmt, **attrs) + + def test_create_firewall_rule_icmp_without_port(self): + attrs = self._get_test_firewall_rule_attrs() + + attrs['protocol'] = 'icmp' + attrs['source_port'] = None + attrs['destination_port'] = None + with self.firewall_rule(source_port=None, + destination_port=None, + protocol='icmp') as firewall_rule: + for k, v in six.iteritems(attrs): + self.assertEqual(v, firewall_rule['firewall_rule'][k]) + + def test_create_firewall_without_source(self): + attrs = self._get_test_firewall_rule_attrs() + attrs['source_ip_address'] = None + attrs['expected_res_status'] = 201 + self._create_firewall_rule(self.fmt, **attrs) + + def test_create_firewall_rule_without_destination(self): + attrs = self._get_test_firewall_rule_attrs() + attrs['destination_ip_address'] = None + attrs['expected_res_status'] = 201 + self._create_firewall_rule(self.fmt, **attrs) + + def test_create_firewall_rule_without_protocol_with_dport(self): + attrs = self._get_test_firewall_rule_attrs() + attrs['protocol'] = None + attrs['source_port'] = None + attrs['expected_res_status'] = 400 + self._create_firewall_rule(self.fmt, **attrs) + + def test_create_firewall_rule_without_protocol_with_sport(self): + attrs = self._get_test_firewall_rule_attrs() + attrs['protocol'] = None + attrs['destination_port'] = None + attrs['expected_res_status'] = 400 + self._create_firewall_rule(self.fmt, **attrs) + + def test_show_firewall_rule_with_fw_policy_not_associated(self): + attrs = self._get_test_firewall_rule_attrs() + with self.firewall_rule() as fw_rule: + res = self._show_req('firewall_rules', + fw_rule['firewall_rule']['id']) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_rule'][k]) + + @testtools.skip('bug/1614673') + def test_show_firewall_rule_with_fw_policy_associated(self): + attrs = self._get_test_firewall_rule_attrs() + with self.firewall_rule() as fw_rule: + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['ingress_firewall_policy_id'] = fwp_id + data = {'firewall_policy': + {'firewall_rules': + [fw_rule['firewall_rule']['id']]}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + req.get_response(self.ext_api) + res = self._show_req('firewall_rules', + fw_rule['firewall_rule']['id']) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_rule'][k]) + + def test_create_firewall_rule_with_ipv6_addrs_and_wrong_ip_version(self): + attrs = self._get_test_firewall_rule_attrs() + attrs['source_ip_address'] = '::/0' + attrs['destination_ip_address'] = '2001:db8:3::/64' + attrs['ip_version'] = 4 + attrs['expected_res_status'] = 400 + self._create_firewall_rule(self.fmt, **attrs) + + attrs = self._get_test_firewall_rule_attrs() + attrs['source_ip_address'] = None + attrs['destination_ip_address'] = '2001:db8:3::/64' + attrs['ip_version'] = 4 + attrs['expected_res_status'] = 400 + self._create_firewall_rule(self.fmt, **attrs) + + attrs = self._get_test_firewall_rule_attrs() + attrs['source_ip_address'] = '::/0' + attrs['destination_ip_address'] = None + attrs['ip_version'] = 4 + attrs['expected_res_status'] = 400 + self._create_firewall_rule(self.fmt, **attrs) + + def test_list_firewall_rules(self): + with self.firewall_rule(name='fwr1') as fwr1, \ + self.firewall_rule(name='fwr2') as fwr2, \ + self.firewall_rule(name='fwr3') as fwr3: + fr = [fwr1, fwr2, fwr3] + query_params = 'protocol=tcp' + self._test_list_resources('firewall_rule', fr, + query_params=query_params) + + def test_update_firewall_rule(self): + name = "new_firewall_rule1" + attrs = self._get_test_firewall_rule_attrs(name) + + attrs['source_port'] = '10:20' + attrs['destination_port'] = '30:40' + with self.firewall_rule() as fwr: + data = {'firewall_rule': {'name': name, + 'protocol': self.PROTOCOL, + 'source_port': '10:20', + 'destination_port': '30:40'}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_rule'][k]) + + attrs['source_port'] = '10000' + attrs['destination_port'] = '80' + with self.firewall_rule() as fwr: + data = {'firewall_rule': {'name': name, + 'protocol': self.PROTOCOL, + 'source_port': 10000, + 'destination_port': 80}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_rule'][k]) + + attrs['source_port'] = '10000' + attrs['destination_port'] = '80' + with self.firewall_rule() as fwr: + data = {'firewall_rule': {'name': name, + 'protocol': self.PROTOCOL, + 'source_port': '10000', + 'destination_port': '80'}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_rule'][k]) + + attrs['source_port'] = None + attrs['destination_port'] = None + with self.firewall_rule() as fwr: + data = {'firewall_rule': {'name': name, + 'source_port': None, + 'destination_port': None}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_rule'][k]) + + def test_update_firewall_rule_with_port_and_no_proto(self): + with self.firewall_rule() as fwr: + data = {'firewall_rule': {'protocol': None, + 'destination_port': 80}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(400, res.status_int) + + def test_update_firewall_rule_without_ports_and_no_proto(self): + with self.firewall_rule() as fwr: + data = {'firewall_rule': {'protocol': None, + 'destination_port': None, + 'source_port': None}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(200, res.status_int) + + def test_update_firewall_rule_with_port(self): + with self.firewall_rule(source_port=None, + destination_port=None, + protocol=None) as fwr: + data = {'firewall_rule': {'destination_port': 80}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(400, res.status_int) + + def test_update_firewall_rule_with_port_illegal_range(self): + with self.firewall_rule() as fwr: + data = {'firewall_rule': {'destination_port': '65535:1024'}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(400, res.status_int) + + def test_update_firewall_rule_with_port_and_protocol(self): + with self.firewall_rule(source_port=None, + destination_port=None, + protocol=None) as fwr: + data = {'firewall_rule': {'destination_port': 80, + 'protocol': 'tcp'}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(200, res.status_int) + + def test_update_firewall_rule_icmp_with_port(self): + with self.firewall_rule(source_port=None, + destination_port=None, + protocol=None) as fwr: + data = {'firewall_rule': {'destination_port': 80, + 'protocol': 'icmp'}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(400, res.status_int) + + with self.firewall_rule(source_port=None, + destination_port=None, + protocol='icmp') as fwr: + data = {'firewall_rule': {'destination_port': 80}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(400, res.status_int) + + def test_update_firewall_rule_protocol_icmp(self): + with self.firewall_rule(source_port=10000) as fwr: + data = {'firewall_rule': {'protocol': 'icmp'}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int) + + def test_update_firewall_rule_protocol_none(self): + with self.firewall_rule(source_port=10000) as fwr: + data = {'firewall_rule': {'protocol': None}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int) + + def test_update_firewall_rule_with_policy_associated(self): + name = "new_firewall_rule1" + attrs = self._get_test_firewall_rule_attrs(name) + with self.firewall_rule() as fwr: + with self.firewall_policy() as fwp: + fwr_id = fwr['firewall_rule']['id'] + data = {'firewall_policy': {'firewall_rules': [fwr_id]}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + req.get_response(self.ext_api) + data = {'firewall_rule': {'name': name}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_rule'][k]) + res = self._show_req('firewall_policies', + fwp['firewall_policy']['id']) + self.assertEqual( + [fwr_id], + res['firewall_policy']['firewall_rules']) + self.assertFalse(res['firewall_policy']['audited']) + + @testtools.skip('bug/1614680') + def test_update_firewall_rule_associated_with_other_tenant_policy(self): + with self.firewall_rule(shared=self, tenant_id='tenant1') as fwr: + fwr_id = [fwr['firewall_rule']['id']] + with self.firewall_policy(shared=False, firewall_rules=fwr_id): + data = {'firewall_rule': {'shared': False}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) + + def test_update_firewall_rule_with_ipv6_ipaddr(self): + with self.firewall_rule(source_ip_address="1::10", + destination_ip_address=None, + ip_version=6) as fwr_v6: + data = {'firewall_rule': { + 'destination_ip_address': "2::20"}} + req = self.new_update_request('firewall_rules', data, + fwr_v6['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(200, res.status_int) + + def test_delete_firewall_rule(self): + ctx = self._get_admin_context() + with self.firewall_rule(do_delete=False) as fwr: + fwr_id = fwr['firewall_rule']['id'] + req = self.new_delete_request('firewall_rules', fwr_id) + res = req.get_response(self.ext_api) + self.assertEqual(204, res.status_int) + self.assertRaises(f_exc.FirewallRuleNotFound, + self.plugin.get_firewall_rule, + ctx, fwr_id) + + def test_delete_firewall_rule_with_policy_associated(self): + with self.firewall_rule() as fwr: + with self.firewall_policy() as fwp: + fwr_id = fwr['firewall_rule']['id'] + data = {'firewall_policy': {'firewall_rules': [fwr_id]}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = req.get_response(self.ext_api) + req = self.new_delete_request('firewall_rules', fwr_id) + res = req.get_response(self.ext_api) + self.assertEqual(409, res.status_int) + + def test_create_firewall_group(self): + attrs = self._get_test_firewall_group_attrs("firewall1") + self._test_create_firewall_group(attrs) + + def test_create_firewall_group_with_router_port(self): + with self.port( + device_owner=nl_constants.DEVICE_OWNER_ROUTER_INTF) as port: + attrs = self._get_test_firewall_group_attrs("fwg1") + attrs['ports'] = [port['port']['id']] + self._test_create_firewall_group(attrs) + + def test_create_firewall_group_with_dvr_port(self): + with self.port( + device_owner=nl_constants.DEVICE_OWNER_DVR_INTERFACE) as port: + attrs = self._get_test_firewall_group_attrs("fwg1") + attrs['ports'] = [port['port']['id']] + self._test_create_firewall_group(attrs) + + def test_create_firewall_group_with_router_port_l3ha(self): + with self.port( + device_owner=nl_constants.DEVICE_OWNER_HA_REPLICATED_INT) as port: + attrs = self._get_test_firewall_group_attrs("fwg1") + attrs['ports'] = [port['port']['id']] + self._test_create_firewall_group(attrs) + + def test_create_firewall_group_with_empty_ports(self): + attrs = self._get_test_firewall_group_attrs("fwg1") + attrs['ports'] = [] + self._test_create_firewall_group(attrs) + + def test_create_default_firewall_group_multiple_times_diff_tenants(self): + ctx_admin = self._get_admin_context() + fwg_admin = self._build_default_fwg(ctx=ctx_admin) + res = self._build_default_fwg(ctx=ctx_admin, is_one=False) + # check that only 1 group has been created + self.assertEqual(1, len(res)) + ctx = self._get_nonadmin_context() + fwg_na = self._build_default_fwg(ctx=ctx) + res = self._build_default_fwg(ctx=ctx, is_one=False) + # check that only 1 group has been created + self.assertEqual(1, len(res)) + # make sure that admin default_fwg and non_admin don't match + self.assertNotEqual(fwg_na['id'], fwg_admin['id']) + # make sure that admin can see default groups for admin and non-admin + res = self._list_req('firewall_groups', ctx=ctx_admin) + self.assertEqual(2, len(res)) + self.assertEqual(set([ctx_admin.tenant_id, ctx.tenant_id]), + set([r['tenant_id'] for r in res])) + + def test_create_default_firewall_group_from_config(self): + group = 'default_fwg_rules' + + cfg.CONF.set_override('shared', True, group) + cfg.CONF.set_override('protocol', 'tcp', group) + cfg.CONF.set_override('enabled', False, group) + cfg.CONF.set_override('ingress_action', 'allow', group) + cfg.CONF.set_override('egress_action', 'deny', group) + cfg.CONF.set_override('ingress_source_port', '7777', group) + cfg.CONF.set_override('egress_source_port', '8888', group) + cfg.CONF.set_override('ingress_destination_port', '6666', group) + cfg.CONF.set_override('egress_destination_port', '5555', group) + cfg.CONF.set_override('ingress_source_ipv4_address', '1.2.3.4', group) + cfg.CONF.set_override('ingress_source_ipv6_address', '1:2:3:4:5:6:7:8', + group) + cfg.CONF.set_override('egress_source_ipv4_address', '4.3.2.1', group) + cfg.CONF.set_override('egress_source_ipv6_address', '8:7:6:5:4:3:2:1', + group) + cfg.CONF.set_override('ingress_destination_ipv4_address', + '251.252.253.254', group) + cfg.CONF.set_override('ingress_destination_ipv6_address', + '88:99:aa:bb:cc:dd:ee:ff', group) + cfg.CONF.set_override('egress_destination_ipv4_address', + '255.254.253.252', group) + cfg.CONF.set_override('egress_destination_ipv6_address', + 'ff:ee:dd:cc:bb:aa:99:88', group) + + self._build_default_fwg() + results = self._list_req('firewall_rules') + for res in results: + res.pop('id') + + base = { + 'shared': True, + 'protocol': 'tcp', + 'enabled': False, + 'tenant_id': 'admin-tenant', + 'project_id': 'admin-tenant', + 'firewall_policy_id': None + } + + ingress_base = dict(base, **{ + 'source_port': '7777', + 'destination_port': '6666', + 'action': 'allow' + }) + + egress_base = dict(base, **{ + 'source_port': '8888', + 'destination_port': '5555', + 'action': 'deny' + }) + + expected = [dict(ingress_base, **{ + 'name': 'default ingress ipv4', + 'description': 'default ingress rule for IPv4', + 'ip_version': 4, + 'source_ip_address': '1.2.3.4', + 'destination_ip_address': '251.252.253.254', + }), dict(ingress_base, **{ + 'name': 'default ingress ipv6', + 'description': 'default ingress rule for IPv6', + 'ip_version': 6, + 'source_ip_address': '1:2:3:4:5:6:7:8', + 'destination_ip_address': '88:99:aa:bb:cc:dd:ee:ff', + }), dict(egress_base, **{ + 'name': 'default egress ipv4', + 'description': 'default egress rule for IPv4', + 'ip_version': 4, + 'source_ip_address': '4.3.2.1', + 'destination_ip_address': '255.254.253.252', + }), dict(egress_base, **{ + 'name': 'default egress ipv6', + 'description': 'default egress rule for IPv6', + 'ip_version': 6, + 'source_ip_address': '8:7:6:5:4:3:2:1', + 'destination_ip_address': 'ff:ee:dd:cc:bb:aa:99:88', + })] + + self.assertEqual(expected, results) + + def test_create_default_firewall_group(self): + self._build_default_fwg() + result_map = { + 'firewall_groups': {"keys": ["description", "name"], + "data": [("Default firewall group", + constants.DEFAULT_FWG)] + }, + 'firewall_policies': { + "keys": ["description", "name"], + "data": [("Ingress firewall policy", + constants.DEFAULT_FWP_INGRESS), + ("Egress firewall policy", + constants.DEFAULT_FWP_EGRESS)]}, + 'firewall_rules': { + "keys": ["description", "action", "protocol", "enabled", + "ip_version", "name"], + "data": [ + ("default ingress rule for IPv4", "deny", None, True, 4, + "default ingress ipv4"), + ("default egress rule for IPv4", "allow", None, True, 4, + "default egress ipv4"), + ("default ingress rule for IPv6", "deny", None, True, 6, + "default ingress ipv6"), + ("default egress rule for IPv6", "allow", None, True, 6, + "default egress ipv6")] + } + } + + def _check_rules_match_policies(policy, direction): + if direction in policy["description"].lower(): + for rule_id in policy['firewall_rules']: + rule = self._show_req( + 'firewall_rules', rule_id)['firewall_rule'] + self.assertTrue(direction in rule["description"]) + + for obj in result_map: + res = self._list_req(obj) + check_keys = result_map[obj]["keys"] + expected = result_map[obj]["data"] + self.assertEqual(len(expected), len(res)) + + # an attempt to check that rules match policies + if obj == 'firewall_policies': + for p in res: + _check_rules_match_policies(p, "ingress") + _check_rules_match_policies(p, "egress") + + # check that a rule with given params is present in actual + # data by comparing expected/actual tuples + actual = [] + for r in res: + actual.append(tuple(r[key] for key in check_keys)) + self.assertEqual(set(expected), set(actual)) + + def test_create_firewall_group_exists_default(self): + self._build_default_fwg()['id'] + attrs = self._get_test_firewall_group_attrs("firewall1") + self._test_create_firewall_group(attrs) + + def test_create_firewall_group_with_fwp_does_not_exist(self): + fmt = self.fmt + fwg_name = "firewall1" + description = "my_firewall1" + not_found_fwp_id = uuidutils.generate_uuid() + self._create_firewall_group(fmt, fwg_name, + description, not_found_fwp_id, + not_found_fwp_id, ports=None, + admin_state_up=self.ADMIN_STATE_UP, + expected_res_status=404) + + def test_create_firewall_group_with_fwp_on_different_tenant(self): + fmt = self.fmt + fwg_name = "firewall1" + description = "my_firewall1" + with self.firewall_policy(shared=False, tenant_id='tenant2') as fwp: + fwp_id = fwp['firewall_policy']['id'] + ctx = self._get_nonadmin_context() + self._create_firewall_group(fmt, fwg_name, + description, + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, + context=ctx, + expected_res_status=404) + + def test_create_firewall_group_with_admin_and_fwp_different_tenant(self): + fmt = self.fmt + fwg_name = "firewall1" + description = "my_firewall1" + with self.firewall_policy(shared=False, tenant_id='tenant2') as fwp: + fwp_id = fwp['firewall_policy']['id'] + ctx = self._get_admin_context() + self._create_firewall_group(fmt, fwg_name, + description, fwp_id, fwp_id, + tenant_id="admin-tenant", + context=ctx, + expected_res_status=404) + + def test_create_firewall_group_with_admin_and_fwp_is_shared(self): + fwg_name = "fw_with_shared_fwp" + with self.firewall_policy(tenant_id="tenantX") as fwp: + fwp_id = fwp['firewall_policy']['id'] + ctx = self._get_admin_context() + target_tenant = 'tenant1' + with self.firewall_group( + name=fwg_name, + ingress_firewall_policy_id=fwp_id, + tenant_id=target_tenant, + context=ctx, + admin_state_up=self.ADMIN_STATE_UP) as fwg: + self.assertEqual(target_tenant, + fwg['firewall_group']['tenant_id']) + + def _test_show_firewall_group(self, attrs): + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['ingress_firewall_policy_id'] = fwp_id + attrs['egress_firewall_policy_id'] = fwp_id + with self.firewall_group( + name=attrs['name'], + ports=attrs['ports'] if 'ports' in attrs else None, + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, + admin_state_up=self.ADMIN_STATE_UP) as firewall_group: + res = self._show_req('firewall_groups', + firewall_group['firewall_group']['id']) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_group'][k]) + + def test_show_firewall_group(self): + attrs = self._get_test_firewall_group_attrs('fwg1') + self._test_show_firewall_group(attrs) + + def test_show_firewall_group_with_ports(self): + attrs = self._get_test_firewall_group_attrs('fwg1') + with self.port( + device_owner=nl_constants.DEVICE_OWNER_ROUTER_INTF) as dummy_port: + attrs['ports'] = [dummy_port['port']['id']] + self._test_show_firewall_group(attrs) + + def test_show_firewall_group_with_empty_ports(self): + attrs = self._get_test_firewall_group_attrs('fwg1') + attrs['ports'] = [] + self._test_show_firewall_group(attrs) + + def test_list_firewall_groups(self): + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group(name='fwg1', tenant_id='tenant1', + ingress_firewall_policy_id=fwp_id, + description='fwg') as fwg1, \ + self.firewall_group(name='fwg2', tenant_id='tenant2', + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, + description='fwg') as fwg2, \ + self.firewall_group(name='fwg3', tenant_id='tenant3', + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, + description='fwg') as fwg3: + fwgrps = [fwg1, fwg2, fwg3] + self._test_list_resources('firewall_group', fwgrps, + query_params='description=fwg') + + def test_update_firewall_group(self): + name = "new_firewall1" + attrs = self._get_test_firewall_group_attrs(name) + + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + ingress_firewall_policy_id=fwp_id, + admin_state_up=self.ADMIN_STATE_UP) as firewall: + data = {'firewall_group': {'name': name}} + req = self.new_update_request('firewall_groups', data, + firewall['firewall_group']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_group'][k]) + + def test_existing_default_create_default_firewall_group(self): + self._build_default_fwg() + self._create_firewall_group(fmt=None, + name=constants.DEFAULT_FWG, + description="", + ingress_firewall_policy_id=None, + egress_firewall_policy_id=None, + expected_res_status=409) + + def test_update_default_firewall_group_with_non_admin_success(self): + ctx = self._get_nonadmin_context() + def_fwg_id = self._build_default_fwg(ctx=ctx)['id'] + with self.port( + device_owner=nl_constants.DEVICE_OWNER_ROUTER_INTF, + tenant_id=ctx.project_id) as dummy_port: + port_id = dummy_port['port']['id'] + success_cases = [ + {'ports': [port_id]}, + {'ports': []}, + {'ports': None}, + {}, + ] + for attr in success_cases: + data = {'firewall_group': attr} + req = self.new_update_request( + 'firewall_groups', data, def_fwg_id) + req.environ['neutron.context'] = ctx + res = req.get_response(self.ext_api) + self.assertEqual(200, res.status_int) + + def test_update_default_firewall_group_with_non_admin_failure(self): + ctx = self._get_nonadmin_context() + def_fwg_id = self._build_default_fwg(ctx=ctx)['id'] + with self.port( + device_owner=nl_constants.DEVICE_OWNER_ROUTER_INTF, + tenant_id=ctx.project_id) as dummy_port: + port_id = dummy_port['port']['id'] + conflict_cases = [ + {'name': ''}, + {'name': 'default'}, + {'name': 'non-default'}, + {'ingress_firewall_policy_id': None}, + {'egress_firewall_policy_id': None}, + {'description': 'try to modify'}, + {'admin_state_up': True}, + {'ports': [port_id], 'name': ''}, + {'ports': [], 'name': 'default'}, + {'ports': None, 'name': 'non-default'}, + ] + for attr in conflict_cases: + data = {'firewall_group': attr} + req = self.new_update_request( + 'firewall_groups', data, def_fwg_id) + req.environ['neutron.context'] = ctx + res = req.get_response(self.ext_api) + self.assertEqual(409, res.status_int) + + def test_update_default_firewall_group_with_admin_success(self): + ctx = self._get_admin_context() + with self.port( + device_owner=nl_constants.DEVICE_OWNER_ROUTER_INTF, + tenant_id=ctx.project_id) as dummy_port: + port_id = dummy_port['port']['id'] + def_fwg_id = self._build_default_fwg(ctx=ctx)['id'] + success_cases = [ + {'ports': [port_id]}, + {'ports': []}, + {'ports': None}, + {'ingress_firewall_policy_id': None}, + {'egress_firewall_policy_id': None}, + {'description': 'try to modify'}, + {'admin_state_up': True}, + {}, + ] + for attr in success_cases: + data = {'firewall_group': attr} + req = self.new_update_request( + 'firewall_groups', data, def_fwg_id) + req.environ['neutron.context'] = ctx + res = req.get_response(self.ext_api) + self.assertEqual(200, res.status_int) + + def test_update_default_firewall_group_with_admin_failure(self): + ctx = self._get_admin_context() + with self.port( + device_owner=nl_constants.DEVICE_OWNER_ROUTER_INTF, + tenant_id=ctx.project_id) as dummy_port: + port_id = dummy_port['port']['id'] + def_fwg_id = self._build_default_fwg(ctx=ctx)['id'] + conflict_cases = [ + {'name': 'default'}, + {'name': 'non-default'}, + {'name': ''}, + {'ports': [port_id], 'name': ''}, + {'ports': [], 'name': 'default'}, + {'ports': None, 'name': 'non-default'}, + ] + for attr in conflict_cases: + data = {'firewall_group': attr} + req = self.new_update_request( + 'firewall_groups', data, def_fwg_id) + req.environ['neutron.context'] = ctx + res = req.get_response(self.ext_api) + self.assertEqual(409, res.status_int) + + def test_update_firewall_group_with_fwp(self): + ctx = self._get_nonadmin_context() + with self.firewall_policy(name='p1', tenant_id=ctx.tenant_id, + shared=False) as fwp1, \ + self.firewall_policy(name='p2', tenant_id=ctx.tenant_id, + shared=False) as fwp2, \ + self.firewall_group( + ingress_firewall_policy_id=fwp1['firewall_policy']['id'], + egress_firewall_policy_id=fwp2['firewall_policy']['id'], + context=ctx) as fw: + fw_id = fw['firewall_group']['id'] + fwp2_id = fwp2['firewall_policy']['id'] + data = {'firewall_group': {'ingress_firewall_policy_id': fwp2_id}} + req = self.new_update_request('firewall_groups', data, fw_id, + context=ctx) + res = req.get_response(self.ext_api) + self.assertEqual(200, res.status_int) + + def test_change_fwg_name_to_default(self): + """ + Make sure that neither admin nor non-admin can change name of + existing firewall group to default + """ + admin_ctx = self._get_admin_context() + nonadmin_ctx = self._get_nonadmin_context() + with self.firewall_group(context=nonadmin_ctx) as fwg: + data = {'firewall_group': {'name': constants.DEFAULT_FWG}} + fwg_id = fwg['firewall_group']['id'] + for ctx in [admin_ctx, nonadmin_ctx]: + req = self.new_update_request('firewall_groups', data, fwg_id, + context=ctx) + res = req.get_response(self.ext_api) + self.assertEqual(409, res.status_int) + + @testtools.skip('bug/1614680') + def test_update_firewall_group_with_shared_fwp(self): + ctx = self._get_nonadmin_context() + with self.firewall_policy(name='p1', tenant_id=ctx.tenant_id, + shared=True) as fwp1, \ + self.firewall_policy(name='p2', tenant_id='tenant2', + shared=True) as fwp2, \ + self.firewall_group( + ingress_firewall_policy_id=fwp1['firewall_policy']['id'], + egress_firewall_policy_id=fwp1['firewall_policy']['id'], + context=ctx) as fw: + fw_id = fw['firewall_group']['id'] + fwp2_id = fwp2['firewall_policy']['id'] + data = {'firewall_group': {'ingress_firewall_policy_id': fwp2_id}} + req = self.new_update_request('firewall_groups', data, fw_id, + context=ctx) + res = req.get_response(self.ext_api) + self.assertEqual(200, res.status_int) + + def test_update_firewall_group_with_admin_and_fwp_different_tenant(self): + ctx = self._get_admin_context() + with self.firewall_policy() as fwp1, \ + self.firewall_policy(tenant_id='tenant2', + shared=False) as fwp2, \ + self.firewall_group( + ingress_firewall_policy_id=fwp1['firewall_policy']['id'], + egress_firewall_policy_id=fwp1['firewall_policy']['id'], + context=ctx) as fw: + fw_id = fw['firewall_group']['id'] + fwp2_id = fwp2['firewall_policy']['id'] + data = {'firewall_group': {'egress_firewall_policy_id': fwp2_id}} + req = self.new_update_request('firewall_groups', data, fw_id, + context=ctx) + res = req.get_response(self.ext_api) + self.assertEqual(404, res.status_int) + + def test_update_firewall_group_fwp_not_found_on_different_tenant(self): + ctx_tenant1 = self._get_nonadmin_context(tenant_id='tenant1') + ctx_tenant2 = self._get_nonadmin_context(tenant_id='tenant2') + + with self.firewall_policy(name='fwp1', context=ctx_tenant1, + shared=False, do_delete=False) as fwp1, \ + self.firewall_group( + ingress_firewall_policy_id=fwp1['firewall_policy']['id'], + context=ctx_tenant1, do_delete=False) as fwg: + fwg_id = fwg['firewall_group']['id'] + # fw_db = self.db._get_firewall_group(ctx_tenant1, fwg_id) + # fw_db['status'] = nl_constants.ACTIVE + + # update firewall from fwp1 to fwp2 (different tenant) + with self.firewall_policy(name='fwp2', context=ctx_tenant2, + shared=False) as fwp2: + data = { + 'firewall_group': { + 'ingress_firewall_policy_id': + fwp2['firewall_policy']['id'], + }, + } + req = self.new_update_request('firewall_groups', data, fwg_id, + context=ctx_tenant1) + res = req.get_response(self.ext_api) + self.assertEqual(404, res.status_int) + + def test_delete_firewall_group(self): + ctx = self._get_admin_context() + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group(ingress_firewall_policy_id=fwp_id, + do_delete=False) as fw: + fw_id = fw['firewall_group']['id'] + req = self.new_delete_request('firewall_groups', fw_id) + res = req.get_response(self.ext_api) + self.assertEqual(204, res.status_int) + self.assertRaises(f_exc.FirewallGroupNotFound, + self.plugin.get_firewall_group, + ctx, fw_id) + + def test_delete_firewall_group_already_deleted(self): + ctx = self._get_admin_context() + with self.firewall_group(do_delete=False, context=ctx) as fwg: + fwg_id = fwg['firewall_group']['id'] + self.assertIsNone(self.plugin.delete_firewall_group(ctx, fwg_id)) + # No error raise is fwg not found on delete + self.assertIsNone(self.plugin.delete_firewall_group(ctx, fwg_id)) + + def test_delete_default_firewall_group_with_admin(self): + ctx_a = self._get_admin_context() + ctx_na = self._get_nonadmin_context() + def_fwg_id = None + for ctx in [ctx_na, ctx_a]: + def_fwg_id = self._build_default_fwg(ctx=ctx)['id'] + req = self.new_delete_request('firewall_groups', def_fwg_id) + req.environ['neutron.context'] = ctx_a + self.assertEqual(204, req.get_response(self.ext_api).status_int) + # check that policy has been deleted by listing as admin and getting 1 + # default fwg with a differnt id + res = self._list_req('firewall_groups', ctx=ctx_a) + self.assertEqual(1, len(res)) + self.assertNotEqual(def_fwg_id, res[0]['id']) + + def test_delete_default_firewall_group_with_non_admin(self): + ctx = self._get_nonadmin_context() + def_fwg_id = self._build_default_fwg(ctx=ctx)['id'] + req = self.new_delete_request('firewall_groups', def_fwg_id) + req.environ['neutron.context'] = ctx + self.assertEqual(409, req.get_response(self.ext_api).status_int) + + def test_insert_rule_in_policy_with_prior_rules_added_via_update(self): + attrs = self._get_test_firewall_policy_attrs() + attrs['audited'] = False + with self.firewall_rule(name='fwr1') as fwr1, \ + self.firewall_rule(name='fwr2') as fwr2, \ + self.firewall_rule(name='fwr3') as fwr3: + frs = [fwr1, fwr2, fwr3] + fr1 = frs[0:2] + fwr3 = frs[2] + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['id'] = fwp_id + fw_rule_ids = [r['firewall_rule']['id'] for r in fr1] + attrs['firewall_rules'] = fw_rule_ids[:] + data = {'firewall_policy': {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + req.get_response(self.ext_api) + self._rule_action('insert', fwp_id, fw_rule_ids[0], + insert_before=fw_rule_ids[0], + insert_after=None, + expected_code=webob.exc.HTTPConflict.code, + expected_body=None) + fwr3_id = fwr3['firewall_rule']['id'] + attrs['firewall_rules'].insert(0, fwr3_id) + self._rule_action('insert', fwp_id, fwr3_id, + insert_before=fw_rule_ids[0], + insert_after=None, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + + def test_insert_rule_in_policy_failures(self): + with self.firewall_rule(name='fwr1') as fr1: + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + fr1_id = fr1['firewall_rule']['id'] + fw_rule_ids = [fr1_id] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + req.get_response(self.ext_api) + # test inserting with empty request body + self._rule_action('insert', fwp_id, '123', + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None, body_data={}) + # test inserting when firewall_rule_id is missing in + # request body + insert_data = {'insert_before': '123', + 'insert_after': '456'} + self._rule_action('insert', fwp_id, '123', + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None, + body_data=insert_data) + # test inserting when firewall_rule_id is None + insert_data = {'firewall_rule_id': None, + 'insert_before': '123', + 'insert_after': '456'} + self._rule_action('insert', fwp_id, None, + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None, + body_data=insert_data) + # test inserting when firewall_policy_id is incorrect + self._rule_action('insert', '123', fr1_id, + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None) + # test inserting when firewall_policy_id is None + self._rule_action('insert', None, fr1_id, + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None) + + def test_insert_rule_and_already_associated(self): + with self.firewall_rule() as fwr: + fwr_id = fwr['firewall_rule']['id'] + with self.firewall_policy(firewall_rules=[fwr_id]) as fwp: + fwp_id = fwp['firewall_policy']['id'] + self._rule_action( + 'insert', fwp_id, fwr_id, + insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPConflict.code, + body_data={'firewall_rule_id': fwr_id}) + + def test_insert_rule_for_previously_associated_rule(self): + with self.firewall_rule() as fwr: + fwr_id = fwr['firewall_rule']['id'] + fw_rule_ids = [fwr_id] + with self.firewall_policy(firewall_rules=fw_rule_ids): + with self.firewall_policy(name='firewall_policy2') as fwp: + fwp_id = fwp['firewall_policy']['id'] + insert_data = {'firewall_rule_id': fwr_id} + self._rule_action( + 'insert', fwp_id, fwr_id, insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPOk.code, + expected_body=None, body_data=insert_data) + + def test_insert_rule_for_previously_associated_rule_other_tenant(self): + with self.firewall_rule(tenant_id='tenant-2') as fwr: + fwr_id = fwr['firewall_rule']['id'] + fw_rule_ids = [fwr_id] + with self.firewall_policy(tenant_id='tenant-2', + firewall_rules=fw_rule_ids): + with self.firewall_policy(name='firewall_policy2') as fwp: + fwp_id = fwp['firewall_policy']['id'] + insert_data = {'firewall_rule_id': fwr_id} + self._rule_action( + 'insert', fwp_id, fwr_id, insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPOk.code, + expected_body=None, body_data=insert_data) + + def test_insert_rule_for_prev_associated_ref_rule(self): + with self.firewall_rule(name='fwr0') as fwr0, \ + self.firewall_rule(name='fwr1') as fwr1: + fwr = [fwr0, fwr1] + fwr0_id = fwr[0]['firewall_rule']['id'] + fwr1_id = fwr[1]['firewall_rule']['id'] + with self.firewall_policy(name='fwp0') as fwp0, \ + self.firewall_policy(name='fwp1', + firewall_rules=[fwr1_id]) as fwp1: + fwp = [fwp0, fwp1] + fwp0_id = fwp[0]['firewall_policy']['id'] + # test inserting before a rule which + # is associated with different policy + self._rule_action('insert', fwp0_id, fwr0_id, + insert_before=fwr1_id, + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None) + # test inserting after a rule which + # is associated with different policy + self._rule_action('insert', fwp0_id, fwr0_id, + insert_after=fwr1_id, + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None) + + def test_insert_rule_for_policy_of_other_tenant(self): + with self.firewall_rule(tenant_id='tenant-2', shared=False) as fwr: + fwr_id = fwr['firewall_rule']['id'] + with self.firewall_policy(name='firewall_policy') as fwp: + fwp_id = fwp['firewall_policy']['id'] + insert_data = {'firewall_rule_id': fwr_id} + self._rule_action( + 'insert', fwp_id, fwr_id, insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPConflict.code, + expected_body=None, body_data=insert_data) + + def test_insert_rule_missing_rule_id(self): + with self.firewall_rule(tenant_id='tenant-2', shared=False): + with self.firewall_policy(name='firewall_policy') as fwp: + fwp_id = fwp['firewall_policy']['id'] + insert_data = {} + self._rule_action( + 'insert', fwp_id, None, insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None, body_data=insert_data) + + def test_insert_rule_empty_rule_id(self): + with self.firewall_rule(tenant_id='tenant-2', shared=False): + with self.firewall_policy(name='firewall_policy') as fwp: + fwp_id = fwp['firewall_policy']['id'] + insert_data = {'firewall_rule_id': None} + self._rule_action( + 'insert', fwp_id, None, insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None, body_data=insert_data) + + def test_insert_rule_invalid_rule_id(self): + with self.firewall_rule(tenant_id='tenant-2', shared=False): + with self.firewall_policy(name='firewall_policy') as fwp: + fwp_id = fwp['firewall_policy']['id'] + fwr_id_fake = 'foo' + insert_data = {'firewall_rule_id': fwr_id_fake} + self._rule_action( + 'insert', fwp_id, fwr_id_fake, insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None, body_data=insert_data) + + def test_insert_rule_nonexistent_rule_id(self): + with self.firewall_rule(tenant_id='tenant-2', shared=False): + with self.firewall_policy(name='firewall_policy') as fwp: + fwp_id = fwp['firewall_policy']['id'] + fwr_id_fake = uuidutils.generate_uuid() + insert_data = {'firewall_rule_id': fwr_id_fake} + self._rule_action( + 'insert', fwp_id, fwr_id_fake, insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None, body_data=insert_data) + + def test_insert_rule_in_policy(self): + attrs = self._get_test_firewall_policy_attrs() + attrs['audited'] = False + with self.firewall_rule(name='fwr0') as fwr0, \ + self.firewall_rule(name='fwr1') as fwr1, \ + self.firewall_rule(name='fwr2') as fwr2, \ + self.firewall_rule(name='fwr3') as fwr3, \ + self.firewall_rule(name='fwr4') as fwr4, \ + self.firewall_rule(name='fwr5') as fwr5, \ + self.firewall_rule(name='fwr6') as fwr6: + fwr = [fwr0, fwr1, fwr2, fwr3, fwr4, fwr5, fwr6] + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['id'] = fwp_id + # test insert when rule list is empty + fwr0_id = fwr[0]['firewall_rule']['id'] + attrs['firewall_rules'].insert(0, fwr0_id) + self._rule_action('insert', fwp_id, fwr0_id, + insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + # test insert at top of rule list, insert_before and + # insert_after not provided + fwr1_id = fwr[1]['firewall_rule']['id'] + attrs['firewall_rules'].insert(0, fwr1_id) + insert_data = {'firewall_rule_id': fwr1_id} + self._rule_action('insert', fwp_id, fwr0_id, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs, body_data=insert_data) + # test insert at top of list above existing rule + fwr2_id = fwr[2]['firewall_rule']['id'] + attrs['firewall_rules'].insert(0, fwr2_id) + self._rule_action('insert', fwp_id, fwr2_id, + insert_before=fwr1_id, + insert_after=None, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + # test insert at bottom of list + fwr3_id = fwr[3]['firewall_rule']['id'] + attrs['firewall_rules'].append(fwr3_id) + self._rule_action('insert', fwp_id, fwr3_id, + insert_before=None, + insert_after=fwr0_id, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + # test insert in the middle of the list using + # insert_before + fwr4_id = fwr[4]['firewall_rule']['id'] + attrs['firewall_rules'].insert(1, fwr4_id) + self._rule_action('insert', fwp_id, fwr4_id, + insert_before=fwr1_id, + insert_after=None, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + # test insert in the middle of the list using + # insert_after + fwr5_id = fwr[5]['firewall_rule']['id'] + attrs['firewall_rules'].insert(1, fwr5_id) + self._rule_action('insert', fwp_id, fwr5_id, + insert_before=None, + insert_after=fwr2_id, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + # test insert when both insert_before and + # insert_after are set + fwr6_id = fwr[6]['firewall_rule']['id'] + attrs['firewall_rules'].insert(1, fwr6_id) + self._rule_action('insert', fwp_id, fwr6_id, + insert_before=fwr5_id, + insert_after=fwr5_id, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + + def test_remove_rule_and_not_associated(self): + with self.firewall_rule(name='fwr0') as fwr: + with self.firewall_policy(name='firewall_policy2') as fwp: + fwp_id = fwp['firewall_policy']['id'] + fwr_id = fwr['firewall_rule']['id'] + msg = "Firewall rule {0} is not associated with " \ + "firewall policy {1}.".format(fwr_id, fwp_id) + result = self._rule_action( + 'remove', fwp_id, fwr_id, + insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPBadRequest.code, + body_data={'firewall_rule_id': fwr_id}) + self.assertEqual(msg, result['NeutronError']['message']) + + def test_remove_rule_from_policy(self): + attrs = self._get_test_firewall_policy_attrs() + attrs['audited'] = False + with self.firewall_rule(name='fwr1') as fwr1, \ + self.firewall_rule(name='fwr2') as fwr2, \ + self.firewall_rule(name='fwr3') as fwr3: + fr1 = [fwr1, fwr2, fwr3] + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['id'] = fwp_id + fw_rule_ids = [r['firewall_rule']['id'] for r in fr1] + attrs['firewall_rules'] = fw_rule_ids[:] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + req.get_response(self.ext_api) + # test removing a rule from a policy that does not exist + self._rule_action('remove', '123', fw_rule_ids[1], + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None) + # test removing a rule in the middle of the list + attrs['firewall_rules'].remove(fw_rule_ids[1]) + self._rule_action('remove', fwp_id, fw_rule_ids[1], + expected_body=attrs) + # test removing a rule at the top of the list + attrs['firewall_rules'].remove(fw_rule_ids[0]) + self._rule_action('remove', fwp_id, fw_rule_ids[0], + expected_body=attrs) + # test removing remaining rule in the list + attrs['firewall_rules'].remove(fw_rule_ids[2]) + self._rule_action('remove', fwp_id, fw_rule_ids[2], + expected_body=attrs) + # test removing rule that is not associated with the policy + self._rule_action('remove', fwp_id, fw_rule_ids[2], + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None) + + def test_remove_rule_from_policy_failures(self): + with self.firewall_rule(name='fwr1') as fr1: + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + fw_rule_ids = [fr1['firewall_rule']['id']] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + req.get_response(self.ext_api) + # test removing rule that does not exist + self._rule_action('remove', fwp_id, '123', + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None) + # test removing rule with bad request + self._rule_action('remove', fwp_id, '123', + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None, body_data={}) + # test removing rule with firewall_rule_id set to None + self._rule_action('remove', fwp_id, '123', + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None, + body_data={'firewall_rule_id': None}) + + def test_show_firewall_rule_by_name(self): + with self.firewall_rule(name='firewall_Rule1') as fw_rule: + res = self._show('firewall_rules', + fw_rule['firewall_rule']['id']) + self.assertEqual('firewall_Rule1', res['firewall_rule']['name']) + + def test_show_firewall_policy_by_name(self): + with self.firewall_policy(name='firewall_Policy1') as fw_policy: + res = self._show('firewall_policies', + fw_policy['firewall_policy']['id']) + self.assertEqual( + 'firewall_Policy1', res['firewall_policy']['name']) + + def test_show_firewall_group_by_name(self): + with self.firewall_group(name='fireWall1') as fw: + res = self._show('firewall_groups', fw['firewall_group']['id']) + self.assertEqual('fireWall1', res['firewall_group']['name']) + + def test_set_port_in_use_for_firewall_group(self): + fwg_db = {'id': 'fake_id'} + new_ports = {'ports': ['fake_port1', 'fake_port2']} + m_context = self._get_admin_context() + with mock.patch.object(m_context.session, 'add', + side_effect=[None, f_exc.FirewallGroupPortInUse( + port_ids=['fake_port2'])]): + self.assertRaises(f_exc.FirewallGroupPortInUse, + self.db._set_ports_for_firewall_group, + m_context, + fwg_db, + new_ports) + + def test_set_port_for_default_firewall_group(self): + ctx = self._get_nonadmin_context() + default_fwg = self._build_default_fwg(ctx=ctx) + port_args = { + 'tenant_id': ctx.tenant_id, + 'device_owner': 'compute:nova', + 'binding:vif_type': 'ovs', + } + self.plugin._is_supported_l2_port = mock.Mock( + return_value=True) + with self.port(**port_args) as port1, self.port(**port_args) as port2: + port1_id = port1['port']['id'] + port2_id = port2['port']['id'] + port_ids = [port1_id, port2_id] + + self.plugin.update_firewall_group( + ctx, + default_fwg['id'], + {'firewall_group': {'ports': port_ids}}, + ) + default_fwg = self.plugin.get_firewall_group(ctx, + default_fwg['id']) + self.assertEqual(sorted(port_ids), sorted(default_fwg['ports'])) diff --git a/neutron_fwaas/tests/unit/privileged/__init__.py b/neutron_fwaas/tests/unit/privileged/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/privileged/netfilter_log/__init__.py b/neutron_fwaas/tests/unit/privileged/netfilter_log/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/privileged/netfilter_log/test_libnetfilter_log.py b/neutron_fwaas/tests/unit/privileged/netfilter_log/test_libnetfilter_log.py new file mode 100644 index 000000000..8b24090d6 --- /dev/null +++ b/neutron_fwaas/tests/unit/privileged/netfilter_log/test_libnetfilter_log.py @@ -0,0 +1,137 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +import socket + +import cffi +import mock +from neutron.tests import base +from oslo_utils import importutils +import testtools + +# mock for dlopen +cffi.FFI = mock.Mock() +cffi.FFI.dlopen = mock.Mock(return_value=mock.Mock()) +lib_log = importutils.import_module( + 'neutron_fwaas.privileged.netfilter_log.libnetfilter_log' +) + + +class NFLogAppTestCase(base.BaseTestCase): + + def setUp(self): + + self.nflog_app = lib_log.NFLogApp() + self.spawn = mock.patch('eventlet.spawn').start() + super(NFLogAppTestCase, self).setUp() + + def test_register_packet_handler(self): + def fake_method(): + pass + self.nflog_app.register_packet_handler(fake_method) + self.assertEqual(fake_method, self.nflog_app.callback) + + def test_unregister_packet_handler(self): + def fake_method(): + pass + self.nflog_app.register_packet_handler(fake_method) + self.assertEqual(fake_method, self.nflog_app.callback) + self.nflog_app.unregister_packet_handler() + self.assertIsNone(self.nflog_app.callback) + + +class NFLogWrapper(base.BaseTestCase): + + def setUp(self): + super(NFLogWrapper, self).setUp() + lib_log.libnflog = mock.Mock() + lib_log.ffi = mock.Mock() + + def test_open_failed(self): + lib_log.libnflog.nflog_open.return_value = None + handle = lib_log.NFLogWrapper.get_instance() + with testtools.ExpectedException(Exception): + handle.open() + lib_log.libnflog.nflog_open.assert_called_once_with() + lib_log.libnflog.nflog_unbind_pf.assert_not_called() + lib_log.libnflog.nflog_bind_pf.assert_not_called() + handle.close() + + def test_bind_pf(self): + nflog_handle = mock.Mock() + lib_log.libnflog.nflog_open.return_value = nflog_handle + handle = lib_log.NFLogWrapper.get_instance() + handle.open() + lib_log.libnflog.nflog_open.assert_called_once_with() + calls = [mock.call(nflog_handle, socket.AF_INET), + mock.call(nflog_handle, socket.AF_INET6)] + lib_log.libnflog.nflog_unbind_pf.assert_has_calls( + calls, any_order=True) + lib_log.libnflog.nflog_bind_pf.assert_has_calls( + calls, any_order=True) + + def test_bind_group_set_mode_failed(self): + nflog_handle = mock.Mock() + g_handle = mock.Mock() + lib_log.libnflog.nflog_open.return_value = nflog_handle + lib_log.libnflog.nflog_bind_group.return_value = g_handle + lib_log.libnflog.nflog_set_mode.return_value = -1 + handle = lib_log.NFLogWrapper.get_instance() + with testtools.ExpectedException(Exception): + handle.open() + handle.bind_group(0) + lib_log.libnflog.nflog_open.assert_called_once_with() + lib_log.libnflog.nflog_bind_group.assert_called_once_with( + nflog_handle, 0) + lib_log.libnflog.nflog_set_mode.assert_called_once_with( + g_handle, 0x2, 0xffff) + lib_log.libnflog.nflog_callback_register.assert_not_called() + + def test_bind_group_set_callback_failed(self): + nflog_handle = mock.Mock() + g_handle = mock.Mock() + lib_log.libnflog.nflog_open.return_value = nflog_handle + lib_log.libnflog.nflog_bind_group.return_value = g_handle + lib_log.libnflog.nflog_set_mode.return_value = 0 + lib_log.libnflog.nflog_callback_register.return_value = -1 + handle = lib_log.NFLogWrapper.get_instance() + with testtools.ExpectedException(Exception): + handle.open() + handle.bind_group(0) + lib_log.libnflog.nflog_open.assert_called_once_with() + lib_log.libnflog.nflog_bind_group.assert_called_once_with( + nflog_handle, 0) + lib_log.libnflog.nflog_set_mode.assert_called_once_with( + g_handle, 0x2, 0xffff) + lib_log.libnflog.nflog_callback_register.assert_called_once_with( + g_handle, handle.cb, lib_log.ffi.NULL) + + def test_bind_group_pass(self): + nflog_handle = mock.Mock() + g_handle = mock.Mock() + lib_log.libnflog.nflog_open.return_value = nflog_handle + lib_log.libnflog.nflog_bind_group.return_value = g_handle + lib_log.libnflog.nflog_set_mode.return_value = 0 + lib_log.libnflog.nflog_callback_register.return_value = 0 + handle = lib_log.NFLogWrapper.get_instance() + handle.open() + handle.bind_group(0) + lib_log.libnflog.nflog_open.assert_called_once_with() + lib_log.libnflog.nflog_bind_group.assert_called_once_with( + nflog_handle, 0) + lib_log.libnflog.nflog_set_mode.assert_called_once_with( + g_handle, 0x2, 0xffff) + lib_log.libnflog.nflog_callback_register.assert_called_once_with( + g_handle, handle.cb, lib_log.ffi.NULL) diff --git a/neutron_fwaas/tests/unit/privileged/test_netlink_lib.py b/neutron_fwaas/tests/unit/privileged/test_netlink_lib.py new file mode 100644 index 000000000..8c96fa04e --- /dev/null +++ b/neutron_fwaas/tests/unit/privileged/test_netlink_lib.py @@ -0,0 +1,329 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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. + +import mock +import testtools + +from neutron_lib import constants + +from neutron_fwaas.privileged import netlink_constants as nl_constants +from neutron_fwaas.privileged import netlink_lib as nl_lib +from neutron_fwaas.tests import base + + +FAKE_ENTRY = {'ipversion': 4, 'protocol': 'icmp', + 'type': '8', 'code': '0', 'id': 1234, + 'src': '1.1.1.1', 'dst': '2.2.2.2'} +FAKE_TCP_ENTRY = {'ipversion': 4, 'protocol': 'tcp', + 'sport': 1, 'dport': 2, + 'src': '1.1.1.1', 'dst': '2.2.2.2'} +FAKE_UDP_ENTRY = {'ipversion': 4, 'protocol': 'udp', + 'sport': 1, 'dport': 2, + 'src': '1.1.1.1', 'dst': '2.2.2.2'} +FAKE_ICMPV6_ENTRY = {'ipversion': 6, 'protocol': 'ipv6-icmp', + 'sport': 1, 'dport': 2, 'type': '8', 'code': '0', + 'id': 3456, 'src': '10::10', 'dst': '20::20'} + + +class NetlinkLibTestCase(base.BaseTestCase): + def setUp(self): + super(NetlinkLibTestCase, self).setUp() + nl_lib.nfct = mock.Mock() + nl_lib.libc = mock.Mock() + + def test_open_new_conntrack_handler_failed(self): + nl_lib.nfct.nfct_open.return_value = None + with testtools.ExpectedException(nl_lib.ConntrackOpenFailedExit): + with nl_lib.ConntrackManager(): + nl_lib.nfct.nfct_open.assert_called_once() + nl_lib.nfct.nfct_close.assert_not_called() + + def test_open_new_conntrack_handler_pass(self): + with nl_lib.ConntrackManager(): + nl_lib.nfct.nfct_open.assert_called_once() + nl_lib.nfct.nfct_close.assert_called_once() + + def test_conntrack_list_entries(self): + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once() + conntrack.list_entries() + nl_lib.nfct.nfct_callback_register.assert_called_once() + nl_lib.nfct.nfct_query.assert_called_once() + nl_lib.nfct.nfct_close.assert_called_once() + + def test_conntrack_flush_entries(self): + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once() + conntrack.flush_entries() + nl_lib.nfct.nfct_query.assert_called_once() + nl_lib.nfct.nfct_close.assert_called_once() + + def test_conntrack_new_failed(self): + nl_lib.nfct.nfct_new.return_value = None + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once() + conntrack.delete_entries([FAKE_ENTRY]) + nl_lib.nfct.nfct_new.assert_called_once() + nl_lib.nfct.nfct_destroy.assert_called_once() + nl_lib.nfct.nfct_close.assert_called_once() + + def test_conntrack_delete_icmp_entry(self): + conntrack_filter = mock.Mock() + nl_lib.nfct.nfct_new.return_value = conntrack_filter + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once() + conntrack.delete_entries([FAKE_ENTRY]) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['icmp']), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_CODE, + int(FAKE_ENTRY['code'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_TYPE, + int(FAKE_ENTRY['type'])) + ] + nl_lib.nfct.nfct_set_attr_u8.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_ID, + nl_lib.libc.htons(FAKE_ENTRY['id'])), + ] + nl_lib.nfct.nfct_set_attr_u16.assert_has_calls(calls) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + conntrack._convert_text_to_binary( + FAKE_ENTRY['src'], 4)), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + conntrack._convert_text_to_binary( + FAKE_ENTRY['dst'], 4)), + ] + nl_lib.nfct.nfct_set_attr.assert_has_calls(calls, any_order=True) + nl_lib.nfct.nfct_destroy.assert_called_once() + nl_lib.nfct.nfct_close.assert_called_once() + + def test_conntrack_delete_icmpv6_entry(self): + conntrack_filter = mock.Mock() + nl_lib.nfct.nfct_new.return_value = conntrack_filter + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once() + conntrack.delete_entries([FAKE_ICMPV6_ENTRY]) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[6]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['ipv6-icmp']), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_CODE, + int(FAKE_ICMPV6_ENTRY['code'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_TYPE, + int(FAKE_ICMPV6_ENTRY['type'])) + ] + nl_lib.nfct.nfct_set_attr_u8.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_ID, + nl_lib.libc.htons(FAKE_ICMPV6_ENTRY['id'])), + ] + nl_lib.nfct.nfct_set_attr_u16.assert_has_calls(calls) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_IPV6_SRC, + conntrack._convert_text_to_binary( + FAKE_ENTRY['src'], 6)), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV6_DST, + conntrack._convert_text_to_binary( + FAKE_ENTRY['dst'], 6)), + ] + nl_lib.nfct.nfct_set_attr.assert_has_calls(calls, any_order=True) + nl_lib.nfct.nfct_destroy.assert_called_once() + nl_lib.nfct.nfct_close.assert_called_once() + + def test_conntrack_delete_udp_entry(self): + conntrack_filter = mock.Mock() + nl_lib.nfct.nfct_new.return_value = conntrack_filter + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once() + conntrack.delete_entries([FAKE_UDP_ENTRY]) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['udp']) + ] + nl_lib.nfct.nfct_set_attr_u8.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_SRC, + nl_lib.libc.htons(FAKE_UDP_ENTRY['sport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_DST, + nl_lib.libc.htons(FAKE_UDP_ENTRY['dport'])) + ] + nl_lib.nfct.nfct_set_attr_u16.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + conntrack._convert_text_to_binary( + FAKE_UDP_ENTRY['src'], 4)), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + conntrack._convert_text_to_binary( + FAKE_UDP_ENTRY['dst'], 4)), + ] + nl_lib.nfct.nfct_set_attr.assert_has_calls(calls, any_order=True) + nl_lib.nfct.nfct_destroy.assert_called_once() + nl_lib.nfct.nfct_close.assert_called_once() + + def test_conntrack_delete_tcp_entry(self): + conntrack_filter = mock.Mock() + nl_lib.nfct.nfct_new.return_value = conntrack_filter + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once() + conntrack.delete_entries([FAKE_TCP_ENTRY]) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['tcp']) + ] + nl_lib.nfct.nfct_set_attr_u8.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_SRC, + nl_lib.libc.htons(FAKE_TCP_ENTRY['sport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_DST, + nl_lib.libc.htons(FAKE_TCP_ENTRY['dport'])) + ] + nl_lib.nfct.nfct_set_attr_u16.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + conntrack._convert_text_to_binary( + FAKE_TCP_ENTRY['src'], 4)), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + conntrack._convert_text_to_binary( + FAKE_TCP_ENTRY['dst'], 4)), + ] + nl_lib.nfct.nfct_set_attr.assert_has_calls(calls, any_order=True) + nl_lib.nfct.nfct_destroy.assert_called_once() + nl_lib.nfct.nfct_close.assert_called_once() + + def test_conntrack_delete_entries(self): + conntrack_filter = mock.Mock() + nl_lib.nfct.nfct_new.return_value = conntrack_filter + with nl_lib.ConntrackManager() as conntrack: + nl_lib.nfct.nfct_open.assert_called_once() + conntrack.delete_entries([FAKE_ENTRY, + FAKE_TCP_ENTRY, + FAKE_UDP_ENTRY]) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['tcp']), + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['udp']), + mock.call(conntrack_filter, + nl_constants.ATTR_L3PROTO, + nl_constants.IPVERSION_SOCKET[4]), + mock.call(conntrack_filter, + nl_constants.ATTR_L4PROTO, + constants.IP_PROTOCOL_MAP['icmp']), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_CODE, + int(FAKE_ENTRY['code'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_TYPE, + int(FAKE_ENTRY['type'])) + ] + nl_lib.nfct.nfct_set_attr_u8.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_SRC, + nl_lib.libc.htons(FAKE_TCP_ENTRY['sport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_DST, + nl_lib.libc.htons(FAKE_TCP_ENTRY['dport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_SRC, + nl_lib.libc.htons(FAKE_UDP_ENTRY['sport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_PORT_DST, + nl_lib.libc.htons(FAKE_UDP_ENTRY['dport'])), + mock.call(conntrack_filter, + nl_constants.ATTR_ICMP_ID, + nl_lib.libc.htons(FAKE_ENTRY['id'])), + ] + nl_lib.nfct.nfct_set_attr_u16.assert_has_calls(calls, + any_order=True) + calls = [ + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + conntrack._convert_text_to_binary( + FAKE_TCP_ENTRY['src'], 4)), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + conntrack._convert_text_to_binary( + FAKE_TCP_ENTRY['dst'], 4)), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + conntrack._convert_text_to_binary( + FAKE_UDP_ENTRY['src'], 4)), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + conntrack._convert_text_to_binary( + FAKE_UDP_ENTRY['dst'], 4)), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_SRC, + conntrack._convert_text_to_binary( + FAKE_ENTRY['src'], 4)), + mock.call(conntrack_filter, + nl_constants.ATTR_IPV4_DST, + conntrack._convert_text_to_binary( + FAKE_UDP_ENTRY['dst'], 4)), + ] + nl_lib.nfct.nfct_set_attr.assert_has_calls(calls, any_order=True) + nl_lib.nfct.nfct_destroy.assert_called_once() + nl_lib.nfct.nfct_close.assert_called_once() diff --git a/neutron_fwaas/tests/unit/privileged/test_utils.py b/neutron_fwaas/tests/unit/privileged/test_utils.py new file mode 100644 index 000000000..525df286c --- /dev/null +++ b/neutron_fwaas/tests/unit/privileged/test_utils.py @@ -0,0 +1,79 @@ +# Copyright (c) 2017 Thales Services SAS +# 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. + +import mock +import testtools + +from neutron_fwaas.privileged import utils +from neutron_fwaas.tests import base + + +class InNamespaceTest(base.BaseTestCase): + ORG_NETNS_FD = 124 + NEW_NETNS_FD = 421 + NEW_NETNS = 'newns' + + def setUp(self): + super(InNamespaceTest, self).setUp() + + # NOTE(cby): we should unmock os.open/close as early as possible + # because there are used in cleanups + open_patch = mock.patch('os.open', return_value=self.ORG_NETNS_FD) + self.open_mock = open_patch.start() + self.addCleanup(open_patch.stop) + + close_patch = mock.patch('os.close') + self.close_mock = close_patch.start() + self.addCleanup(close_patch.stop) + + self.setns_mock = mock.patch( + 'pyroute2.netns.setns').start() + + def test_in_namespace(self): + with utils.in_namespace(self.NEW_NETNS): + self.setns_mock.assert_called_once_with(self.NEW_NETNS) + + setns_calls = [mock.call(self.NEW_NETNS), mock.call(self.ORG_NETNS_FD)] + self.setns_mock.assert_has_calls(setns_calls) + + def test_in_no_namespace(self): + for namespace in ('', None): + with utils.in_namespace(namespace): + pass + self.setns_mock.assert_not_called() + self.close_mock.assert_not_called() + + def test_in_namespace_failed(self): + with testtools.ExpectedException(ValueError): + with utils.in_namespace(self.NEW_NETNS): + self.setns_mock.assert_called_once_with(self.NEW_NETNS) + raise ValueError + + setns_calls = [mock.call(self.NEW_NETNS), mock.call(self.ORG_NETNS_FD)] + self.setns_mock.assert_has_calls(setns_calls) + + def test_in_namespace_enter_failed(self): + self.setns_mock.side_effect = ValueError + with testtools.ExpectedException(ValueError): + with utils.in_namespace(self.NEW_NETNS): + self.fail('It should fail before we reach this code') + + self.setns_mock.assert_called_once_with(self.NEW_NETNS) + + def test_in_namespace_exit_failed(self): + self.setns_mock.side_effect = [self.NEW_NETNS_FD, ValueError] + with testtools.ExpectedException(utils.BackInNamespaceExit): + with utils.in_namespace(self.NEW_NETNS): + pass diff --git a/neutron_fwaas/tests/unit/services/__init__.py b/neutron_fwaas/tests/unit/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/__init__.py b/neutron_fwaas/tests/unit/services/firewall/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/__init__.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/__init__.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/__init__.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/__init__.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/__init__.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/noop/__init__.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/noop/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/noop/test_noop_driver.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/noop/test_noop_driver.py new file mode 100644 index 000000000..fea3c29f3 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/noop/test_noop_driver.py @@ -0,0 +1,44 @@ +# Copyright 2017 Mirantis Inc. +# 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. + +import mock + +from neutron import manager +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + noop import noop_driver +from neutron_fwaas.tests import base + + +class TestNoopDriver(base.BaseTestCase): + def setUp(self): + super(TestNoopDriver, self).setUp() + mock_br = mock.Mock() + self.firewall = noop_driver.NoopFirewallL2Driver(mock_br) + + def test_basic_methods(self): + # just make sure it doesn't crash + fwg_mock = mock.Mock() + self.firewall.create_firewall_group(ports=[], firewall_group=fwg_mock) + self.firewall.update_firewall_group(ports=[], firewall_group=fwg_mock) + self.firewall.delete_firewall_group(ports=[], firewall_group=fwg_mock) + self.firewall.filter_defer_apply_on() + self.firewall.filter_defer_apply_off() + self.firewall.defer_apply() + self.firewall.ports + + def test_load_firewall_class(self): + res = manager.NeutronManager.load_class_for_provider( + 'neutron.agent.l2.firewall_drivers', 'noop') + self.assertEqual(res, noop_driver.NoopFirewallL2Driver) diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/__init__.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/test_firewall.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/test_firewall.py new file mode 100644 index 000000000..2177b7377 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/test_firewall.py @@ -0,0 +1,699 @@ +# Copyright 2017 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from neutron_lib import constants +import testtools + +from neutron.agent.common import ovs_lib +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts +from neutron.plugins.ml2.drivers.openvswitch.agent import \ + ovs_agent_extension_api as ovs_ext_api +from neutron.tests import base + +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import constants as fwaas_ovs_consts +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import exceptions +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import firewall as ovsfw + +TESTING_VLAN_TAG = 1 + + +def create_ofport(port_dict): + ovs_port = mock.Mock(vif_mac='00:00:00:00:00:00', ofport=1, + port_name="port-name") + return ovsfw.OFPort(port_dict, ovs_port, vlan_tag=TESTING_VLAN_TAG) + + +class TestCreateRegNumbers(base.BaseTestCase): + def test_no_registers_defined(self): + flow = {'foo': 'bar'} + ovsfw.create_reg_numbers(flow) + self.assertEqual({'foo': 'bar'}, flow) + + def test_both_registers_defined(self): + flow = {'foo': 'bar', 'reg_port': 1, 'reg_net': 2} + expected_flow = {'foo': 'bar', + 'reg{:d}'.format(fwaas_ovs_consts.REG_PORT): 1, + 'reg{:d}'.format(fwaas_ovs_consts.REG_NET): 2} + ovsfw.create_reg_numbers(flow) + self.assertEqual(expected_flow, flow) + + +class TestFirewallGroup(base.BaseTestCase): + def setUp(self): + super(TestFirewallGroup, self).setUp() + self.fwg = ovsfw.FirewallGroup('123') + self.fwg.members = {'type': [1, 2, 3, 4]} + + def test_update_rules(self): + ingress_rules = [{'foo-ingress': 'bar', 'rule': 'all'}, + {'bar-ingress': 'foo'}] + egress_rules = [{'foo-egress': '123456'}, {'bar-egress': 'bar'}] + self.fwg.update_rules(ingress_rules, egress_rules) + + self.assertEqual(ingress_rules, self.fwg.ingress_rules) + self.assertEqual(egress_rules, self.fwg.egress_rules) + + def test_update_rules_protocols(self): + # XXX FIXME(ivasilevskaya) figure out what this test does and fix + # appropriately + # leaving failing as it may be important + rules = [ + {'foo': 'bar', 'protocol': constants.PROTO_NAME_ICMP, + 'ethertype': constants.IPv4}, + {'foo': 'bar', 'protocol': constants.PROTO_NAME_ICMP, + 'ethertype': constants.IPv6}, + {'foo': 'bar', 'protocol': constants.PROTO_NAME_IPV6_ICMP_LEGACY, + 'ethertype': constants.IPv6}, + {'foo': 'bar', 'protocol': constants.PROTO_NAME_TCP}, + {'foo': 'bar', 'protocol': '94'}, + {'foo': 'bar', 'protocol': 'baz'}, + {'foo': 'no_proto'}] + self.fwg.update_rules(rules, []) + + self.assertEqual({'foo': 'no_proto'}, self.fwg.ingress_rules.pop()) + protos = [rule['protocol'] for rule in self.fwg.ingress_rules] + self.assertEqual([constants.PROTO_NUM_ICMP, + constants.PROTO_NUM_IPV6_ICMP, + constants.PROTO_NUM_IPV6_ICMP, + constants.PROTO_NUM_TCP, + 94, + 'baz'], protos) + + def test_get_ethertype_filtered_addresses(self): + addresses = self.fwg.get_ethertype_filtered_addresses('type') + expected_addresses = [1, 2, 3, 4] + self.assertEqual(expected_addresses, addresses) + + +class TestOFPort(base.BaseTestCase): + def setUp(self): + super(TestOFPort, self).setUp() + self.ipv4_addresses = ['10.0.0.1', '192.168.0.1'] + self.ipv6_addresses = ['fe80::f816:3eff:fe2e:1'] + port_dict = {'device': 1, + 'fixed_ips': [ + {'subnet_id': 's_%s' % ip, 'ip_address': ip} + for ip in self.ipv4_addresses + self.ipv6_addresses]} + self.port = create_ofport(port_dict) + + def test_ipv4_address(self): + ipv4_addresses = self.port.ipv4_addresses + self.assertEqual(self.ipv4_addresses, ipv4_addresses) + + def test_ipv6_address(self): + ipv6_addresses = self.port.ipv6_addresses + self.assertEqual(self.ipv6_addresses, ipv6_addresses) + + def test__get_allowed_pairs(self): + port = { + 'allowed_address_pairs': [ + {'mac_address': 'foo', 'ip_address': '10.0.0.1'}, + {'mac_address': 'bar', 'ip_address': '192.168.0.1'}, + {'mac_address': 'qux', 'ip_address': '169.254.0.0/16'}, + {'mac_address': 'baz', 'ip_address': '2003::f'}, + ]} + allowed_pairs_v4 = ovsfw.OFPort._get_allowed_pairs(port, version=4) + allowed_pairs_v6 = ovsfw.OFPort._get_allowed_pairs(port, version=6) + expected_aap_v4 = {('foo', '10.0.0.1'), ('bar', '192.168.0.1'), + ('qux', '169.254.0.0/16')} + expected_aap_v6 = {('baz', '2003::f')} + self.assertEqual(expected_aap_v4, allowed_pairs_v4) + self.assertEqual(expected_aap_v6, allowed_pairs_v6) + + def test__get_allowed_pairs_empty(self): + port = {} + allowed_pairs = ovsfw.OFPort._get_allowed_pairs(port, version=4) + self.assertFalse(allowed_pairs) + + def test_update(self): + old_port_dict = self.port.neutron_port_dict + new_port_dict = old_port_dict.copy() + added_ips = [1, 2, 3] + new_port_dict.update({ + 'fixed_ips': added_ips, + 'allowed_address_pairs': [ + {'mac_address': '00:00:00:00:00:01', + 'ip_address': '192.168.0.1'}, + {'mac_address': '00:00:00:00:00:01', + 'ip_address': '2003::f'}], + }) + self.port.update(new_port_dict) + self.assertEqual(new_port_dict, self.port.neutron_port_dict) + self.assertIsNot(new_port_dict, self.port.neutron_port_dict) + self.assertEqual(added_ips, self.port.fixed_ips) + self.assertEqual({('00:00:00:00:00:01', '192.168.0.1')}, + self.port.allowed_pairs_v4) + self.assertIn(('00:00:00:00:00:01', '2003::f'), + self.port.allowed_pairs_v6) + + +class TestFWGPortMap(base.BaseTestCase): + def setUp(self): + super(TestFWGPortMap, self).setUp() + self.map = ovsfw.FWGPortMap() + + def test_get_or_create_fwg_existing_fwg(self): + self.map.fw_groups['id'] = mock.sentinel + fwg = self.map.get_or_create_fwg('id') + self.assertIs(mock.sentinel, fwg) + + def test_get_or_create_fwg_nonexisting_fwg(self): + with mock.patch.object(ovsfw, 'FirewallGroup') as fwg_mock: + fwg = self.map.get_or_create_fwg('id') + self.assertEqual(fwg_mock.return_value, fwg) + + def _check_port(self, port_id, expected_id): + port = self.map.ports[port_id] + expected_fwg = self.map.fw_groups[expected_id] + self.assertEqual(expected_fwg, port.fw_group) + + def _check_fwg(self, fwg_id, expected_port_ids): + fwg = self.map.fw_groups[fwg_id] + expected_ports = {self.map.ports[port_id] + for port_id in expected_port_ids} + self.assertEqual(expected_ports, fwg.ports) + + def _create_ports_and_fwgs(self): + fwg_1 = ovsfw.FirewallGroup(1) + fwg_2 = ovsfw.FirewallGroup(2) + fwg_3 = ovsfw.FirewallGroup(3) + port_a = create_ofport({'device': 'a'}) + port_b = create_ofport({'device': 'b'}) + port_c = create_ofport({'device': 'c'}) + self.map.ports = {'a': port_a, 'b': port_b, 'c': port_c} + self.map.fw_groups = {1: fwg_1, 2: fwg_2, 3: fwg_3} + # XXX FIXME(ivasilevskaya) see note for OFPORT + port_a.fw_group = fwg_1 + port_b.fw_group = fwg_2 + port_c.fw_group = fwg_2 + fwg_1.ports = {port_a} + fwg_2.ports = {port_b, port_c} + + def test_create_port(self): + """Create a port and assign it to firewall group + + It is implied that 1 port can be assigned to one firewall group only + """ + port = create_ofport({'device': 'a'}) + port_dict = {'some-port-attributes-go-here': 42, + 'firewall_group': 1} + self.map.create_port(port, port_dict) + self._check_port('a', 1) + self._check_fwg(1, ['a']) + + def test_update_port_another_fwg_added(self): + """Update a port with new firewall group id + + It is implied that 1 port can be assigned to one firewall group only + """ + self._create_ports_and_fwgs() + self._check_port('b', 2) + port_dict = {'firewall_group': 3} + self.map.update_port(self.map.ports['b'], port_dict) + self._check_port('a', 1) + self._check_port('b', 3) + self._check_port('c', 2) + self._check_fwg(1, ['a']) + self._check_fwg(2, ['c']) + self._check_fwg(3, ['b']) + + def test_remove_port(self): + self._create_ports_and_fwgs() + self.map.remove_port(self.map.ports['c']) + self._check_port('b', 2) + self._check_fwg(1, ['a']) + self._check_fwg(2, ['b']) + self.assertNotIn('c', self.map.ports) + + def test_update_rules(self): + """Just make sure it doesn't crash""" + self.map.update_rules(42, [], []) + + def test_update_members(self): + """Just make sure it doesn't crash""" + self.map.update_members(42, []) + + +class FakeOVSPort(object): + def __init__(self, name, port, mac): + self.port_name = name + self.ofport = port + self.vif_mac = mac + + +class TestOVSFirewallDriver(base.BaseTestCase): + def setUp(self): + super(TestOVSFirewallDriver, self).setUp() + self._mock_ovs_br = mock.patch.object( + ovs_lib, 'OVSBridge', autospec=True) + mock_bridge = self._mock_ovs_br.start() + mock_agent_api = mock.patch.object( + ovs_ext_api.OVSAgentExtensionAPI, 'request_int_br', + return_value=mock_bridge).start() + self.firewall = ovsfw.OVSFirewallDriver(mock_agent_api) + self.mock_bridge = self.firewall.int_br + self.mock_bridge.reset_mock() + self.fake_ovs_port = FakeOVSPort('port', 1, '00:00:00:00:00:00') + self.mock_bridge.br.get_vif_port_by_id.return_value = \ + self.fake_ovs_port + + def tearDown(self): + self._mock_ovs_br.stop() + super(TestOVSFirewallDriver, self).tearDown() + + def _prepare_firewall_group(self): + ingress_rules = [ + {'position': '1', + 'protocol': 'tcp', + 'ip_version': 4, + 'destination_port': '123', + 'enabled': True, + 'action': 'allow', + 'id': 'fake-fw-rule1'} + ] + egress_rules = [ + {'position': '2', + 'protocol': 'udp', + 'ip_version': 4, + 'enabled': True, + 'action': 'allow', + 'id': 'fake-fw-rule2'}, + {'position': '3', + 'protocol': 'tcp', + 'ip_version': 6, + 'enabled': True, + 'action': 'allow', + 'id': 'fake-fw-rule3'}] + self.firewall.update_firewall_group_rules(1, ingress_rules, []) + self.firewall.update_firewall_group_rules(2, [], egress_rules) + + @property + def port_ofport(self): + return self.mock_bridge.br.get_vif_port_by_id.return_value.ofport + + @property + def port_mac(self): + return self.mock_bridge.br.get_vif_port_by_id.return_value.vif_mac + + def test_initialize_bridge(self): + br = self.firewall.initialize_bridge(self.mock_bridge) + self.assertEqual(br, self.mock_bridge.deferred.return_value) + + def test__add_flow_dl_type_formatted_to_string(self): + dl_type = 0x0800 + self.firewall._add_flow(dl_type=dl_type) + + def test__add_flow_registers_are_replaced(self): + self.firewall._add_flow(in_port=1, reg_port=1, reg_net=2) + expected_calls = {'in_port': 1, + 'reg{:d}'.format(fwaas_ovs_consts.REG_PORT): 1, + 'reg{:d}'.format(fwaas_ovs_consts.REG_NET): 2} + self.mock_bridge.br.add_flow.assert_called_once_with( + **expected_calls) + + def test__drop_all_unmatched_flows(self): + self.firewall._drop_all_unmatched_flows() + expected_calls = [ + mock.call(actions='drop', priority=0, + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE)] + actual_calls = self.firewall.int_br.br.add_flow.call_args_list + self.assertEqual(expected_calls, actual_calls) + + def test_get_or_create_ofport_non_existing(self): + port_dict = { + 'device': 'port-id', + 'firewall_group': 123, + 'lvlan': TESTING_VLAN_TAG, + } + port = self.firewall.get_or_create_ofport(port_dict) + port_dict = { + 'device': 'port-id', + 'firewall_group': 456, + 'lvlan': TESTING_VLAN_TAG, + } + port = self.firewall.get_or_create_ofport(port_dict) + sg1, sg2 = sorted( + self.firewall.fwg_port_map.fw_groups.values(), + key=lambda x: x.id) + self.assertIn(port, self.firewall.fwg_port_map.ports.values()) + self.assertEqual(port.fw_group, sg2) + self.assertEqual(set(), sg1.ports) + self.assertIn(port, sg2.ports) + + def test_get_or_create_ofport_existing(self): + port_dict = { + 'device': 'port-id', + 'firewall_group': 123} + of_port = create_ofport(port_dict) + self.firewall.fwg_port_map.ports[of_port.id] = of_port + port = self.firewall.get_or_create_ofport(port_dict) + [sg1] = sorted(self.firewall.fwg_port_map.fw_groups.values(), + key=lambda x: x.id) + self.assertIs(of_port, port) + self.assertIn(port, self.firewall.fwg_port_map.ports.values()) + self.assertEqual(port.fw_group, sg1) + self.assertIn(port, sg1.ports) + + def test_get_or_create_ofport_changed(self): + port_dict = { + 'device': 'port-id', + 'firewall_group': 123} + of_port = create_ofport(port_dict) + self.firewall.fwg_port_map.ports[of_port.id] = of_port + fake_ovs_port = FakeOVSPort('port', 2, '00:00:00:00:00:00') + self.mock_bridge.br.get_vif_port_by_id.return_value = \ + fake_ovs_port + port = self.firewall.get_or_create_ofport(port_dict) + self.assertEqual(port.ofport, 2) + + def test_get_or_create_ofport_missing(self): + port_dict = { + 'device': 'port-id', + 'firewall_group': 123} + self.mock_bridge.br.get_vif_port_by_id.return_value = None + with testtools.ExpectedException(exceptions.OVSFWaaSPortNotFound): + self.firewall.get_or_create_ofport(port_dict) + + def test_get_or_create_ofport_missing_nocreate(self): + port_dict = { + 'device': 'port-id', + 'firewall_group': 123} + self.mock_bridge.br.get_vif_port_by_id.return_value = None + self.assertIsNone(self.firewall.get_ofport(port_dict)) + self.assertFalse(self.mock_bridge.br.get_vif_port_by_id.called) + + def test_is_port_managed_managed_port(self): + port_dict = {'device': 'port-id'} + self.firewall.fwg_port_map.ports[port_dict['device']] = object() + is_managed = self.firewall.is_port_managed(port_dict) + self.assertTrue(is_managed) + + def test_is_port_managed_not_managed_port(self): + port_dict = {'device': 'port-id'} + is_managed = self.firewall.is_port_managed(port_dict) + self.assertFalse(is_managed) + + def test_prepare_port_filter(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'fixed_ips': [{'subnet_id': "some_subnet_id_here", + 'ip_address': "10.0.0.1"}], + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.prepare_port_filter(port_dict) + exp_egress_classifier = mock.call( + actions='set_field:{:d}->reg5,set_field:{:d}->reg6,' + 'resubmit(,{:d})'.format( + self.port_ofport, TESTING_VLAN_TAG, + fwaas_ovs_consts.FW_BASE_EGRESS_TABLE), + in_port=self.port_ofport, + priority=105, + table=ovs_consts.TRANSIENT_TABLE) + exp_ingress_classifier = mock.call( + actions='set_field:{:d}->reg5,set_field:{:d}->reg6,' + 'strip_vlan,resubmit(,{:d})'.format( + self.port_ofport, TESTING_VLAN_TAG, + fwaas_ovs_consts.FW_BASE_INGRESS_TABLE), + dl_dst=self.port_mac, + dl_vlan='0x%x' % TESTING_VLAN_TAG, + priority=95, + table=ovs_consts.TRANSIENT_TABLE) + filter_rule = mock.call( + actions='ct(commit,zone=NXM_NX_REG6[0..15]),' + 'output:{:d},resubmit(,{:d})'.format( + self.port_ofport, + ovs_consts.ACCEPTED_INGRESS_TRAFFIC_TABLE), + dl_type="0x{:04x}".format(constants.ETHERTYPE_IP), + nw_proto=constants.PROTO_NUM_TCP, + priority=70, + reg5=self.port_ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED, + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + tcp_dst='0x007b') + calls = self.mock_bridge.br.add_flow.call_args_list + for call in exp_ingress_classifier, exp_egress_classifier, filter_rule: + self.assertIn(call, calls) + + def test_prepare_port_filter_in_coexistence_mode(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'fixed_ips': [{'subnet_id': "some_subnet_id_here", + 'ip_address': "10.0.0.1"}], + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.sg_with_ovs = True + self.firewall.prepare_port_filter(port_dict) + exp_egress_classifier = mock.call( + actions='set_field:{:d}->reg5,set_field:{:d}->reg6,' + 'resubmit(,{:d})'.format( + self.port_ofport, TESTING_VLAN_TAG, + fwaas_ovs_consts.FW_BASE_EGRESS_TABLE), + in_port=self.port_ofport, + priority=105, + table=ovs_consts.TRANSIENT_TABLE) + exp_ingress_classifier = mock.call( + actions='set_field:{:d}->reg5,set_field:{:d}->reg6,' + 'strip_vlan,resubmit(,{:d})'.format( + self.port_ofport, TESTING_VLAN_TAG, + fwaas_ovs_consts.FW_BASE_INGRESS_TABLE), + dl_dst=self.port_mac, + dl_vlan='0x%x' % TESTING_VLAN_TAG, + priority=95, + table=ovs_consts.TRANSIENT_TABLE) + filter_rule = mock.call( + actions='resubmit(,{:d})'.format(ovs_consts.RULES_INGRESS_TABLE), + dl_type="0x{:04x}".format(constants.ETHERTYPE_IP), + nw_proto=constants.PROTO_NUM_TCP, + priority=70, + reg5=self.port_ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED, + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + tcp_dst='0x007b') + calls = self.mock_bridge.br.add_flow.call_args_list + for call in exp_ingress_classifier, exp_egress_classifier, filter_rule: + self.assertIn(call, calls) + + def test_prepare_port_filter_port_security_disabled(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'port_security_enabled': False} + self._prepare_firewall_group() + with mock.patch.object( + self.firewall, 'initialize_port_flows') as m_init_flows: + self.firewall.prepare_port_filter(port_dict) + self.assertFalse(m_init_flows.called) + + def test_prepare_port_filter_initialized_port(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.prepare_port_filter(port_dict) + self.assertFalse(self.mock_bridge.br.delete_flows.called) + self.firewall.prepare_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + + def test_update_port_filter(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.prepare_port_filter(port_dict) + port_dict['firewall_group'] = 2 + self.mock_bridge.reset_mock() + + self.firewall.update_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + filter_rules = [ + mock.call( + actions='resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + dl_type="0x{:04x}".format(constants.ETHERTYPE_IP), + nw_proto=constants.PROTO_NUM_UDP, + priority=71, + ct_state=fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED, + reg5=self.port_ofport, + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE), + # XXX FIXME NOTE(ivasilevskaya) this test originally tested that + # flows for SG with remote_group=this group were generated with + # proper conjunction action. If the original idea that conj_manager + # isn't needed for firewall groups proves to be wrong this needs to + # be revizited and properly fixed/covered with tests + mock.call( + actions='resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED_NOT_REPLY, + dl_type=mock.ANY, + nw_proto=6, + priority=70, reg5=self.port_ofport, + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE)] + self.mock_bridge.br.add_flow.assert_has_calls(filter_rules, + any_order=True) + + def test_update_port_filter_in_coexistence_mode(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.sg_with_ovs = True + self.firewall.prepare_port_filter(port_dict) + port_dict['firewall_group'] = 2 + self.mock_bridge.reset_mock() + + self.firewall.update_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + filter_rules = [ + mock.call( + actions='resubmit(,{:d})'.format( + ovs_consts.RULES_EGRESS_TABLE), + dl_type="0x{:04x}".format(constants.ETHERTYPE_IP), + nw_proto=constants.PROTO_NUM_UDP, + priority=71, + ct_state=fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED, + reg5=self.port_ofport, + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE), + # XXX FIXME NOTE(ivasilevskaya) this test originally tested that + # flows for SG with remote_group=this group were generated with + # proper conjunction action. If the original idea that conj_manager + # isn't needed for firewall groups proves to be wrong this needs to + # be revizited and properly fixed/covered with tests + mock.call( + actions='resubmit(,{:d})'.format( + ovs_consts.RULES_EGRESS_TABLE), + ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED_NOT_REPLY, + dl_type=mock.ANY, + nw_proto=6, + priority=70, reg5=self.port_ofport, + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE)] + self.mock_bridge.br.add_flow.assert_has_calls(filter_rules, + any_order=True) + + def test_update_port_filter_create_new_port_if_not_present(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1} + self._prepare_firewall_group() + with mock.patch.object( + self.firewall, 'prepare_port_filter') as prepare_mock: + self.firewall.update_port_filter(port_dict) + self.assertTrue(prepare_mock.called) + + def test_update_port_filter_port_security_disabled(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.prepare_port_filter(port_dict) + port_dict['port_security_enabled'] = False + self.firewall.update_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + + def test_remove_port_filter(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.prepare_port_filter(port_dict) + self.firewall.remove_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + self.assertIn(1, self.firewall.fwg_to_delete) + + def test_remove_port_filter_port_security_disabled(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1} + self.firewall.remove_port_filter(port_dict) + self.assertFalse(self.mock_bridge.br.delete_flows.called) + + def test_update_firewall_group_rules(self): + """Just make sure it doesn't crash""" + new_rules_ingress = [ + {'ip_version': 4, + 'action': 'allow', + 'protocol': constants.PROTO_NAME_ICMP}, + {'ip_version': 4, + 'direction': 'deny'}] + self.firewall.update_firewall_group_rules(1, new_rules_ingress, []) + + def test__cleanup_stale_sg(self): + self._prepare_firewall_group() + self.firewall.fwg_to_delete = {1} + with mock.patch.object(self.firewall.fwg_port_map, + 'delete_fwg') as delete_fwg_mock: + self.firewall._cleanup_stale_fwg() + delete_fwg_mock.assert_called_once_with(1) + + def test_get_ovs_port(self): + ovs_port = self.firewall.get_ovs_port('port_id') + self.assertEqual(self.fake_ovs_port, ovs_port) + + def test_get_ovs_port_non_existent(self): + self.mock_bridge.br.get_vif_port_by_id.return_value = None + with testtools.ExpectedException(exceptions.OVSFWaaSPortNotFound): + self.firewall.get_ovs_port('port_id') + + def test__initialize_egress_no_port_security_sends_to_egress(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self.firewall._initialize_egress_no_port_security(port_dict) + expected_call = mock.call( + table=ovs_consts.TRANSIENT_TABLE, + priority=100, + in_port=self.fake_ovs_port.ofport, + actions='set_field:%d->reg%d,' + 'set_field:%d->reg%d,' + 'resubmit(,%d)' % ( + self.fake_ovs_port.ofport, + fwaas_ovs_consts.REG_PORT, + TESTING_VLAN_TAG, + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE) + ) + calls = self.mock_bridge.br.add_flow.call_args_list + self.assertIn(expected_call, calls) + + def test__initialize_egress_no_port_security_no_tag(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': None} + self.firewall._initialize_egress_no_port_security(port_dict) + self.assertFalse(self.mock_bridge.br.add_flow.called) + + def test__remove_egress_no_port_security_deletes_flow(self): + self.mock_bridge.br.db_get_val.return_value = {'tag': TESTING_VLAN_TAG} + self.firewall.fwg_port_map.unfiltered['port_id'] = 1 + self.firewall._remove_egress_no_port_security('port_id') + expected_call = mock.call( + table=ovs_consts.TRANSIENT_TABLE, + in_port=self.fake_ovs_port.ofport, + ) + calls = self.mock_bridge.br.delete_flows.call_args_list + self.assertIn(expected_call, calls) + + def test__remove_egress_no_port_security_no_tag(self): + self.mock_bridge.br.db_get_val.return_value = {} + self.firewall._remove_egress_no_port_security('port_id') + self.assertFalse(self.mock_bridge.br.delete_flows.called) diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/test_rules.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/test_rules.py new file mode 100644 index 000000000..22ab7327d --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/l2/openvswitch_firewall/test_rules.py @@ -0,0 +1,339 @@ +# Copyright 2015 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from neutron_lib import constants + +from neutron.tests import base + +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import constants as fwaas_ovs_consts +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import firewall as ovsfw +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\ + openvswitch_firewall import rules + +TESTING_VLAN_TAG = 1 + + +class TestIsValidPrefix(base.BaseTestCase): + def test_valid_prefix_ipv4(self): + is_valid = rules.is_valid_prefix('10.0.0.0/0') + self.assertTrue(is_valid) + + def test_invalid_prefix_ipv4(self): + is_valid = rules.is_valid_prefix('0.0.0.0/0') + self.assertFalse(is_valid) + + def test_valid_prefix_ipv6(self): + is_valid = rules.is_valid_prefix('ffff::0/0') + self.assertTrue(is_valid) + + def test_invalid_prefix_ipv6(self): + is_valid = rules.is_valid_prefix('0000:0::0/0') + self.assertFalse(is_valid) + is_valid = rules.is_valid_prefix('::0/0') + self.assertFalse(is_valid) + is_valid = rules.is_valid_prefix('::/0') + self.assertFalse(is_valid) + + +class TestCreateFlowsFromRuleAndPort(base.BaseTestCase): + def setUp(self): + super(TestCreateFlowsFromRuleAndPort, self).setUp() + ovs_port = mock.Mock(vif_mac='00:00:00:00:00:00') + ovs_port.ofport = 1 + port_dict = {'device': 'port_id'} + self.port = ovsfw.OFPort( + port_dict, ovs_port, vlan_tag=TESTING_VLAN_TAG) + + self._mock_create_flows = mock.patch.object( + rules, 'create_protocol_flows') + self.create_flows_mock = self._mock_create_flows.start() + + def tearDown(self): + self._mock_create_flows.stop() + super(TestCreateFlowsFromRuleAndPort, self).tearDown() + + @property + def passed_flow_template(self): + return self.create_flows_mock.call_args[0][1] + + def _test_create_flows_from_rule_and_port_helper( + self, rule, expected_template): + rules.create_flows_from_rule_and_port(rule, self.port) + + self.assertEqual(expected_template, self.passed_flow_template) + + def test_create_flows_from_rule_and_port_no_ip_ipv4(self): + rule = { + 'ethertype': constants.IPv4, + 'direction': constants.INGRESS_DIRECTION, + } + expected_template = { + 'priority': 70, + 'dl_type': constants.ETHERTYPE_IP, + 'reg_port': self.port.ofport, + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + def test_create_flows_from_rule_and_port_src_and_dst_ipv4(self): + rule = { + 'ethertype': constants.IPv4, + 'direction': constants.INGRESS_DIRECTION, + 'source_ip_prefix': '192.168.0.0/24', + 'dest_ip_prefix': '10.0.0.1/32', + } + expected_template = { + 'priority': 70, + 'dl_type': constants.ETHERTYPE_IP, + 'reg_port': self.port.ofport, + 'nw_src': '192.168.0.0/24', + 'nw_dst': '10.0.0.1/32', + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + def test_create_flows_from_rule_and_port_src_and_dst_with_zero_ipv4(self): + rule = { + 'ethertype': constants.IPv4, + 'direction': constants.INGRESS_DIRECTION, + 'source_ip_prefix': '192.168.0.0/24', + 'dest_ip_prefix': '0.0.0.0/0', + } + expected_template = { + 'priority': 70, + 'dl_type': constants.ETHERTYPE_IP, + 'reg_port': self.port.ofport, + 'nw_src': '192.168.0.0/24', + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + def test_create_flows_from_rule_and_port_no_ip_ipv6(self): + rule = { + 'ethertype': constants.IPv6, + 'direction': constants.INGRESS_DIRECTION, + } + expected_template = { + 'priority': 70, + 'dl_type': constants.ETHERTYPE_IPV6, + 'reg_port': self.port.ofport, + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + def test_create_flows_from_rule_and_port_src_and_dst_ipv6(self): + rule = { + 'ethertype': constants.IPv6, + 'direction': constants.INGRESS_DIRECTION, + 'source_ip_prefix': '2001:db8:bbbb::1/64', + 'dest_ip_prefix': '2001:db8:aaaa::1/64', + } + expected_template = { + 'priority': 70, + 'dl_type': constants.ETHERTYPE_IPV6, + 'reg_port': self.port.ofport, + 'ipv6_src': '2001:db8:bbbb::1/64', + 'ipv6_dst': '2001:db8:aaaa::1/64', + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + def test_create_flows_from_rule_and_port_src_and_dst_with_zero_ipv6(self): + rule = { + 'ethertype': constants.IPv6, + 'direction': constants.INGRESS_DIRECTION, + 'source_ip_prefix': '2001:db8:bbbb::1/64', + 'dest_ip_prefix': '::/0', + } + expected_template = { + 'priority': 70, + 'dl_type': constants.ETHERTYPE_IPV6, + 'reg_port': self.port.ofport, + 'ipv6_src': '2001:db8:bbbb::1/64', + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + +class TestCreateProtocolFlows(base.BaseTestCase): + def setUp(self): + super(TestCreateProtocolFlows, self).setUp() + ovs_port = mock.Mock(vif_mac='00:00:00:00:00:00') + ovs_port.ofport = 1 + port_dict = {'device': 'port_id'} + self.port = ovsfw.OFPort( + port_dict, ovs_port, vlan_tag=TESTING_VLAN_TAG) + + def _test_create_protocol_flows_helper(self, direction, rule, + expected_flows): + flow_template = {'some_settings': 'foo'} + for flow in expected_flows: + flow.update(flow_template) + flows = rules.create_protocol_flows( + direction, flow_template, self.port, rule) + self.assertEqual(expected_flows, flows) + + def test_create_protocol_flows_ingress(self): + rule = {'protocol': constants.PROTO_NUM_TCP} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + 'actions': 'output:1', + 'nw_proto': constants.PROTO_NUM_TCP + }] + self._test_create_protocol_flows_helper( + constants.INGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_egress(self): + rule = {'protocol': constants.PROTO_NUM_TCP} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_TCP, + }] + self._test_create_protocol_flows_helper( + constants.EGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_no_protocol(self): + rule = {} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + }] + self._test_create_protocol_flows_helper( + constants.EGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_icmp6(self): + rule = {'ethertype': constants.IPv6, + 'protocol': constants.PROTO_NUM_IPV6_ICMP} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_IPV6_ICMP, + }] + self._test_create_protocol_flows_helper( + constants.EGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_port_range(self): + rule = {'ethertype': constants.IPv4, + 'protocol': constants.PROTO_NUM_TCP, + 'port_range_min': 22, + 'port_range_max': 23} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_TCP, + 'tcp_dst': '0x0016/0xfffe' + }] + self._test_create_protocol_flows_helper( + constants.EGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_icmp(self): + rule = {'ethertype': constants.IPv4, + 'protocol': constants.PROTO_NUM_ICMP, + 'port_range_min': 0} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_ICMP, + 'icmp_type': 0 + }] + self._test_create_protocol_flows_helper( + constants.EGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_ipv6_icmp(self): + rule = {'ethertype': constants.IPv6, + 'protocol': constants.PROTO_NUM_IPV6_ICMP, + 'port_range_min': 5, + 'port_range_max': 0} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_IPV6_ICMP, + 'icmp_type': 5, + 'icmp_code': 0, + }] + self._test_create_protocol_flows_helper( + constants.EGRESS_DIRECTION, rule, expected_flows) + + +class TestCreatePortRangeFlows(base.BaseTestCase): + def _test_create_port_range_flows_helper(self, expected_flows, rule): + flow_template = {'some_settings': 'foo'} + for flow in expected_flows: + flow.update(flow_template) + port_range_flows = rules.create_port_range_flows(flow_template, rule) + self.assertEqual(expected_flows, port_range_flows) + + def test_create_port_range_flows_with_source_and_destination(self): + rule = { + 'protocol': constants.PROTO_NUM_TCP, + 'source_port_range_min': 123, + 'source_port_range_max': 124, + 'port_range_min': 10, + 'port_range_max': 11, + } + expected_flows = [ + {'tcp_src': '0x007b', 'tcp_dst': '0x000a/0xfffe'}, + {'tcp_src': '0x007c', 'tcp_dst': '0x000a/0xfffe'}, + ] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_flows_with_source(self): + rule = { + 'protocol': constants.PROTO_NUM_TCP, + 'source_port_range_min': 123, + 'source_port_range_max': 124, + } + expected_flows = [ + {'tcp_src': '0x007b'}, + {'tcp_src': '0x007c'}, + ] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_flows_with_destination(self): + rule = { + 'protocol': constants.PROTO_NUM_TCP, + 'port_range_min': 10, + 'port_range_max': 11, + } + expected_flows = [ + {'tcp_dst': '0x000a/0xfffe'}, + ] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_flows_without_port_range(self): + rule = { + 'protocol': constants.PROTO_NUM_TCP, + } + expected_flows = [] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_with_icmp_protocol(self): + # NOTE: such call is prevented by create_protocols_flows + rule = { + 'protocol': constants.PROTO_NUM_ICMP, + 'port_range_min': 10, + 'port_range_max': 11, + } + expected_flows = [] + self._test_create_port_range_flows_helper(expected_flows, rule) diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_iptables_fwaas_v2.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_iptables_fwaas_v2.py new file mode 100644 index 000000000..c0770de2c --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_iptables_fwaas_v2.py @@ -0,0 +1,520 @@ +# Copyright (c) 2016 +# 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. + +import copy + +import mock +from neutron.tests import base +from neutron.tests.unit.api.v2 import test_base as test_api_v2 + +import neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.\ + iptables_fwaas_v2 as fwaas + + +_uuid = test_api_v2._uuid +FAKE_SRC_PREFIX = '10.0.0.0/24' +FAKE_DST_PREFIX = '20.0.0.0/24' +FAKE_PROTOCOL = 'tcp' +FAKE_SRC_PORT = 5000 +FAKE_DST_PORT = 22 +FAKE_FW_ID = 'fake-fw-uuid' +FAKE_PORT_IDS = ('1_fake-port-uuid', '2_fake-port-uuid') +FW_LEGACY = 'legacy' +MAX_INTF_NAME_LEN = 14 + + +class IptablesFwaasTestCase(base.BaseTestCase): + def setUp(self): + super(IptablesFwaasTestCase, self).setUp() + self.iptables_cls_p = mock.patch( + 'neutron.agent.linux.iptables_manager.IptablesManager') + self.iptables_cls_p.start() + self.firewall = fwaas.IptablesFwaasDriver() + self.firewall.conntrack.delete_entries = mock.Mock() + self.firewall.conntrack.flush_entries = mock.Mock() + + def _fake_rules_v4(self, fwid, apply_list): + rule_list = [] + rule1 = {'enabled': True, + 'action': 'allow', + 'ip_version': 4, + 'protocol': 'tcp', + 'destination_port': '80', + 'source_ip_address': '10.24.4.2', + 'id': 'fake-fw-rule1'} + rule2 = {'enabled': True, + 'action': 'deny', + 'ip_version': 4, + 'protocol': 'tcp', + 'destination_port': '22', + 'id': 'fake-fw-rule2'} + rule3 = {'enabled': True, + 'action': 'reject', + 'ip_version': 4, + 'protocol': 'tcp', + 'destination_port': '23', + 'id': 'fake-fw-rule3'} + ingress_chain = ('iv4%s' % fwid)[:11] + egress_chain = ('ov4%s' % fwid)[:11] + for router_info_inst, port_ids in apply_list: + v4filter_inst = router_info_inst.iptables_manager.ipv4['filter'] + v4filter_inst.chains.append(ingress_chain) + v4filter_inst.chains.append(egress_chain) + rule_list.append(rule1) + rule_list.append(rule2) + rule_list.append(rule3) + return rule_list + + def _fake_rules_v6(self, fwid, apply_list): + rule_list = [] + rule1 = {'enabled': True, + 'action': 'allow', + 'ip_version': 6, + 'protocol': 'icmp', + 'destination_ip_address': '2001:db8::2', + 'id': 'fake-fw-rule1'} + ingress_chain = ('iv6%s' % fwid)[:11] + egress_chain = ('ov6%s' % fwid)[:11] + for router_info_inst, port_ids in apply_list: + v6filter_inst = router_info_inst.iptables_manager.ipv6['filter'] + v6filter_inst.chains.append(ingress_chain) + v6filter_inst.chains.append(egress_chain) + rule_list.append(rule1) + return rule_list + + def _fake_firewall_no_rule(self): + rule_list = [] + fw_inst = {'id': FAKE_FW_ID, + 'admin_state_up': True, + 'tenant_id': 'tenant-uuid', + 'egress_rule_list': rule_list, + 'ingress_rule_list': rule_list} + return fw_inst + + def _fake_firewall(self, rule_list): + _rule_list = copy.deepcopy(rule_list) + for rule in _rule_list: + rule['position'] = str(_rule_list.index(rule)) + fw_inst = {'id': FAKE_FW_ID, + 'admin_state_up': True, + 'tenant_id': 'tenant-uuid', + 'egress_rule_list': _rule_list, + 'ingress_rule_list': _rule_list} + return fw_inst + + def _fake_firewall_with_admin_down(self, rule_list): + fw_inst = {'id': FAKE_FW_ID, + 'admin_state_up': False, + 'tenant_id': 'tenant-uuid', + 'egress_rule_list': rule_list, + 'ingress_rule_list': rule_list} + return fw_inst + + def _fake_apply_list(self, router_count=1, distributed=False, + distributed_mode=None): + apply_list = [] + while router_count > 0: + iptables_inst = mock.Mock() + if distributed: + router_inst = {'distributed': distributed} + else: + router_inst = {} + v4filter_inst = mock.Mock() + v6filter_inst = mock.Mock() + v4filter_inst.chains = [] + v6filter_inst.chains = [] + iptables_inst.ipv4 = {'filter': v4filter_inst} + iptables_inst.ipv6 = {'filter': v6filter_inst} + router_info_inst = mock.Mock() + router_info_inst.iptables_manager = iptables_inst + router_info_inst.snat_iptables_manager = iptables_inst + if distributed_mode == 'dvr': + router_info_inst.rtr_fip_connect = True + router_info_inst.router = router_inst + apply_list.append((router_info_inst, FAKE_PORT_IDS)) + router_count -= 1 + return apply_list + + def _get_intf_name(self, if_prefix, port_id): + _name = "%s%s" % (if_prefix, port_id) + return _name[:MAX_INTF_NAME_LEN] + + def _setup_firewall_with_rules(self, func, router_count=1, + distributed=False, distributed_mode=None): + apply_list = self._fake_apply_list(router_count=router_count, + distributed=distributed, distributed_mode=distributed_mode) + rule_list = self._fake_rules_v4(FAKE_FW_ID, apply_list) + firewall = self._fake_firewall(rule_list) + if distributed: + if distributed_mode == 'dvr_snat': + if_prefix = 'sg-' + if distributed_mode == 'dvr': + if_prefix = 'rfp-' + else: + if_prefix = 'qr-' + distributed_mode = 'legacy' + func(distributed_mode, apply_list, firewall) + binary_name = fwaas.iptables_manager.binary_name + dropped = '%s-dropped' % binary_name + accepted = '%s-accepted' % binary_name + rejected = '%s-rejected' % binary_name + invalid_rule = '-m state --state INVALID -j %s' % dropped + est_rule = '-m state --state RELATED,ESTABLISHED -j ACCEPT' + rule1 = '-p tcp -s 10.24.4.2/32 -m tcp --dport 80 -j %s' % accepted + rule2 = '-p tcp -m tcp --dport 22 -j %s' % dropped + rule3 = '-p tcp -m tcp --dport 23 -j %s' % rejected + ingress_chain = 'iv4%s' % firewall['id'] + egress_chain = 'ov4%s' % firewall['id'] + ipt_mgr_ichain = '%s-%s' % (binary_name, ingress_chain[:11]) + ipt_mgr_echain = '%s-%s' % (binary_name, egress_chain[:11]) + for router_info_inst, port_ids in apply_list: + v4filter_inst = router_info_inst.iptables_manager.ipv4['filter'] + calls = [mock.call.remove_chain('iv4fake-fw-uuid'), + mock.call.remove_chain('ov4fake-fw-uuid'), + mock.call.remove_chain('fwaas-default-policy'), + mock.call.add_chain('fwaas-default-policy'), + mock.call.add_rule( + 'fwaas-default-policy', '-j %s' % dropped), + mock.call.add_chain(ingress_chain), + mock.call.add_rule(ingress_chain, invalid_rule), + mock.call.add_rule(ingress_chain, est_rule), + mock.call.add_chain(egress_chain), + mock.call.add_rule(egress_chain, invalid_rule), + mock.call.add_rule(egress_chain, est_rule), + mock.call.add_rule(ingress_chain, rule1), + mock.call.add_rule(ingress_chain, rule2), + mock.call.add_rule(ingress_chain, rule3), + mock.call.add_rule(egress_chain, rule1), + mock.call.add_rule(egress_chain, rule2), + mock.call.add_rule(egress_chain, rule3) + ] + + for port in FAKE_PORT_IDS: + intf_name = self._get_intf_name(if_prefix, port) + calls.append(mock.call.add_rule('FORWARD', + '-o %s -j %s' % (intf_name, ipt_mgr_ichain))) + for port in FAKE_PORT_IDS: + intf_name = self._get_intf_name(if_prefix, port) + calls.append(mock.call.add_rule('FORWARD', + '-i %s -j %s' % (intf_name, ipt_mgr_echain))) + + for direction in ['o', 'i']: + for port_id in FAKE_PORT_IDS: + intf_name = self._get_intf_name(if_prefix, port_id) + calls.append(mock.call.add_rule('FORWARD', + '-%s %s -j %s-fwaas-defau' % (direction, + intf_name, binary_name))) + v4filter_inst.assert_has_calls(calls) + + def _setup_firewall_with_rules_v6(self, func, router_count=1, + distributed=False, distributed_mode=None): + apply_list = self._fake_apply_list(router_count=router_count, + distributed=distributed, distributed_mode=distributed_mode) + rule_list = self._fake_rules_v6(FAKE_FW_ID, apply_list) + firewall = self._fake_firewall(rule_list) + if distributed: + if distributed_mode == 'dvr_snat': + if_prefix = 'sg-' + if distributed_mode == 'dvr': + if_prefix = 'rfp-' + else: + if_prefix = 'qr-' + distributed_mode = 'legacy' + func(distributed_mode, apply_list, firewall) + binary_name = fwaas.iptables_manager.binary_name + dropped = '%s-dropped' % binary_name + accepted = '%s-accepted' % binary_name + invalid_rule = '-m state --state INVALID -j %s' % dropped + est_rule = '-m state --state RELATED,ESTABLISHED -j ACCEPT' + rule1 = '-p ipv6-icmp -d 2001:db8::2/128 -j %s' % accepted + ingress_chain = 'iv6%s' % firewall['id'] + egress_chain = 'ov6%s' % firewall['id'] + ipt_mgr_ichain = '%s-%s' % (binary_name, ingress_chain[:11]) + ipt_mgr_echain = '%s-%s' % (binary_name, egress_chain[:11]) + for router_info_inst, port_ids in apply_list: + v6filter_inst = router_info_inst.iptables_manager.ipv6['filter'] + calls = [mock.call.remove_chain('iv6fake-fw-uuid'), + mock.call.remove_chain('ov6fake-fw-uuid'), + mock.call.remove_chain('fwaas-default-policy'), + mock.call.add_chain('fwaas-default-policy'), + mock.call.add_rule( + 'fwaas-default-policy', '-j %s' % dropped), + mock.call.add_chain(ingress_chain), + mock.call.add_rule(ingress_chain, invalid_rule), + mock.call.add_rule(ingress_chain, est_rule), + mock.call.add_chain(egress_chain), + mock.call.add_rule(egress_chain, invalid_rule), + mock.call.add_rule(egress_chain, est_rule), + mock.call.add_rule(ingress_chain, rule1), + mock.call.add_rule(egress_chain, rule1) + ] + for port in FAKE_PORT_IDS: + intf_name = self._get_intf_name(if_prefix, port) + calls.append(mock.call.add_rule('FORWARD', + '-o %s -j %s' % (intf_name, ipt_mgr_ichain))) + for port in FAKE_PORT_IDS: + intf_name = self._get_intf_name(if_prefix, port) + calls.append(mock.call.add_rule('FORWARD', + '-i %s -j %s' % (intf_name, ipt_mgr_echain))) + + for direction in ['o', 'i']: + for port_id in FAKE_PORT_IDS: + intf_name = self._get_intf_name(if_prefix, port_id) + calls.append(mock.call.add_rule('FORWARD', + '-%s %s -j %s-fwaas-defau' % (direction, + intf_name, binary_name))) + + v6filter_inst.assert_has_calls(calls) + + def test_create_firewall_group_no_rules(self): + apply_list = self._fake_apply_list() + first_ri = apply_list[0][0] + firewall = self._fake_firewall_no_rule() + self.firewall.create_firewall_group('legacy', apply_list, firewall) + binary_name = fwaas.iptables_manager.binary_name + dropped = '%s-dropped' % binary_name + invalid_rule = '-m state --state INVALID -j %s' % dropped + est_rule = '-m state --state RELATED,ESTABLISHED -j ACCEPT' + for ip_version in (4, 6): + ingress_chain = ('iv%s%s' % (ip_version, firewall['id'])) + egress_chain = ('ov%s%s' % (ip_version, firewall['id'])) + calls = [mock.call.remove_chain( + 'iv%sfake-fw-uuid' % ip_version), + mock.call.remove_chain( + 'ov%sfake-fw-uuid' % ip_version), + mock.call.remove_chain('fwaas-default-policy'), + mock.call.add_chain('fwaas-default-policy'), + mock.call.add_rule( + 'fwaas-default-policy', '-j %s' % dropped), + mock.call.add_chain(ingress_chain), + mock.call.add_rule(ingress_chain, invalid_rule), + mock.call.add_rule(ingress_chain, est_rule), + mock.call.add_chain(egress_chain), + mock.call.add_rule(egress_chain, invalid_rule), + mock.call.add_rule(egress_chain, est_rule)] + + for port_id in FAKE_PORT_IDS: + for direction in ['o', 'i']: + mock.call.add_rule('FORWARD', + '-%s qr-%s -j %s-fwaas-defau' % (port_id, + direction, + binary_name)) + if ip_version == 4: + v4filter_inst = first_ri.iptables_manager.ipv4['filter'] + v4filter_inst.assert_has_calls(calls) + else: + v6filter_inst = first_ri.iptables_manager.ipv6['filter'] + v6filter_inst.assert_has_calls(calls) + + def test_create_firewall_group_with_rules(self): + self._setup_firewall_with_rules(self.firewall.create_firewall_group) + + def test_create_firewall_group_with_rules_v6(self): + self._setup_firewall_with_rules_v6(self.firewall.create_firewall_group) + + def test_create_firewall_group_with_rules_without_distributed_attr(self): + self._setup_firewall_with_rules(self.firewall.create_firewall_group, + distributed=None) + + def test_create_firewall_group_with_rules_two_routers(self): + self._setup_firewall_with_rules(self.firewall.create_firewall_group, + router_count=2) + + def test_update_firewall_group_with_rules(self): + self._setup_firewall_with_rules(self.firewall.update_firewall_group) + + def test_update_firewall_group_with_rules_v6(self): + self._setup_firewall_with_rules_v6(self.firewall.update_firewall_group) + + def test_update_firewall_group_with_rules_without_distributed_attr(self): + self._setup_firewall_with_rules(self.firewall.update_firewall_group, + distributed=None) + + def _test_delete_firewall_group(self, distributed=False): + apply_list = self._fake_apply_list(distributed=distributed) + first_ri = apply_list[0][0] + firewall = self._fake_firewall_no_rule() + self.firewall.delete_firewall_group('legacy', apply_list, firewall) + ingress_chain = 'iv4%s' % firewall['id'] + egress_chain = 'ov4%s' % firewall['id'] + calls = [mock.call.remove_chain(ingress_chain), + mock.call.remove_chain(egress_chain), + mock.call.remove_chain('fwaas-default-policy')] + first_ri.iptables_manager.ipv4['filter'].assert_has_calls(calls) + + def test_delete_firewall_group(self): + self._test_delete_firewall_group() + + def test_delete_firewall_group_without_distributed_attr(self): + self._test_delete_firewall_group(distributed=None) + + def test_create_firewall_group_with_admin_down(self): + apply_list = self._fake_apply_list() + first_ri = apply_list[0][0] + rule_list = self._fake_rules_v4(FAKE_FW_ID, apply_list) + firewall = self._fake_firewall_with_admin_down(rule_list) + binary_name = fwaas.iptables_manager.binary_name + dropped = '%s-dropped' % binary_name + self.firewall.create_firewall_group('legacy', apply_list, firewall) + calls = [mock.call.remove_chain('iv4fake-fw-uuid'), + mock.call.remove_chain('ov4fake-fw-uuid'), + mock.call.remove_chain('fwaas-default-policy'), + mock.call.add_chain('fwaas-default-policy'), + mock.call.add_rule('fwaas-default-policy', '-j %s' % dropped)] + first_ri.iptables_manager.ipv4['filter'].assert_has_calls(calls) + + def test_create_firewall_group_with_rules_dvr_snat(self): + self._setup_firewall_with_rules(self.firewall.create_firewall_group, + distributed=True, distributed_mode='dvr_snat') + + def test_update_firewall_group_with_rules_dvr_snat(self): + self._setup_firewall_with_rules(self.firewall.update_firewall_group, + distributed=True, distributed_mode='dvr_snat') + + def test_create_firewall_group_with_rules_dvr(self): + self._setup_firewall_with_rules(self.firewall.create_firewall_group, + distributed=True, distributed_mode='dvr') + + def test_update_firewall_group_with_rules_dvr(self): + self._setup_firewall_with_rules(self.firewall.update_firewall_group, + distributed=True, distributed_mode='dvr') + + def test_remove_conntrack_new_firewall(self): + apply_list = self._fake_apply_list() + firewall = self._fake_firewall_no_rule() + self.firewall.create_firewall_group(FW_LEGACY, apply_list, firewall) + for router_info_inst, port_ids in apply_list: + namespace = router_info_inst.iptables_manager.namespace + calls = [mock.call(namespace)] + self.firewall.conntrack.flush_entries.assert_has_calls(calls) + + def test_remove_conntrack_inserted_rule(self): + apply_list = self._fake_apply_list() + rule_list = self._fake_rules_v4(FAKE_FW_ID, apply_list) + firewall = self._fake_firewall(rule_list) + self.firewall.create_firewall_group(FW_LEGACY, apply_list, firewall) + self.firewall.pre_firewall = dict(firewall) + insert_rule = {'enabled': True, + 'action': 'deny', + 'ip_version': 4, + 'protocol': 'icmp', + 'id': 'fake-fw-rule'} + rule_list.insert(2, insert_rule) + firewall = self._fake_firewall(rule_list) + self.firewall.update_firewall_group(FW_LEGACY, apply_list, firewall) + rules_changed = [ + {'destination_port': '23', + 'position': '2', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'}, + {'destination_port': '23', + 'position': '3', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'} + ] * 2 # Egress and ingress rule lists + rules_inserted = [ + {'id': 'fake-fw-rule', + 'protocol': 'icmp', + 'ip_version': 4, + 'enabled': True, + 'action': 'deny', + 'position': '2'} + ] * 2 # Egress and ingress rule lists + for router_info_inst, port_ids in apply_list: + namespace = router_info_inst.iptables_manager.namespace + self.firewall.conntrack.delete_entries.assert_called_once_with( + rules_changed + rules_inserted, namespace + ) + + def test_remove_conntrack_removed_rule(self): + apply_list = self._fake_apply_list() + rule_list = self._fake_rules_v4(FAKE_FW_ID, apply_list) + firewall = self._fake_firewall(rule_list) + self.firewall.create_firewall_group(FW_LEGACY, apply_list, firewall) + self.firewall.pre_firewall = dict(firewall) + remove_rule = rule_list[1] + rule_list.remove(remove_rule) + firewall = self._fake_firewall(rule_list) + self.firewall.update_firewall_group(FW_LEGACY, apply_list, firewall) + rules_changed = [ + {'destination_port': '23', + 'position': '2', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'}, + {'destination_port': '23', + 'position': '1', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'} + ] * 2 # Egress and ingress rule lists + rules_removed = [ + {'enabled': True, + 'position': '1', + 'protocol': 'tcp', + 'id': 'fake-fw-rule2', + 'ip_version': 4, + 'action': 'deny', + 'destination_port': '22'} + ] * 2 # Egress and ingress rule lists + for router_info_inst, port_ids in apply_list: + namespace = router_info_inst.iptables_manager.namespace + self.firewall.conntrack.delete_entries.assert_called_once_with( + rules_changed + rules_removed, namespace + ) + + def test_remove_conntrack_changed_rule(self): + apply_list = self._fake_apply_list() + rule_list = self._fake_rules_v4(FAKE_FW_ID, apply_list) + firewall = self._fake_firewall(rule_list) + self.firewall.create_firewall_group(FW_LEGACY, apply_list, firewall) + income_rule = {'enabled': True, + 'action': 'deny', + 'ip_version': 4, + 'protocol': 'tcp', + 'id': 'fake-fw-rule3'} + rule_list[2] = income_rule + firewall = self._fake_firewall(rule_list) + self.firewall.update_firewall_group(FW_LEGACY, apply_list, firewall) + rules_changed = [ + {'id': 'fake-fw-rule3', + 'enabled': True, + 'action': 'reject', + 'position': '2', + 'destination_port': '23', + 'ip_version': 4, + 'protocol': 'tcp'}, + {'position': '2', + 'enabled': True, + 'action': 'deny', + 'id': 'fake-fw-rule3', + 'ip_version': 4, + 'protocol': 'tcp'} + ] * 2 # Egress and ingress rule lists + for router_info_inst, port_ids in apply_list: + namespace = router_info_inst.iptables_manager.namespace + self.firewall.conntrack.delete_entries.assert_called_once_with( + rules_changed, namespace + ) diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_legacy_conntrack.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_legacy_conntrack.py new file mode 100644 index 000000000..a2dc539b1 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_legacy_conntrack.py @@ -0,0 +1,177 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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. + +import mock +import testtools + +from neutron.tests import base +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux \ + import legacy_conntrack +from neutron_lib import constants + + +FW_RULES = [ + {'position': '1', + 'protocol': 'icmp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule1'}, + {'source_port': '0:10', + 'destination_port': '0:10', + 'position': '2', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule2'}, + {'source_port': '0:10', + 'destination_port': '0:20', + 'position': '3', + 'protocol': 'udp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'}, + {'source_port': '1', + 'destination_port': '0:10', + 'position': '4', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule5'}, + {'source_port': '0:10', + 'destination_port': None, + 'position': '5', + 'protocol': 'udp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule5'}, + {'source_port': '1', + 'destination_port': '3', + 'position': '6', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule6'}, + {'source_port': '1', + 'destination_port': '2', + 'position': '7', + 'protocol': 'udp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule7'}, +] + +ICMP_ENTRY = (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', '1234') +TCP_ENTRY = (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2') +UDP_ENTRY = (4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2') + +CONNTRACK_LIST = '''\ +icmp 1 27 src=1.1.1.1 dst=2.2.2.2 type=8 code=0 id=18127 src=2.2.2.2 dst=1.1.1.1 type=0 code=0 id=18127 mark=0 use=1 +tcp 6 88 SYN_SENT src=1.1.1.1 dst=2.2.2.2 sport=36567 dport=5000 [UNREPLIED] src=2.2.2.2 dst=1.1.1.1 sport=5000 dport=36567 mark=0 use=1 +unknown 2 551 src=0.0.0.0 dst=224.0.0.1 [UNREPLIED] src=224.0.0.1 dst=0.0.0.0 mark=0 use=1 +udp 17 28 src=0.0.0.0 dst=255.255.255.255 sport=68 dport=67 [UNREPLIED] src=255.255.255.255 dst=0.0.0.0 sport=67 dport=68 mark=0 use=1 +''' # noqa + +ROUTER_NAMESPACE = 'qrouter-fake-namespace' + + +class ConntrackLegacyTestCase(base.BaseTestCase): + def setUp(self): + super(ConntrackLegacyTestCase, self).setUp() + self.utils_exec = mock.Mock() + self.conntrack_driver = legacy_conntrack.ConntrackLegacy() + self.conntrack_driver.initialize(execute=self.utils_exec) + + def test_normalize_filters_tuple(self): + self.assertEqual( + (4, 'udp', [], [], '', ''), + legacy_conntrack.normalize_filters_tuple( + (4, 'udp', [], [], None, None))) + self.assertEqual( + (4, '', [], [], '', ''), + legacy_conntrack.normalize_filters_tuple( + (4, None, [], [], None, None))) + + def test_excecute_command_failed(self): + with testtools.ExpectedException(RuntimeError): + self.conntrack_driver._execute_command(['fake', 'command']) + raise RuntimeError("Failed execute conntrack command fake command") + + def test_flush_entries(self): + self.conntrack_driver.flush_entries(ROUTER_NAMESPACE) + self.utils_exec.assert_called_with( + ['ip', 'netns', 'exec', ROUTER_NAMESPACE, + 'conntrack', '-D'], + check_exit_code=True, + extra_ok_codes=[1], + run_as_root=True, + privsep_exec=True) + + def test_list_entries(self): + def get_contrack_entries(conntrack_cmd): + if 'ipv' + str(constants.IP_VERSION_4) in conntrack_cmd: + return CONNTRACK_LIST + return '' + + self.conntrack_driver._execute_command = mock.Mock( + side_effect=get_contrack_entries) + entries = self.conntrack_driver.list_entries(ROUTER_NAMESPACE) + protocols = set([entry[1] for entry in entries]) + supported_protocols = set(legacy_conntrack.ATTR_POSITIONS.keys()) + self.assertTrue(protocols.issubset(supported_protocols)) + + def test_delete_entries(self): + list_entries_mock = mock.patch( + 'neutron_fwaas.services.firewall.service_drivers.agents.drivers.' + 'linux.legacy_conntrack.ConntrackLegacy.list_entries') + self.list_entries = list_entries_mock.start() + + self.conntrack_driver.list_entries.return_value = [ + ICMP_ENTRY, TCP_ENTRY, UDP_ENTRY] + self.conntrack_driver.delete_entries(FW_RULES, ROUTER_NAMESPACE) + calls = [ + mock.call(['ip', 'netns', 'exec', ROUTER_NAMESPACE, + 'conntrack', '-D', '-f', 'ipv4', '-p', 'icmp', + '--icmp-type', 8, '--icmp-code', 0, + '-s', '1.1.1.1', '-d', '2.2.2.2', '--icmp-id', '1234'], + check_exit_code=True, + extra_ok_codes=[1], + run_as_root=True, + privsep_exec=True), + mock.call(['ip', 'netns', 'exec', ROUTER_NAMESPACE, + 'conntrack', '-D', '-f', 'ipv4', '-p', 'tcp', + '--sport', 1, '--dport', 2, + '-s', '1.1.1.1', '-d', '2.2.2.2'], + check_exit_code=True, + extra_ok_codes=[1], + run_as_root=True, + privsep_exec=True), + mock.call(['ip', 'netns', 'exec', ROUTER_NAMESPACE, + 'conntrack', '-D', '-f', 'ipv4', '-p', 'udp', + '--sport', 1, '--dport', 2, + '-s', '1.1.1.1', '-d', '2.2.2.2'], + check_exit_code=True, + extra_ok_codes=[1], + run_as_root=True, + privsep_exec=True), + + ] + self.utils_exec.assert_has_calls(calls) diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_netlink_conntrack.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_netlink_conntrack.py new file mode 100644 index 000000000..0637459df --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/drivers/linux/test_netlink_conntrack.py @@ -0,0 +1,240 @@ +# Copyright (c) 2017 Fujitsu Limited +# 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. + +import mock + +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux \ + import netlink_conntrack +from neutron_fwaas.tests import base + +FW_RULES = [ + {'position': '1', + 'protocol': 'icmp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule1'}, + {'source_port': '0:10', + 'destination_port': '0:10', + 'position': '2', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule2'}, + {'source_port': '0:10', + 'destination_port': '0:20', + 'position': '3', + 'protocol': 'udp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule3'}, + {'source_port': None, + 'destination_port': '0:10', + 'position': '2', + 'protocol': 'tcp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule5'}, + {'source_port': '0:10', + 'destination_port': None, + 'position': '3', + 'protocol': 'udp', + 'ip_version': 4, + 'enabled': True, + 'action': 'reject', + 'id': 'fake-fw-rule5'}, +] + +ICMP_ENTRY = (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', '1234') +TCP_ENTRY = (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2') +UDP_ENTRY = (4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2') + +ROUTER_NAMESPACE = 'qrouter-fake-namespace' + + +class ConntrackNetlinkTestCase(base.BaseTestCase): + def setUp(self): + super(ConntrackNetlinkTestCase, self).setUp() + self.conntrack_driver = netlink_conntrack.ConntrackNetlink() + self.conntrack_driver.initialize() + nl_flush_entries = mock.patch('neutron_fwaas.privileged.' + 'netlink_lib.flush_entries') + self.flush_entries = nl_flush_entries.start() + nl_list_entries = mock.patch('neutron_fwaas.privileged.' + 'netlink_lib.list_entries') + self.list_entries = nl_list_entries.start() + nl_delete_entries = mock.patch('neutron_fwaas.privileged.' + 'netlink_lib.delete_entries') + self.delete_entries = nl_delete_entries.start() + + def test_flush_entries(self): + self.conntrack_driver.flush_entries(ROUTER_NAMESPACE) + self.flush_entries.assert_called_with(ROUTER_NAMESPACE) + + def test_delete_with_empty_conntrack_entries(self): + self.list_entries.return_value = [] + self.conntrack_driver.delete_entries([], ROUTER_NAMESPACE) + self.list_entries.assert_called_with(ROUTER_NAMESPACE) + self.delete_entries.assert_not_called() + + def test_delete_icmp_entry(self): + """Testing delete an icmp entry + + The icmp entry can be deleted if there is an icmp conntrack entry + matched with an icmp firewall rule. + The information passing to nl_lib.kill_entry will include: + (ipversion, protocol, icmp_type, icmp_code, src_address, dst_addres, + icmp_ip) + """ + self.list_entries.return_value = [ICMP_ENTRY] + self.conntrack_driver.delete_entries(FW_RULES, ROUTER_NAMESPACE) + self.list_entries.assert_called_with(ROUTER_NAMESPACE) + self.delete_entries.assert_called_with([(4, 'icmp', 8, 0, + '1.1.1.1', '2.2.2.2', + '1234')], ROUTER_NAMESPACE) + + def test_delete_tcp_entry(self): + """Testing delete a tcp entry + + The tcp entry can be deleted if there is a tcp conntrack entry + matched with a tcp firewall rule. + The information passing to nl_lib.kill_entry will include: + (ipversion, protocol, src_port, dst_port, src_address, dst_addres) + """ + self.list_entries.return_value = [TCP_ENTRY] + self.conntrack_driver.delete_entries(FW_RULES, ROUTER_NAMESPACE) + self.list_entries.assert_called_with(ROUTER_NAMESPACE) + self.delete_entries.assert_called_with( + [(4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2')], ROUTER_NAMESPACE) + + def test_delete_udp_entry(self): + """Testing delete an udp entry + + The udp entry can be deleted if there is an udp conntrack entry + matched with an udp firewall rule. + The information passing to nl_lib.kill_entry will include: + (ipversion, protocol, src_port, dst_port, src_address, dst_addres) + """ + self.list_entries.return_value = [UDP_ENTRY] + self.conntrack_driver.delete_entries(FW_RULES, ROUTER_NAMESPACE) + self.list_entries.assert_called_with(ROUTER_NAMESPACE) + self.delete_entries.assert_called_with( + [(4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2')], ROUTER_NAMESPACE) + + def test_delete_multiple_entries(self): + self.list_entries.return_value = [ICMP_ENTRY, TCP_ENTRY, UDP_ENTRY] + self.conntrack_driver.delete_entries(FW_RULES, ROUTER_NAMESPACE) + self.list_entries.assert_called_with(ROUTER_NAMESPACE) + self.delete_entries.assert_called_with( + [(4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', '1234'), + (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2'), + (4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2')], ROUTER_NAMESPACE) + + def _test_entry_to_delete(self, rule_filter, entry, expect_result): + is_entry_to_delete = ( + self.conntrack_driver._compare_entry_and_rule(rule_filter, entry)) + self.assertEqual(expect_result, is_entry_to_delete) + + def test_icmp_entry_match_rule(self): + entry = (4, 'icmp', 8, 0, '1.1.1.1', '2.2.2.2', '1234') + rule_filter = (4, 'icmp', None, None) + self._test_entry_to_delete(rule_filter, entry, 0) + + def test_tcp_entry_match_rule(self): + entry = (4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2') + rule_filters = [(4, 'tcp', None, None), + (4, 'tcp', [1], None), + (4, 'tcp', None, [2]), + (4, 'tcp', [1], [2]), + (4, 'tcp', ['0', '10'], ['0', '10']), ] + for rule_filter in rule_filters: + self._test_entry_to_delete(rule_filter, entry, 0) + + def test_udp_entry_match_rule(self): + entry = (4, 'udp', 1, 2, '1.1.1.1', '2.2.2.2') + rule_filters = [(4, 'udp', None, None), + (4, 'udp', [1], None), + (4, 'udp', None, [2]), + (4, 'udp', [1], [2]), + (4, 'udp', ['0', '10'], ['0', '10']), ] + for rule_filter in rule_filters: + self._test_entry_to_delete(rule_filter, entry, 0) + + def test_entry_unmatch_rule(self): + wrong_ipv = [(4, 'tcp', '1', '2', '1.1.1.1', '2.2.2.2'), + (6, 'tcp', None, None), -1] + wrong_proto = [(4, 'tcp', '1', '2', '1.1.1.1', '2.2.2.2'), + (4, 'udp', None, None), -1] + not_in_sport_range = [(4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2'), + (4, 'tcp', ['2', '100'], [2]), -1] + not_in_dport_range = [(4, 'tcp', 1, 2, '1.1.1.1', '2.2.2.2'), + (4, 'tcp', [1], ['3', '100']), -1] + for entry, rule_filter, expect in [ + wrong_ipv, wrong_proto, not_in_sport_range, not_in_dport_range]: + self._test_entry_to_delete(rule_filter, entry, expect) + + def test_get_filter_from_rules(self): + fw_rule_icmp = FW_RULES[0] + fw_rule_port_range = FW_RULES[1] + fw_rule_dest_port = FW_RULES[3] + fw_rule_source_port = FW_RULES[4] + + # filter format: + # ('ip_version', 'protocol', 'source_port', 'destination_port', + # 'source_ip_address', 'destination_ip_address') + + expected_icmp_filter = (4, 'icmp', [], [], [], []) + expected_port_range_filter = (4, 'tcp', ['0', '10'], ['0', '10'], + [], []) + expected_dest_port_filter = (4, 'tcp', [], ['0', '10'], [], []) + expected_source_port_filter = (4, 'udp', ['0', '10'], [], [], []) + + actual_icmp_filter = self.conntrack_driver._get_filter_from_rule( + fw_rule_icmp) + actual_port_range_filter = \ + self.conntrack_driver._get_filter_from_rule(fw_rule_port_range) + actual_dest_port_filter = \ + self.conntrack_driver._get_filter_from_rule(fw_rule_dest_port) + actual_source_port_filter = \ + self.conntrack_driver._get_filter_from_rule(fw_rule_source_port) + + self.assertEqual(expected_icmp_filter, actual_icmp_filter) + self.assertEqual(expected_port_range_filter, actual_port_range_filter) + self.assertEqual(expected_dest_port_filter, actual_dest_port_filter) + self.assertEqual(expected_source_port_filter, + actual_source_port_filter) + + def test_get_entries_to_delete(self): + rule_filters = sorted( + [(4, 'tcp', ['0', '10'], ['1', '10']), + (4, 'udp', ['0', '10'], ['0', '10']), + (4, 'icmp', None, None)]) + TCP_ENTRY_IN_RANGE = (4, 'tcp', 2, 3, '1.1.1.1', '2.2.2.2') + TCP_ENTRY_OUT_RANGE = (4, 'tcp', 22, 100, '1.1.1.1', '2.2.2.2') + UDP_ENTRY_IN_RANGE = (4, 'udp', 3, 4, '1.1.1.1', '2.2.2.2') + UDP_ENTRY_OUT_RANGE = (4, 'udp', 100, 200, '1.1.1.1', '2.2.2.2') + self.list_entries.return_value = sorted( + [ICMP_ENTRY, TCP_ENTRY, UDP_ENTRY, + TCP_ENTRY_IN_RANGE, TCP_ENTRY_OUT_RANGE, + UDP_ENTRY_IN_RANGE, UDP_ENTRY_OUT_RANGE]) + expected_delete_entries = sorted( + [ICMP_ENTRY, TCP_ENTRY, UDP_ENTRY, + TCP_ENTRY_IN_RANGE, UDP_ENTRY_IN_RANGE]) + actual_delete_entries = self.conntrack_driver._get_entries_to_delete( + rule_filters, self.list_entries()) + self.assertEqual(expected_delete_entries, actual_delete_entries) diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/__init__.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/fake_data.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/fake_data.py new file mode 100644 index 000000000..33bfea013 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/fake_data.py @@ -0,0 +1,153 @@ +# Copyright 2017 FUJITSU LIMITED +# 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. +# + +import copy + +import mock +from neutron_lib import constants as nl_consts +from oslo_utils import uuidutils + +TENANT_UUID = uuidutils.generate_uuid() +TENANT_ID = TENANT_UUID +PROJECT_ID = TENANT_UUID +NETWORK_ID = uuidutils.generate_uuid() +SUBNET_ID = uuidutils.generate_uuid() +DEVICE_ID = uuidutils.generate_uuid() +PORT1 = uuidutils.generate_uuid() +PORT2 = uuidutils.generate_uuid() +PORT3 = uuidutils.generate_uuid() +PORT4 = uuidutils.generate_uuid() +HOST = 'fake_host' + + +class FakeFWaaSL2Agent(object): + + def __init__(self): + super(FakeFWaaSL2Agent, self).__init__() + + def create(self, resource, attrs=None, minimal=False): + """Create a fake fwaas v2 resources + + :param resource: A dictionary with all attributes + :type resource: string + :param attrs: A dictionary of each attribute you need to modify + :type attrs: dictionary + :param minimal: True if minimal port_detail is necessary + otherwise False + :type minimal: boolean + :return: + A OrderedDict faking the fwaas v2 resource + """ + target = getattr(self, "_" + resource) + return copy.deepcopy(target(attrs=attrs, minimal=minimal)) + + def _fwg(self, **kwargs): + + fwg = { + 'id': uuidutils.generate_uuid(), + 'name': 'my-group-' + uuidutils.generate_uuid(), + 'ingress_firewall_policy_id': uuidutils.generate_uuid(), + 'egress_firewall_policy_id': uuidutils.generate_uuid(), + 'description': 'my firewall group', + 'status': nl_consts.PENDING_CREATE, + 'ports': [PORT3, PORT4], + 'admin_state_up': True, + 'shared': False, + 'tenant_id': TENANT_ID, + 'project_id': PROJECT_ID + } + attrs = kwargs.get('attrs', None) + if attrs: + fwg.update(attrs) + return fwg + + def _fwg_with_rule(self, **kwargs): + + fwg_with_rule = self.create('fwg', attrs={'ports': [PORT1, PORT2]}) + rules = { + 'ingress_rule_list': [mock.Mock()], + 'egress_rule_list': [mock.Mock()], + 'add-port-ids': [PORT1], + 'del-port-ids': [PORT2], + 'port_details': { + PORT1: { + 'device': uuidutils.generate_uuid(), + 'device_owner': 'compute:nova', + 'host': HOST, + 'network_id': NETWORK_ID, + 'fixed_ips': [ + {'subnet_id': SUBNET_ID, 'ip_address': '172.24.4.5'}], + 'allowed_address_pairs': [], + 'port_security_enabled': True, + 'id': PORT1 + }, + PORT2: { + 'device': uuidutils.generate_uuid(), + 'device_owner': 'compute:nova', + 'host': HOST, + 'network_id': NETWORK_ID, + 'fixed_ips': [ + {'subnet_id': SUBNET_ID, 'ip_address': '172.24.4.6'}], + 'allowed_address_pairs': [], + 'port_security_enabled': True, + 'id': PORT2 + } + }, + } + fwg_with_rule.update(rules) + + if kwargs.get('minimal', None): + fwg_with_rule.update({'ports': []}) + fwg_with_rule.update({'add-port-ids': []}) + fwg_with_rule.update({'del-port-ids': []}) + fwg_with_rule.update({'port_details': {}}) + + attrs = kwargs.get('attrs', None) + if attrs: + fwg_with_rule.update(attrs) + return fwg_with_rule + + def _port(self, **kwargs): + + if kwargs.get('minimal', None): + return {'port_id': uuidutils.generate_uuid()} + + port_detail = { + 'profile': {}, + 'network_qos_policy_id': None, + 'qos_policy_id': None, + 'allowed_address_pairs': [], + 'admin_state_up': True, + 'network_id': NETWORK_ID, + 'segmentation_id': None, + 'fixed_ips': [ + {'subnet_id': SUBNET_ID, 'ip_address': '172.24.4.5'}], + 'vif_port': mock.Mock(), + 'device_owner': 'compute:node', + 'physical_network': 'physnet', + 'mac_address': 'fa:16:3e:8a:80:2b', + 'device': DEVICE_ID, + 'port_security_enabled': True, + 'port_id': uuidutils.generate_uuid(), + 'network_type': 'flat', + 'security_groups': [] + } + + attrs = kwargs.get('attrs', None) + + if attrs: + port_detail.update(attrs) + return port_detail diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/test_fwaas_v2.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/test_fwaas_v2.py new file mode 100644 index 000000000..96a850905 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l2/test_fwaas_v2.py @@ -0,0 +1,775 @@ +# Copyright 2017 Cisco Systems +# +# 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 + +import mock +from neutron_lib import constants as nl_consts +from neutron_lib import context +from neutron_lib.exceptions import firewall_v2 as f_exc +from oslo_config import cfg + +from neutron_fwaas.common import fwaas_constants as consts +from neutron_fwaas.services.firewall.service_drivers.agents.l2 import fwaas_v2 +from neutron_fwaas.tests import base +from neutron_fwaas.tests.unit.services.firewall.service_drivers.agents.l2\ + import fake_data + + +class TestFWaasV2AgentExtensionBase(base.BaseTestCase): + + def setUp(self): + super(TestFWaasV2AgentExtensionBase, self).setUp() + + self.fake = fake_data.FakeFWaaSL2Agent() + self.port = self.fake.create('port') + self.port_minimal = self.fake.create('port', minimal=True) + self.fwg = self.fake.create('fwg') + self.fwg_with_rule = self.fake.create('fwg_with_rule') + self.port_id = self.port['port_id'] + self.fwg_id = self.fwg['id'] + self.host = fake_data.HOST + self.ctx = context.get_admin_context() + + self.l2 = fwaas_v2.FWaaSV2AgentExtension() + self.l2.consume_api(mock.Mock()) + self._driver_mock = mock.patch( + 'neutron.manager.NeutronManager.load_class_for_provider') + self.driver = self._driver_mock.start() + self.l2.initialize(None, 'ovs') + self.l2.vlan_manager = mock.Mock() + self.conf = cfg.ConfigOpts() + self.l2.fwg_map = mock.Mock() + self.l2.conf.host = self.host + self.rpc = self.l2.plugin_rpc + + def tearDown(self): + self._driver_mock.stop() + super(TestFWaasV2AgentExtensionBase, self).tearDown() + + +class TestFWaasV2AgentExtension(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestFWaasV2AgentExtension, self).setUp() + cfg.CONF.set_override('firewall_l2_driver', 'ovs', group='fwaas') + + def test_initialize(self): + with mock.patch('neutron_lib.rpc.Connection') as conn: + self.l2.initialize(None, 'ovs') + self.driver.assert_called_with('neutron.agent.l2.firewall_drivers', + 'ovs') + conn.assert_called_with() + self.l2.conn.create_consumer.assert_called_with( + consts.FW_AGENT, [self.l2], fanout=False) + self.l2.conn.consume_in_threads.assert_called_with() + + +class TestHandlePort(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestHandlePort, self).setUp() + self.rpc.get_firewall_group_for_port = mock.Mock( + return_value=self.fwg) + self.l2._compute_status = mock.Mock(return_value=nl_consts.ACTIVE) + self.l2._apply_fwg_rules = mock.Mock(return_value=True) + self.l2._send_fwg_status = mock.Mock() + self.ctx = context.get_admin_context() + self.l2._add_rule_for_trusted_port = mock.Mock() + + def test_normal(self): + self.l2.fwg_map.get_port_fwg.return_value = None + self.l2.handle_port(self.ctx, self.port) + self.rpc.get_firewall_group_for_port.assert_called_once_with( + self.ctx, self.port['port_id']) + self.l2._apply_fwg_rules.assert_called_once_with(self.fwg, [self.port]) + self.l2._compute_status.assert_called_once_with( + self.fwg, True, event=consts.HANDLE_PORT) + self.l2.fwg_map.set_port_fwg.assert_called_once_with(self.port, + self.fwg) + self.l2._send_fwg_status.assert_called_once_with( + self.ctx, fwg_id=self.fwg['id'], + status=nl_consts.ACTIVE, host=self.l2.conf.host) + + def test_non_layer2_port(self): + self.port['device_owner'] = 'network:router_gateway' + self.l2.handle_port(self.ctx, self.port) + + self.rpc.get_firewall_group_for_port.assert_not_called() + self.l2._apply_fwg_rules.assert_not_called() + self.l2._compute_status.assert_not_called() + self.l2.fwg_map.set_port_fwg.assert_not_called() + self.l2._send_fwg_status.assert_not_called() + + def test_no_fwg_is_asossicate_to_port(self): + self.l2.fwg_map.get_port_fwg.return_value = None + self.rpc.get_firewall_group_for_port.return_value = None + self.l2.handle_port(self.ctx, self.port) + + self.rpc.get_firewall_group_for_port.assert_called_once_with( + self.ctx, self.port['port_id']) + self.l2._apply_fwg_rules.assert_not_called() + self.l2._compute_status.assert_not_called() + self.l2.fwg_map.set_port_fwg.assert_not_called() + self.l2._send_fwg_status.assert_not_called() + + def test_port_already_apply_fwg(self): + self.l2.fwg_map.get_port_fwg.return_value = self.fwg + self.l2.handle_port(self.ctx, self.port) + + self.rpc.get_firewall_group_for_port.assert_not_called() + self.l2._apply_fwg_rules.assert_not_called() + self.l2._compute_status.assert_not_called() + self.l2.fwg_map.set_port_fwg.assert_not_called() + self.l2._send_fwg_status.assert_not_called() + + def test_trusted_port(self): + self.l2.fwg_map.get_port.return_value = None + self.port['device_owner'] = 'network:foo' + self.l2.handle_port(self.ctx, self.port) + + self.l2._add_rule_for_trusted_port.assert_called_once_with(self.port) + self.l2.fwg_map.set_port.assert_called_once_with(self.port) + self.rpc.get_firewall_group_for_port.assert_not_called() + + def test_trusted_port_registered_map(self): + self.port['device_owner'] = 'network:dhcp' + self.l2.fwg_map.get_port.return_value = self.port + self.l2.handle_port(self.ctx, self.port) + + self.l2._add_rule_for_trusted_port.assert_not_called() + self.l2.fwg_map.set_port.assert_not_called() + self.rpc.get_firewall_group_for_port.assert_not_called() + + +class TestDeletePort(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestDeletePort, self).setUp() + self.l2._compute_status = mock.Mock(return_value=nl_consts.ACTIVE) + self.l2._apply_fwg_rules = mock.Mock(return_value=True) + self.l2._send_fwg_status = mock.Mock() + self.l2._delete_rule_for_trusted_port = mock.Mock() + + self.l2.fwg_map.get_port_fwg = mock.Mock(return_value=self.fwg) + self.l2.fwg_map.set_fwg = mock.Mock() + self.l2.fwg_map.get_port = mock.Mock(return_value=self.port) + self.l2.fwg_map.remove_port = mock.Mock() + + def test_include_vif_port_attribute(self): + self.port_minimal.update({'vif_port': None}) + self.l2.fwg_map.get_port_fwg.return_value = None + self.l2.delete_port(self.ctx, self.port_minimal) + + self.l2.fwg_map.get_port_fwg.assert_not_called() + self.l2._apply_fwg_rules.assert_not_called() + + def test_port_belongs_to_fwg(self): + expected_ports = self.fwg['ports'] + self.fwg['ports'].append(self.port['port_id']) + self.l2.delete_port(self.ctx, self.port_minimal) + + self.l2.fwg_map.get_port_fwg.assert_called_once_with(self.port) + self.l2._apply_fwg_rules.assert_called_once_with( + self.fwg, [self.port], event=consts.DELETE_FWG) + # 'port_id' has been removed from 'ports' + self.assertEqual(expected_ports, self.fwg['ports']) + self.l2.fwg_map.set_fwg.assert_called_once_with(self.fwg) + + def test_port_belongs_to_no_fwg(self): + expected_ports = self.fwg['ports'] + self.l2.delete_port(self.ctx, self.port_minimal) + + self.l2.fwg_map.get_port_fwg.assert_called_once_with(self.port) + self.l2._apply_fwg_rules.assert_called_once_with( + self.fwg, [self.port], event=consts.DELETE_FWG) + # 'ports' not changed during delete_port() + self.assertEqual(expected_ports, self.fwg['ports']) + self.l2.fwg_map.set_fwg.assert_called_once_with(self.fwg) + + def test_non_layer2_port(self): + self.port['device_owner'] = 'network:router_gateway' + self.l2.delete_port(self.ctx, self.port_minimal) + + self.l2.fwg_map.get_port_fwg.assert_not_called() + + def test_cannot_get_fwg_from_port(self): + self.l2.fwg_map.get_port_fwg.return_value = None + self.l2.delete_port(self.ctx, self.port_minimal) + + self.l2.fwg_map.get_port_fwg.assert_called_once_with(self.port) + self.l2._apply_fwg_rules.assert_not_called() + + def test_trusted_port_with_map(self): + self.port['device_owner'] = 'network:dhcp' + self.l2.fwg_map.get_port.return_value = self.port + self.l2.delete_port(self.ctx, self.port_minimal) + + self.l2._delete_rule_for_trusted_port.assert_called_once_with( + self.port) + self.l2.fwg_map.remove_port.assert_called_once_with(self.port) + + +class TestCreateFirewallGroup(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestCreateFirewallGroup, self).setUp() + self.l2._apply_fwg_rules = mock.Mock(return_value=True) + self.l2._compute_status = mock.Mock(return_value='ACTIVE') + self.l2._send_fwg_status = mock.Mock() + + def test_create_event_is_create(self): + fwg = self.fwg_with_rule + fwg['ports'] = [fake_data.PORT1] + ports = [fwg['port_details'][fake_data.PORT1]] + self.l2._create_firewall_group( + self.ctx, fwg, self.host, event=consts.CREATE_FWG) + self.l2._apply_fwg_rules.assert_called_once_with( + fwg, ports, consts.CREATE_FWG) + self.l2._compute_status.assert_called_once_with( + fwg, True, consts.CREATE_FWG) + + def test_create_event_is_not_create(self): + fwg = self.fwg_with_rule + fwg['ports'] = [fake_data.PORT1] + ports = [fwg['port_details'][fake_data.PORT1]] + self.l2._create_firewall_group( + self.ctx, fwg, self.host, event=consts.UPDATE_FWG) + self.l2._apply_fwg_rules.assert_called_once_with( + fwg, ports, consts.UPDATE_FWG) + + def test_create_with_port(self): + fwg = self.fwg_with_rule + ports = [fwg['port_details'][fake_data.PORT1]] + self.l2.create_firewall_group(self.ctx, fwg, self.host) + self.l2._apply_fwg_rules.assert_called_once_with( + fwg, ports, consts.CREATE_FWG) + + for idx, args in enumerate(self.l2._compute_status.call_args_list): + self.assertEqual(fwg, args[0][0]) + self.assertEqual(True, args[0][1]) + self.assertEqual(consts.CREATE_FWG, args[0][2]) + + for idx, args in enumerate(self.l2._send_fwg_status.call_args_list): + self.assertEqual(self.ctx, args[0][0]) + self.assertEqual(fwg['id'], args[0][1]) + self.assertEqual('ACTIVE', args[0][2]) + self.assertEqual(self.host, args[0][3]) + + def test_create_with_no_ports(self): + self.fwg_with_rule['add-port-ids'] = [] + self.assertIsNone(self.l2.create_firewall_group( + self.ctx, self.fwg_with_rule, self.host)) + self.l2._apply_fwg_rules.assert_not_called() + self.l2.fwg_map.set_port_fwg.assert_not_called() + self.l2._send_fwg_status.assert_called_once_with( + self.ctx, self.fwg_with_rule['id'], 'INACTIVE', self.host) + + def test_create_with_invalid_host(self): + self.fwg_with_rule['port_details'][fake_data.PORT1]['host'] = 'invalid' + self.l2.create_firewall_group(self.ctx, self.fwg_with_rule, self.host) + self.l2._apply_fwg_rules.assert_not_called() + self.l2._send_fwg_status.assert_called_once_with( + self.ctx, self.fwg_with_rule['id'], 'INACTIVE', self.host) + + def test_illegal_create_with_no_l2_ports(self): + fwg = { + 'name': 'non-default', + 'id': self.fwg_id, + 'ports': [], + 'add-port-ids': [self.port_id], + 'admin_state_up': True, + 'port_details': { + self.port_id: { + 'device_owner': 'network:router_interface' + } + } + } + self.l2.create_firewall_group(self.ctx, fwg, self.host) + self.l2._apply_fwg_rules.assert_not_called() + self.l2.fwg_map.set_port_fwg.assert_not_called() + self.l2._send_fwg_status.assert_called_once_with( + self.ctx, fwg['id'], 'INACTIVE', self.host) + + +class TestDeleteFirewallGroup(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestDeleteFirewallGroup, self).setUp() + self.l2._apply_fwg_rules = mock.Mock(return_value=True) + self.l2._compute_status = mock.Mock(return_value='ACTIVE') + self.l2._send_fwg_status = mock.Mock() + self.rpc.firewall_group_deleted = mock.Mock() + + def test_delete_with_port(self): + fwg = self.fwg_with_rule + ports = [fwg['port_details'][fake_data.PORT2]] + + self.assertIsNone(self.l2.delete_firewall_group( + self.ctx, self.fwg_with_rule, self.host)) + self.l2._apply_fwg_rules.assert_called_once_with( + fwg, ports, event=consts.DELETE_FWG) + self.l2.fwg_map.remove_fwg.assert_called_once_with(fwg) + for idx, args in enumerate(self.l2._compute_status.call_args_list): + self.assertEqual(fwg, args[0][0]) + self.assertEqual(True, args[0][2]) + self.assertEqual({'event': consts.CREATE_FWG}, args[1]) + + for idx, args in enumerate(self.l2._send_fwg_status.call_args_list): + self.assertEqual(self.ctx, args[0][0]) + self.assertEqual(fwg['id'], args[0][1]) + self.assertEqual('ACTIVE', args[0][2]) + self.assertEqual(self.host, args[0][3]) + + def test_delete_with_no_ports(self): + self.fwg_with_rule['del-port-ids'] = [] + self.l2.delete_firewall_group(self.ctx, self.fwg_with_rule, self.host) + self.l2._apply_fwg_rules.assert_not_called() + + def test_delete_with_no_l2_ports(self): + self.fwg_with_rule['port_details'][fake_data.PORT2][ + 'device_owner'] = 'network:router_interface' + self.l2.delete_firewall_group(self.ctx, self.fwg_with_rule, self.host) + self.l2._apply_fwg_rules.assert_not_called() + + def test_delete_with_exception(self): + self.l2._delete_firewall_group = mock.Mock(side_effect=Exception) + self.assertIsNone(self.l2.delete_firewall_group( + self.ctx, self.fwg_with_rule, self.host)) + + def test_delete_event_is_update(self): + self.l2._delete_firewall_group( + self.ctx, self.fwg_with_rule, self.host, event=consts.UPDATE_FWG) + self.l2.fwg_map.remove_fwg.assert_not_called() + self.rpc.firewall_group_deleted.assert_not_called() + self.l2._compute_status.assert_called_once_with( + self.fwg_with_rule, True, consts.UPDATE_FWG) + self.l2._send_fwg_status.assert_called_once_with( + self.ctx, self.fwg_with_rule['id'], 'ACTIVE', self.host) + + +class TestUpdateFirewallGroup(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestUpdateFirewallGroup, self).setUp() + self.l2._delete_firewall_group = mock.Mock() + self.l2._create_firewall_group = mock.Mock() + self.l2._send_fwg_status = mock.Mock() + + def test_update(self): + self.assertIsNone(self.l2.update_firewall_group( + self.ctx, mock.ANY, self.host)) + + self.l2._delete_firewall_group.assert_called_once_with( + self.ctx, mock.ANY, self.host, event=consts.UPDATE_FWG) + self.l2._create_firewall_group.assert_called_once_with( + self.ctx, mock.ANY, self.host, event=consts.UPDATE_FWG) + + def test_update_raised_in_delete_firewall_group(self): + self.l2._delete_firewall_group.side_effect = Exception + fwg = self.fwg_with_rule + self.assertIsNone(self.l2.update_firewall_group( + self.ctx, fwg, self.host)) + self.l2._send_fwg_status.assert_called_once_with( + self.ctx, fwg['id'], status='ERROR', host=self.host) + + def test_update_raised_in_create_firewall_group(self): + self.l2._create_firewall_group.side_effect = Exception + fwg = self.fwg_with_rule + self.assertIsNone(self.l2.update_firewall_group( + self.ctx, fwg, self.host)) + self.l2._send_fwg_status.assert_called_once_with( + self.ctx, fwg['id'], status='ERROR', host=self.host) + + +class TestIsPortLayer2(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestIsPortLayer2, self).setUp() + + def test_vm_port(self): + self.assertTrue(self.l2._is_port_layer2(self.port)) + + def test_not_vm_port(self): + for device_owner in [nl_consts.DEVICE_OWNER_ROUTER_INTF, + nl_consts.DEVICE_OWNER_ROUTER_GW, + nl_consts.DEVICE_OWNER_DHCP, + nl_consts.DEVICE_OWNER_DVR_INTERFACE, + nl_consts.DEVICE_OWNER_AGENT_GW, + nl_consts.DEVICE_OWNER_ROUTER_SNAT, + nl_consts.DEVICE_OWNER_LOADBALANCER, + nl_consts.DEVICE_OWNER_LOADBALANCERV2, + 'unknown device_owner', + '']: + self.port['device_owner'] = device_owner + self.assertFalse(self.l2._is_port_layer2(self.port)) + + def test_illegal_no_device_owner(self): + del self.port['device_owner'] + self.assertFalse(self.l2._is_port_layer2(self.port)) + + +class TestComputeStatus(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestComputeStatus, self).setUp() + self.ports = list(self.fwg_with_rule['port_details'].values()) + + def test_normal(self): + result = True + fwg = self.fwg_with_rule + self.assertEqual('ACTIVE', self.l2._compute_status(fwg, result)) + + def test_event_is_delete(self): + result = True + fwg = self.fwg_with_rule + self.assertIsNone(self.l2._compute_status( + fwg, result, consts.DELETE_FWG)) + + def test_event_is_update(self): + result = True + fwg = self.fwg_with_rule + self.assertEqual('ACTIVE', self.l2._compute_status( + fwg, result, consts.UPDATE_FWG)) + + def test_event_is_update_and_has_last_port(self): + result = True + fwg = self.fake.create('fwg_with_rule', attrs={'last-port': False}) + + self.assertEqual('ACTIVE', self.l2._compute_status( + fwg, result, consts.UPDATE_FWG)) + + fwg = self.fake.create('fwg_with_rule', attrs={'last-port': True}) + self.assertEqual('INACTIVE', self.l2._compute_status( + fwg, result, consts.UPDATE_FWG)) + + def test_event_is_update_and_has_no_last_port_but_has_ports(self): + result = True + fwg = self.fwg_with_rule + self.assertEqual('ACTIVE', self.l2._compute_status( + fwg, result, consts.UPDATE_FWG)) + + def test_event_is_update_and_has_no_last_port_and_ports(self): + result = True + fwg = self.fwg_with_rule + fwg['ports'] = [] + self.assertEqual('INACTIVE', self.l2._compute_status( + fwg, result, consts.UPDATE_FWG)) + + def test_event_is_create(self): + result = True + fwg = self.fwg_with_rule + self.assertEqual('ACTIVE', self.l2._compute_status( + fwg, result, consts.CREATE_FWG)) + + def test_event_is_create_and_no_fwg_ports(self): + result = True + fwg = self.fwg_with_rule + fwg['ports'] = [] + self.assertEqual('INACTIVE', self.l2._compute_status( + fwg, result, consts.CREATE_FWG)) + + def test_event_is_handle_port(self): + result = True + fwg = self.fwg_with_rule + self.assertEqual('ACTIVE', self.l2._compute_status( + fwg, result, consts.HANDLE_PORT)) + + def test_event_is_delete_port(self): + result = True + fwg = self.fwg_with_rule + self.assertEqual('ACTIVE', self.l2._compute_status( + fwg, result, consts.DELETE_PORT)) + + def test_event_is_delete_port_and_no_fwg_ports(self): + result = True + fwg = self.fwg_with_rule + fwg['ports'] = [] + self.assertEqual('INACTIVE', self.l2._compute_status( + fwg, result, consts.DELETE_PORT)) + + def test_driver_result_is_false(self): + result = False + fwg = self.fwg_with_rule + self.assertEqual('ERROR', self.l2._compute_status( + fwg, result)) + + def test_admin_state_up_is_false(self): + result = True + self.fwg_with_rule['admin_state_up'] = False + + self.assertEqual('DOWN', self.l2._compute_status( + self.fwg_with_rule, self.ports, result)) + + def test_active_inactive_patterns(self): + result = True + fwg = self.fwg_with_rule + # Case1: ingress/egress_firewall_policy_id + # Case2: ports --> already tested at above cases + expect_and_attrs = [ + ('INACTIVE', ('ingress_firewall_policy_id', + 'egress_firewall_policy_id')), + ('ACTIVE', ('ingress_firewall_policy_id',)), + ('ACTIVE', ('egress_firewall_policy_id',)), + ] + for attr in expect_and_attrs: + fwg = self.fake.create('fwg_with_rule') + expect = attr[0] + for p in attr[1]: + fwg[p] = None + self.assertEqual(expect, self.l2._compute_status(fwg, result)) + + +class TestApplyFwgRules(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestApplyFwgRules, self).setUp() + + class DummyVlan(object): + + def __init__(self, vlan=None): + self.vlan = vlan + + self.l2.vlan_manager.get.return_value = DummyVlan(vlan='999') + + def test_event_is_create(self): + fwg_ports = [self.fwg_with_rule['port_details'][fake_data.PORT1]] + driver_ports = copy.deepcopy(fwg_ports) + driver_ports[0].update({'lvlan': 999}) + + self.assertTrue(self.l2._apply_fwg_rules( + self.fwg_with_rule, fwg_ports, event=consts.CREATE_FWG)) + + self.l2.driver.create_firewall_group.assert_called_once_with( + driver_ports, self.fwg_with_rule) + self.l2.driver.delete_firewall_group.assert_not_called() + self.l2.driver.update_firewall_group.assert_not_called() + + def test_event_is_update(self): + fwg_ports = [self.fwg_with_rule['port_details'][fake_data.PORT1]] + driver_ports = copy.deepcopy(fwg_ports) + driver_ports[0].update({'lvlan': 999}) + + self.assertTrue(self.l2._apply_fwg_rules( + self.fwg_with_rule, fwg_ports, event=consts.UPDATE_FWG)) + + self.l2.driver.update_firewall_group.assert_called_once_with( + driver_ports, self.fwg_with_rule) + + def test_event_is_delete(self): + fwg_ports = [self.fwg_with_rule['port_details'][fake_data.PORT1]] + driver_ports = copy.deepcopy(fwg_ports) + driver_ports[0].update({'lvlan': 999}) + + self.assertTrue(self.l2._apply_fwg_rules( + self.fwg_with_rule, fwg_ports, event=consts.DELETE_FWG)) + + self.l2.driver.delete_firewall_group.assert_called_once_with( + fwg_ports, self.fwg_with_rule) + + def test_raised_in_driver(self): + self.l2.driver.delete_firewall_group.side_effect = \ + f_exc.FirewallInternalDriverError(driver='ovs firewall') + fwg_ports = [self.fwg_with_rule['port_details'][fake_data.PORT1]] + driver_ports = copy.deepcopy(fwg_ports) + driver_ports[0].update({'lvlan': 999}) + + self.assertFalse(self.l2._apply_fwg_rules( + self.fwg_with_rule, fwg_ports, event=consts.DELETE_FWG)) + + self.l2.driver.delete_firewall_group.assert_called_once_with( + fwg_ports, self.fwg_with_rule) + + +class TestSendFwgStatus(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestSendFwgStatus, self).setUp() + self.rpc.set_firewall_group_status = mock.Mock() + + def test_success(self): + self.assertIsNone(self.l2._send_fwg_status( + self.ctx, self.fwg_id, 'ACTIVE', self.host)) + + def test_failure(self): + self.rpc.set_firewall_group_status.side_effect = Exception + self.assertIsNone(self.l2._send_fwg_status( + self.ctx, self.fwg_id, 'ACTIVE', self.host)) + + +class TestAddLocalVlanToPorts(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestAddLocalVlanToPorts, self).setUp() + + class DummyVlan(object): + + def __init__(self, vlan=None): + self.vlan = vlan + + self.l2.vlan_manager.get.return_value = DummyVlan(vlan='999') + self.port_with_detail = { + 'port_id': fake_data.PORT1, + 'id': fake_data.PORT1, + 'network_id': fake_data.NETWORK_ID, + 'port_details': { + fake_data.PORT1: { + 'device': 'c12e5c1e-d68e-45bd-a2d3-1f2f32604e41', + 'device_owner': 'compute:nova', + 'host': self.host, + 'network_id': fake_data.NETWORK_ID, + 'fixed_ips': [ + {'subnet_id': fake_data.SUBNET_ID, + 'ip_address': '172.24.4.5'}], + 'allowed_address_pairs': [], + 'port_security_enabled': True, + 'id': fake_data.PORT1 + } + } + } + + def test_port_has_detail_and_port_id(self): + del self.port_with_detail['id'] + expect = [copy.deepcopy(self.port_with_detail)] + expect[0].update({'lvlan': 999}) + actual = self.l2._add_local_vlan_to_ports([self.port_with_detail]) + + self.l2.vlan_manager.get.assert_called_once_with( + self.port_with_detail['network_id']) + self.assertEqual(expect, actual) + + def test_port_has_detail_and_id(self): + del self.port_with_detail['port_id'] + expect = [copy.deepcopy(self.port_with_detail)] + expect[0].update({'lvlan': 999}) + actual = self.l2._add_local_vlan_to_ports([self.port_with_detail]) + + self.l2.vlan_manager.get.assert_called_once_with( + self.port_with_detail['network_id']) + self.assertEqual(expect, actual) + + def test_port_has_no_detail(self): + del self.port_with_detail['port_details'] + expect = [copy.deepcopy(self.port_with_detail)] + expect[0].update({'lvlan': 999}) + actual = self.l2._add_local_vlan_to_ports([self.port_with_detail]) + + self.l2.vlan_manager.get.assert_called_once_with( + self.port_with_detail['network_id']) + self.assertEqual(expect, actual) + + +class TestFWaaSL2PluginApi(TestFWaasV2AgentExtensionBase): + + def setUp(self): + super(TestFWaaSL2PluginApi, self).setUp() + + self.plugin = fwaas_v2.FWaaSL2PluginApi( + consts.FIREWALL_PLUGIN, self.host) + self.plugin.client = mock.Mock() + self.cctxt = self.plugin.client.prepare() + + def test_get_firewall_group_for_port(self): + self.plugin.get_firewall_group_for_port(self.ctx, mock.ANY) + self.cctxt.call.assert_called_once_with( + self.ctx, + 'get_firewall_group_for_port', + port_id=mock.ANY + ) + + def test_set_firewall_group_status(self): + self.plugin.set_firewall_group_status( + self.ctx, self.fwg_id, 'ACTIVE', self.host) + self.cctxt.call.assert_called_once_with( + self.ctx, + 'set_firewall_group_status', + fwg_id=self.fwg_id, + status='ACTIVE', + host=self.host, + ) + + def test_firewall_group_deleted(self): + self.plugin.firewall_group_deleted(self.ctx, self.fwg_id, self.host) + self.cctxt.call.assert_called_once_with( + self.ctx, + 'firewall_group_deleted', + fwg_id=self.fwg_id, + host=self.host, + ) + + +class TestPortFirewallGroupMap(base.BaseTestCase): + + def setUp(self): + super(TestPortFirewallGroupMap, self).setUp() + self.fake = fake_data.FakeFWaaSL2Agent() + self.map = fwaas_v2.PortFirewallGroupMap() + self.fwg = self.fake.create('fwg') + self.fwg_id = self.fwg['id'] + self.port = self.fake.create('port') + self.fwg['ports'] = [] + + def test_set_and_get(self): + self.map.set_fwg(self.fwg) + self.assertEqual(self.fwg, self.map.get_fwg(self.fwg_id)) + + def test_set_and_get_port_fwg(self): + port1 = self.port + port2 = self.fake.create('port') + self.map.set_port_fwg(port1, self.fwg) + self.map.set_port_fwg(port2, self.fwg) + self.assertEqual(self.fwg, self.map.get_port_fwg(port1)) + self.assertEqual(self.fwg, self.map.get_port_fwg(port2)) + self.assertIsNone(self.map.get_port_fwg('unknown')) + + def test_remove_port(self): + port1 = self.port + port2 = self.fake.create('port') + self.map.set_port_fwg(port1, self.fwg) + self.map.remove_port(port2) + + self.map.set_port_fwg(port2, self.fwg) + self.map.remove_port(port1) + self.assertIsNone(self.map.get_port(port1)) + self.assertEqual([port2['port_id']], + self.map.get_fwg(self.fwg_id)['ports']) + self.map.remove_port(port2) + self.assertIsNone(self.map.get_port(port2)) + self.assertEqual([], self.map.get_fwg(self.fwg_id)['ports']) + + def test_remove_non_exist_port(self): + port1 = self.port + port2 = self.fake.create('port') + self.map.set_port_fwg(port1, self.fwg) + + self.map.remove_port(port2) + self.assertIsNone(self.map.get_port(port2)) + + def test_illegal_remove_port_no_relation_with_fwg(self): + port1 = self.port + port1_id = port1['port_id'] + self.map.set_port_fwg(port1, self.fwg) + self.map.port_fwg[port1_id] = None + self.map.remove_port(port1) + self.assertIsNone(self.map.get_port(port1)) + + def test_remove_fwg(self): + self.map.set_fwg(self.fwg) + self.assertEqual(self.fwg, self.map.get_fwg(self.fwg_id)) + self.map.remove_fwg(self.fwg) + self.assertIsNone(self.map.get_fwg(self.fwg_id)) + + def test_remove_fwg_non_exist(self): + self.map.remove_fwg(self.fwg) + self.assertIsNone(self.map.get_fwg(self.fwg_id)) diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l3reference/__init__.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l3reference/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l3reference/test_firewall_l3_agent_v2.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l3reference/test_firewall_l3_agent_v2.py new file mode 100644 index 000000000..ade916a1a --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/l3reference/test_firewall_l3_agent_v2.py @@ -0,0 +1,613 @@ +# Copyright (c) 2016 +# 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. + +import mock +from neutron.agent.l3 import l3_agent_extension_api as l3_agent_api +from neutron.agent.l3 import router_info +from neutron.agent.linux import ip_lib +from neutron.conf.agent.l3 import config as l3_config +from neutron_lib import context +from oslo_config import cfg +from oslo_utils import uuidutils + +from neutron_fwaas.common import fwaas_constants +from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux \ + import iptables_fwaas_v2 +from neutron_fwaas.services.firewall.service_drivers.agents \ + import firewall_agent_api +from neutron_fwaas.services.firewall.service_drivers.agents.l3reference \ + import firewall_l3_agent_v2 +from neutron_fwaas.tests import base +from neutron_fwaas.tests.unit.services.firewall.service_drivers.agents \ + import test_firewall_agent_api + + +class FWaasHelper(object): + def __init__(self): + pass + + +class FWaasAgent(firewall_l3_agent_v2.L3WithFWaaS, FWaasHelper): + neutron_service_plugins = [] + + def add_router(self, context, data): + pass + + def delete_router(self, context, data): + pass + + def update_router(self, context, data): + pass + + +def _setup_test_agent_class(service_plugins): + class FWaasTestAgent(firewall_l3_agent_v2.L3WithFWaaS, + FWaasHelper): + neutron_service_plugins = service_plugins + + def __init__(self, conf): + self.event_observers = mock.Mock() + self.conf = conf + firewall_agent_api._check_required_agent_extension = mock.Mock() + super(FWaasTestAgent, self).__init__(conf) + + def delete_router(self, context, data): + pass + + return FWaasTestAgent + + +class TestFWaaSL3AgentExtension(base.BaseTestCase): + def setUp(self): + super(TestFWaaSL3AgentExtension, self).setUp() + + self.conf = cfg.ConfigOpts() + self.conf.register_opts(l3_config.OPTS) + self.conf.register_opts(firewall_agent_api.FWaaSOpts, 'fwaas') + self.conf.host = 'myhost' + self.api = FWaasAgent(self.conf) + self.api.agent_api = mock.Mock() + self.api.fwaas_driver = test_firewall_agent_api.NoopFwaasDriverV2() + self.adminContext = context.get_admin_context() + self.context = mock.sentinel.context + self.router_id = uuidutils.generate_uuid() + self.agent_conf = mock.Mock() + self.ri_kwargs = {'router': {'id': self.router_id, + 'project_id': uuidutils.generate_uuid()}, + 'agent_conf': self.agent_conf, + 'interface_driver': mock.ANY, + 'use_ipv6': mock.ANY + } + + def test_fw_config_match(self): + test_agent_class = _setup_test_agent_class([fwaas_constants.FIREWALL]) + cfg.CONF.set_override('enabled', True, 'fwaas') + with mock.patch('oslo_utils.importutils.import_object'): + test_agent_class(cfg.CONF) + + def test_fw_config_mismatch_plugin_enabled_agent_disabled(self): + self.skipTest('this is broken') + test_agent_class = _setup_test_agent_class([fwaas_constants.FIREWALL]) + cfg.CONF.set_override('enabled', False, 'fwaas') + self.assertRaises(SystemExit, test_agent_class, cfg.CONF) + + def test_fw_plugin_list_unavailable(self): + test_agent_class = _setup_test_agent_class(None) + cfg.CONF.set_override('enabled', False, 'fwaas') + with mock.patch('oslo_utils.importutils.import_object'): + test_agent_class(cfg.CONF) + + def test_create_firewall_group(self): + firewall_group = {'id': 0, 'project_id': 1, + 'admin_state_up': True, + 'add-port-ids': [1, 2]} + self.api.plugin_rpc = mock.Mock() + with mock.patch.object(self.api, '_get_firewall_group_ports' + ) as mock_get_firewall_group_ports, \ + mock.patch.object(self.api, '_get_in_ns_ports' + ) as mock_get_in_ns_ports, \ + mock.patch.object(self.api.fwaas_driver, + 'create_firewall_group' + ) as mock_driver_create_firewall_group, \ + mock.patch.object(self.api.fwplugin_rpc, + 'set_firewall_group_status' + ) as mock_set_firewall_group_status: + + mock_driver_create_firewall_group.return_value = True + + self.api.create_firewall_group(self.context, firewall_group, + host='host') + + mock_get_firewall_group_ports.assert_called_once_with(self.context, + firewall_group) + mock_get_in_ns_ports.assert_called + assert mock_get_in_ns_ports + mock_set_firewall_group_status.assert_called_once_with( + self.context, firewall_group['id'], 'ACTIVE') + + def test_update_firewall_group_with_ports_added_and_deleted(self): + firewall_group = {'id': 0, 'project_id': 1, + 'admin_state_up': True, + 'ports': [1, 2, 3, 4], + 'add-port-ids': [1, 2], + 'del-port-ids': [3, 4], + 'router_ids': [], + 'last-port': False} + + self.api.plugin_rpc = mock.Mock() + with mock.patch.object(self.api, '_get_firewall_group_ports' + ) as mock_get_firewall_group_ports, \ + mock.patch.object(self.api, '_get_in_ns_ports' + ) as mock_get_in_ns_ports, \ + mock.patch.object(self.api.fwaas_driver, + 'update_firewall_group' + ) as mock_driver_update_firewall_group, \ + mock.patch.object(self.api.fwaas_driver, + 'delete_firewall_group' + ) as mock_driver_delete_firewall_group, \ + mock.patch.object(self.api.fwplugin_rpc, + 'set_firewall_group_status' + ) as mock_set_firewall_group_status: + + mock_driver_delete_firewall_group.return_value = True + mock_driver_update_firewall_group.return_value = True + + calls = [mock.call(self.context, firewall_group, to_delete=True, + require_new_plugin=True), + mock.call(self.context, firewall_group)] + + self.api.update_firewall_group(self.context, firewall_group, + host='host') + + self.assertEqual(mock_get_firewall_group_ports.call_args_list, + calls) + mock_get_in_ns_ports.assert_called + mock_set_firewall_group_status.assert_called_once_with( + self.context, firewall_group['id'], 'ACTIVE') + + def test_update_firewall_group_with_ports_added_and_admin_state_down(self): + firewall_group = {'id': 0, 'project_id': 1, + 'admin_state_up': False, + 'ports': [1, 2], + 'add-port-ids': [1, 2], + 'del-port-ids': [], + 'router_ids': [], + 'last-port': False} + + self.api.plugin_rpc = mock.Mock() + with mock.patch.object(self.api, '_get_firewall_group_ports' + ) as mock_get_firewall_group_ports, \ + mock.patch.object(self.api, '_get_in_ns_ports' + ) as mock_get_in_ns_ports, \ + mock.patch.object(self.api.fwaas_driver, + 'update_firewall_group' + ) as mock_driver_update_firewall_group, \ + mock.patch.object(self.api.fwplugin_rpc, + 'set_firewall_group_status' + ) as mock_set_firewall_group_status: + + mock_driver_update_firewall_group.return_value = True + + self.api.update_firewall_group(self.context, firewall_group, + host='host') + + mock_get_firewall_group_ports.assert_called + mock_get_in_ns_ports.assert_called + mock_set_firewall_group_status.assert_called_once_with( + self.context, firewall_group['id'], 'DOWN') + + def test_update_firewall_group_with_all_ports_deleted(self): + firewall_group = {'id': 0, 'project_id': 1, + 'admin_state_up': True, + 'ports': [3, 4], + 'add-port-ids': [], + 'del-port-ids': [3, 4], + 'last-port': True} + + self.api.plugin_rpc = mock.Mock() + with mock.patch.object(self.api, '_get_firewall_group_ports' + ) as mock_get_firewall_group_ports, \ + mock.patch.object(self.api, '_get_in_ns_ports' + ) as mock_get_in_ns_ports, \ + mock.patch.object(self.api.fwaas_driver, + 'delete_firewall_group' + ) as mock_driver_delete_firewall_group, \ + mock.patch.object(self.api.fwplugin_rpc, + 'set_firewall_group_status' + ) as mock_set_firewall_group_status: + + mock_driver_delete_firewall_group.return_value = True + + self.api.update_firewall_group(self.context, firewall_group, + host='host') + calls = [ + mock.call._get_firewall_group_ports( + self.context, firewall_group, require_new_plugin=True, + to_delete=True), + mock.call._get_firewall_group_ports( + self.context, firewall_group) + ] + mock_get_firewall_group_ports.assert_has_calls(calls) + mock_get_in_ns_ports.assert_called + mock_set_firewall_group_status.assert_called_once_with( + self.context, firewall_group['id'], 'INACTIVE') + + def test_update_firewall_group_with_no_ports_added_or_deleted(self): + firewall_group = {'id': 0, 'project_id': 1, + 'admin_state_up': True, + 'ports': [], + 'add-port-ids': [], + 'del-port-ids': [], + 'last-port': True} + + self.api.plugin_rpc = mock.Mock() + with mock.patch.object(self.api, '_get_firewall_group_ports' + ) as mock_get_firewall_group_ports, \ + mock.patch.object(self.api, '_get_in_ns_ports' + ) as mock_get_in_ns_ports, \ + mock.patch.object(self.api.fwaas_driver, + 'update_firewall_group' + ) as mock_driver_update_firewall_group, \ + mock.patch.object(self.api.fwplugin_rpc, + 'set_firewall_group_status' + ) as mock_set_firewall_group_status: + + mock_driver_update_firewall_group.return_value = True + + self.api.update_firewall_group(self.context, firewall_group, + host='host') + calls = [ + mock.call._get_firewall_group_ports( + self.context, firewall_group, require_new_plugin=True, + to_delete=True), + mock.call._get_firewall_group_ports( + self.context, firewall_group) + ] + mock_get_firewall_group_ports.assert_has_calls(calls) + mock_get_in_ns_ports.assert_called + mock_set_firewall_group_status.assert_called_once_with( + self.context, firewall_group['id'], 'INACTIVE') + + def test_update_firewall_group_with_only_ports_added(self): + # This test is for bug/1634114 + firewall_group = {'id': 0, 'project_id': 1, + 'admin_state_up': True, + 'ports': [1, 2], + 'add-port-ids': ['1', '2'], + 'del-port-ids': [], + 'last-port': False + } + with mock.patch.object(self.api, '_get_firewall_group_ports' + ) as mock_get_firewall_group_ports, \ + mock.patch.object(self.api, '_get_in_ns_ports' + ) as mock_get_in_ns_ports, \ + mock.patch.object(self.api.fwaas_driver, + 'update_firewall_group' + ) as mock_driver_update_firewall_group, \ + mock.patch.object(self.api.fwplugin_rpc, + 'set_firewall_group_status' + ) as mock_set_firewall_group_status: + + mock_driver_update_firewall_group.return_value = True + + self.api.update_firewall_group(self.context, firewall_group, + host='host') + calls = [ + mock.call._get_firewall_group_ports( + self.context, firewall_group, require_new_plugin=True, + to_delete=True), + mock.call._get_firewall_group_ports( + self.context, firewall_group) + ] + mock_get_firewall_group_ports.assert_has_calls(calls) + mock_get_in_ns_ports.assert_called + mock_set_firewall_group_status.assert_called_once_with( + self.context, firewall_group['id'], 'ACTIVE') + + def test_update_firewall_group_with_only_ports_removed(self): + firewall_group = {'id': 0, 'project_id': 1, + 'admin_state_up': True, + 'ports': [1, 2], + 'add-port-ids': [], + 'del-port-ids': ['1'], + 'last-port': False + } + self.api.plugin_rpc = mock.Mock() + with mock.patch.object(self.api, '_get_firewall_group_ports' + ) as mock_get_firewall_group_ports, \ + mock.patch.object(self.api, '_get_in_ns_ports' + ) as mock_get_in_ns_ports, \ + mock.patch.object(self.api.fwaas_driver, + 'update_firewall_group' + ) as mock_driver_update_firewall_group, \ + mock.patch.object(self.api.fwplugin_rpc, + 'set_firewall_group_status' + ) as mock_set_firewall_group_status: + + mock_driver_update_firewall_group.return_value = True + + self.api.update_firewall_group(self.context, firewall_group, + host='host') + calls = [ + mock.call._get_firewall_group_ports( + self.context, firewall_group, require_new_plugin=True, + to_delete=True), + mock.call._get_firewall_group_ports( + self.context, firewall_group) + ] + mock_get_firewall_group_ports.assert_has_calls(calls) + mock_get_in_ns_ports.assert_called + mock_set_firewall_group_status.assert_called_once_with( + self.context, firewall_group['id'], 'ACTIVE') + + def test_update_firewall_group_with_only_policies_added_or_deleted(self): + firewall_group = {'id': 0, 'project_id': 1, + 'tenant_id': 1, + 'admin_state_up': True, + 'ports': [1, 2], + 'add-port-ids': [], + 'del-port-ids': [], + 'last-port': False + } + self.api.plugin_rpc = mock.Mock() + with mock.patch.object(self.api, '_get_firewall_group_ports' + ) as mock_get_firewall_group_ports, \ + mock.patch.object(self.api, '_get_in_ns_ports' + ) as mock_get_in_ns_ports, \ + mock.patch.object(self.api.fwaas_driver, + 'update_firewall_group' + ) as mock_driver_update_firewall_group, \ + mock.patch.object(self.api.fwplugin_rpc, + 'set_firewall_group_status' + ) as mock_set_firewall_group_status: + + mock_driver_update_firewall_group.return_value = True + + self.api.update_firewall_group(self.context, firewall_group, + host='host') + calls = [ + mock.call._get_firewall_group_ports( + self.context, firewall_group, require_new_plugin=True, + to_delete=True), + mock.call._get_firewall_group_ports( + self.context, firewall_group) + ] + mock_get_firewall_group_ports.assert_has_calls(calls) + mock_get_in_ns_ports.assert_called + mock_set_firewall_group_status.assert_called_once_with( + self.context, firewall_group['id'], 'ACTIVE') + + def test_delete_firewall_group(self): + firewall_group = {'id': 0, 'project_id': 1, + 'admin_state_up': True, + 'ports': [3, 4], + 'add-port-ids': [], + 'del-port-ids': [3, 4], + 'last-port': False} + + self.api.plugin_rpc = mock.Mock() + with mock.patch.object(self.api, '_get_firewall_group_ports' + ) as mock_get_firewall_group_ports, \ + mock.patch.object(self.api, '_get_in_ns_ports' + ) as mock_get_in_ns_ports, \ + mock.patch.object(self.api.fwaas_driver, + 'delete_firewall_group' + ) as mock_driver_delete_firewall_group, \ + mock.patch.object(self.api.fwplugin_rpc, + 'firewall_group_deleted' + ) as mock_firewall_group_deleted: + + mock_driver_delete_firewall_group.return_value = True + + self.api.delete_firewall_group(self.context, firewall_group, + host='host') + + mock_get_firewall_group_ports.assert_called_once_with( + self.context, firewall_group, to_delete=True) + mock_get_in_ns_ports.assert_called + mock_firewall_group_deleted.assert_called_once_with(self.context, + firewall_group['id']) + + def _prepare_router_data(self): + return router_info.RouterInfo(self.api, + self.router_id, + **self.ri_kwargs) + + def test_get_in_ns_ports_for_non_ns_fw(self): + port_ids = [1, 2] + ports = [{'id': pid} for pid in port_ids] + ri = self._prepare_router_data() + ri.internal_ports = ports + router_info = {ri.router_id: ri} + api_object = l3_agent_api.L3AgentExtensionAPI(router_info, None) + self.api.consume_api(api_object) + fw_port_ids = port_ids + + with mock.patch.object(ip_lib, + 'list_network_namespaces') as mock_list_netns: + mock_list_netns.return_value = [] + ports_for_fw_list = self.api._get_in_ns_ports(fw_port_ids) + + mock_list_netns.assert_called_with() + self.assertFalse(ports_for_fw_list) + + def test_get_in_ns_ports_for_fw(self): + port_ids = [1, 2] + ports = [{'id': pid} for pid in port_ids] + ri = self._prepare_router_data() + ri.internal_ports = ports + router_info = {} + router_info[ri.router_id] = ri + api_object = l3_agent_api.L3AgentExtensionAPI(router_info, None) + self.api.consume_api(api_object) + fw_port_ids = port_ids + ports_for_fw_expected = [(ri, port_ids)] + + with mock.patch.object(ip_lib, + 'list_network_namespaces') as mock_list_netns: + mock_list_netns.return_value = [ri.ns_name] + ports_for_fw_actual = self.api._get_in_ns_ports(fw_port_ids) + self.assertEqual(ports_for_fw_expected, ports_for_fw_actual) + + def test_add_router_for_check_input(self): + fw_agent = _setup_test_agent_class([fwaas_constants.FIREWALL]) + cfg.CONF.set_override('enabled', True, 'fwaas') + updated_router = { + '_interfaces': [{ + 'device_owner': 'network: router_interface', + 'id': '1', + 'tenant_id': 'demo_tenant_id', + }], + 'tenant_id': 'demo_tenant_id', + 'id': '0b109a4e-d228-479d-ad43-08bf3245adbb', + 'name': 'demo_router' + } + fwg = { + 'status': 'ACTIVE', + 'admin_state_up': True, + 'tenant_id': 'demo_tenant_id', + 'ports': [1], + 'del-port-ids': [], + 'add-port-ids': ['1'], + 'id': '2932b3d9-3a7b-48a1-a16c-bf9f7b2751a5' + } + with mock.patch('oslo_utils.importutils.import_object'): + agent = fw_agent(cfg.CONF) + agent.agent_api = mock.Mock() + agent.fwplugin_rpc = mock.Mock() + agent.conf.agent_mode = mock.Mock() + agent.fwaas_driver = iptables_fwaas_v2.IptablesFwaasDriver() + with mock.patch.object(agent.fwplugin_rpc, + 'get_firewall_groups_for_project' + ) as mock_get_firewall_groups_for_project, \ + mock.patch.object(agent.agent_api, + 'get_router_hosting_port' + ) as mock_get_router_hosting_port, \ + mock.patch.object(agent.fwaas_driver, + '_get_ipt_mgrs_with_if_prefix' + ) as mock_get_ipt_mgrs_with_if_prefix: + mock_get_firewall_groups_for_project.return_value = [fwg] + mock_get_router_hosting_port.return_value = mock.Mock() + agent.add_router(self.context, updated_router) + mock_get_ipt_mgrs_with_if_prefix.assert_any_call( + agent.conf.agent_mode, mock.ANY) + + @mock.patch('oslo_utils.importutils.import_object') + def test_add_router_with_several_ports(self, mock_import_object): + fw_agent = _setup_test_agent_class([fwaas_constants.FIREWALL]) + cfg.CONF.set_override('enabled', True, 'fwaas') + updated_router = { + '_interfaces': [ + {'device_owner': 'network: router_interface', + 'id': '1', + 'tenant_id': 'demo_tenant_id'}, + {'device_owner': 'network: router_interface', + 'id': '2', + 'tenant_id': 'demo_tenant_id'}, + {'device_owner': 'network: router_interface', + 'id': '3', + 'tenant_id': 'demo_tenant_id'}], + 'tenant_id': 'demo_tenant_id', + 'id': '0b109a4e-d228-479d-ad43-08bf3245adbb', + 'name': 'demo_router' + } + fwg1 = { + 'status': 'ACTIVE', + 'admin_state_up': True, + 'tenant_id': 'demo_tenant_id', + 'del-port-ids': [], + 'add-port-ids': ['1', '3'], + 'id': '2932b3d9-3a7b-48a1-a16c-bf9f7b2751a5' + } + fwg2 = { + 'status': 'ACTIVE', + 'admin_state_up': True, + 'tenant_id': 'demo_tenant_id', + 'del-port-ids': [], + 'add-port-ids': ['2', '3'], + 'id': '2932b3d9-3a7b-48a1-a16c-bf9f7b2751a5' + } + agent = fw_agent(cfg.CONF) + agent.agent_api = mock.Mock() + agent.fwplugin_rpc = mock.Mock() + agent.conf.agent_mode = mock.Mock() + agent.fwaas_driver = iptables_fwaas_v2.IptablesFwaasDriver() + + patch_project = mock.patch.object( + agent.fwplugin_rpc, 'get_firewall_groups_for_project') + patch_invoke = mock.patch.object( + agent, '_invoke_driver_for_sync_from_plugin') + + with patch_project as mock_get_firewall_groups, \ + patch_invoke as mock_invoke_driver: + mock_get_firewall_groups.return_value = [fwg1, fwg2] + agent.add_router(self.context, updated_router) + + # Check that mock_invoke_driver was called exactly twice with + # correct arguments. + self.assertEqual([ + mock.call(mock.ANY, {'1', '3'}, fwg1), + mock.call(mock.ANY, {'2'}, fwg2), + ], mock_invoke_driver.call_args_list) + + def test_add_router(self): + fw_agent = _setup_test_agent_class([fwaas_constants.FIREWALL]) + cfg.CONF.set_override('enabled', True, 'fwaas') + new_router = { + '_interfaces': [{ + 'device_owner': 'network: router_interface', + 'id': '1', + 'tenant_id': 'demo_tenant_id', + }], + 'tenant_id': 'demo_tenant_id', + 'id': '0b109a4e-d228-479d-ad43-08bf3245adbb', + 'name': 'demo_router' + } + with mock.patch('oslo_utils.importutils.import_object'): + agent = fw_agent(cfg.CONF) + agent.agent_api = mock.Mock() + agent.fwplugin_rpc = mock.Mock() + agent.conf.agent_mode = mock.Mock() + agent.fwaas_driver = iptables_fwaas_v2.IptablesFwaasDriver() + with mock.patch.object(agent, + '_process_router_update', + ) as mock_process_router_update: + agent.add_router(self.context, new_router) + mock_process_router_update.assert_called_with(new_router) + + def test_update_router(self): + fw_agent = _setup_test_agent_class([fwaas_constants.FIREWALL]) + cfg.CONF.set_override('enabled', True, 'fwaas') + updated_router = { + '_interfaces': [{ + 'device_owner': 'network: router_interface', + 'id': '1', + 'tenant_id': 'demo_tenant_id', + }], + 'tenant_id': 'demo_tenant_id', + 'id': '0b109a4e-d228-479d-ad43-08bf3245adbb', + 'name': 'demo_router' + } + with mock.patch('oslo_utils.importutils.import_object'): + agent = fw_agent(cfg.CONF) + agent.agent_api = mock.Mock() + agent.fwplugin_rpc = mock.Mock() + agent.conf.agent_mode = mock.Mock() + agent.fwaas_driver = iptables_fwaas_v2.IptablesFwaasDriver() + with mock.patch.object(agent, + '_process_router_update', + ) as mock_process_router_update: + agent.update_router(self.context, updated_router) + mock_process_router_update.assert_called_with(updated_router) diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_agents.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_agents.py new file mode 100644 index 000000000..b3ee78462 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_agents.py @@ -0,0 +1,661 @@ +# Copyright 2016 +# 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. + +import mock +import six + +from neutron import extensions as neutron_extensions +from neutron.tests.unit.extensions import test_l3 +from neutron_lib import constants as nl_constants +from neutron_lib import context +from neutron_lib.exceptions import firewall_v2 as f_exc +from neutron_lib.plugins import directory +from oslo_config import cfg + +from neutron_fwaas._i18n import _ +from neutron_fwaas.db.firewall.v2.firewall_db_v2 import FirewallGroup +from neutron_fwaas.services.firewall.service_drivers.agents import agents +from neutron_fwaas.tests import base +from neutron_fwaas.tests.unit.services.firewall import test_fwaas_plugin_v2 + + +FIREWALL_AGENT_PLUGIN = ('neutron_fwaas.services.firewall.service_drivers.' + 'agents.agents') +FIREWALL_AGENT_PLUGIN_KLASS = FIREWALL_AGENT_PLUGIN + '.FirewallAgentDriver' +DELETEFW_PATH = (FIREWALL_AGENT_PLUGIN + '.FirewallAgentApi.' + 'delete_firewall_group') + + +class FakeAgentApi(agents.FirewallAgentCallbacks): + """ + This class used to mock the AgentAPI delete method inherits from + FirewallCallbacks because it needs access to the firewall_deleted method. + The delete_firewall method belongs to the FirewallAgentApi, which has + no access to the firewall_deleted method normally because it's not + responsible for deleting the firewall from the DB. However, it needs + to in the unit tests since there is no agent to call back. + """ + def __init__(self): + return + + def delete_firewall_group(self, context, firewall_group, **kwargs): + self.plugin = directory.get_plugin('FIREWALL_V2') + self.firewall_db = self.plugin.driver.firewall_db + self.firewall_group_deleted(context, firewall_group['id'], **kwargs) + + +class TestFirewallAgentApi(base.BaseTestCase): + def setUp(self): + super(TestFirewallAgentApi, self).setUp() + + self.api = agents.FirewallAgentApi('topic', 'host') + + def test_init(self): + self.assertEqual('topic', self.api.client.target.topic) + self.assertEqual('host', self.api.host) + + def _call_test_helper(self, method_name): + with mock.patch.object(self.api.client, 'cast') as rpc_mock, \ + mock.patch.object(self.api.client, 'prepare') as prepare_mock: + prepare_mock.return_value = self.api.client + getattr(self.api, method_name)(mock.sentinel.context, 'test') + + prepare_args = {'fanout': True} + prepare_mock.assert_called_once_with(**prepare_args) + + rpc_mock.assert_called_once_with(mock.sentinel.context, method_name, + firewall_group='test', host='host') + + def test_create_firewall_group(self): + self._call_test_helper('create_firewall_group') + + def test_update_firewall_group(self): + self._call_test_helper('update_firewall_group') + + def test_delete_firewall_group(self): + self._call_test_helper('delete_firewall_group') + + +class TestAgentDriver(test_fwaas_plugin_v2.FirewallPluginV2TestCase, + test_l3.L3NatTestCaseMixin): + + def setUp(self): + self._mock_agentapi_del_fw_p = mock.patch( + DELETEFW_PATH, create=True, + new=FakeAgentApi().delete_firewall_group, + ) + self.agentapi_del_fw_p = self._mock_agentapi_del_fw_p.start() + self._mock_get_client = mock.patch.object(agents.n_rpc, 'get_client') + self._mock_get_client.start() + mock.patch.object(agents.n_rpc, 'Connection').start() + + l3_plugin_str = ('neutron.tests.unit.extensions.test_l3.' + 'TestL3NatServicePlugin') + l3_plugin = {'l3_plugin_name': l3_plugin_str} + super(TestAgentDriver, self).setUp( + service_provider=FIREWALL_AGENT_PLUGIN_KLASS, + extra_service_plugins=l3_plugin, + extra_extension_paths=neutron_extensions.__path__) + + self.db = self.plugin.driver.firewall_db + self.callbacks = agents.FirewallAgentCallbacks(self.db) + + router_distributed_opts = [ + cfg.BoolOpt( + 'router_distributed', + default=False, + help=_("System-wide flag to determine the type of router " + "that tenants can create. Only admin can override.")), + ] + cfg.CONF.register_opts(router_distributed_opts) + + def tearDown(self): + self._mock_get_client.stop() + self._mock_agentapi_del_fw_p.stop() + super(TestAgentDriver, self).tearDown() + + @property + def _self_context(self): + return context.Context('', self._tenant_id) + + def _get_test_firewall_group_attrs(self, name, + status=nl_constants.INACTIVE): + return super(TestAgentDriver, self)._get_test_firewall_group_attrs( + name, status=status) + + def test_set_firewall_group_status(self): + ctx = context.get_admin_context() + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + ingress_firewall_policy_id=fwp_id, + admin_state_up=self.ADMIN_STATE_UP + ) as fwg: + fwg_id = fwg['firewall_group']['id'] + res = self.callbacks.set_firewall_group_status(ctx, fwg_id, + nl_constants.ACTIVE) + fwg_db = self.plugin.get_firewall_group(ctx, fwg_id) + self.assertEqual(nl_constants.ACTIVE, fwg_db['status']) + self.assertTrue(res) + res = self.callbacks.set_firewall_group_status(ctx, fwg_id, + nl_constants.ERROR) + fwg_db = self.plugin.get_firewall_group(ctx, fwg_id) + self.assertEqual(nl_constants.ERROR, fwg_db['status']) + self.assertFalse(res) + + def test_firewall_group_deleted(self): + ctx = context.get_admin_context() + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + ingress_firewall_policy_id=fwp_id, + admin_state_up=self.ADMIN_STATE_UP, + do_delete=False + ) as fwg: + fwg_id = fwg['firewall_group']['id'] + with ctx.session.begin(subtransactions=True): + fwg_db = self.db._get_firewall_group(ctx, fwg_id) + fwg_db['status'] = nl_constants.PENDING_DELETE + + observed = self.callbacks.firewall_group_deleted(ctx, fwg_id) + self.assertTrue(observed) + + self.assertRaises(f_exc.FirewallGroupNotFound, + self.plugin.get_firewall_group, + ctx, fwg_id) + + def test_firewall_group_deleted_concurrently(self): + ctx = context.get_admin_context() + alt_ctx = context.get_admin_context() + + _get_firewall_group = self.db._get_firewall_group + + def getdelete(context, fwg_id): + fwg_db = _get_firewall_group(context, fwg_id) + # NOTE(cby): Use a different session to simulate a concurrent del + with alt_ctx.session.begin(subtransactions=True): + alt_ctx.session.query(FirewallGroup).filter_by( + id=fwg_id).delete() + return fwg_db + + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + firewall_policy_id=fwp_id, + admin_state_up=self.ADMIN_STATE_UP, + do_delete=False + ) as fwg: + fwg_id = fwg['firewall_group']['id'] + with ctx.session.begin(subtransactions=True): + fwg_db = self.db._get_firewall_group(ctx, fwg_id) + fwg_db['status'] = nl_constants.PENDING_DELETE + ctx.session.flush() + + with mock.patch.object( + self.db, '_get_firewall_group', side_effect=getdelete + ): + observed = self.callbacks.firewall_group_deleted( + ctx, fwg_id) + self.assertTrue(observed) + + self.assertRaises(f_exc.FirewallGroupNotFound, + self.plugin.get_firewall_group, + ctx, fwg_id) + + def test_firewall_group_deleted_not_found(self): + ctx = context.get_admin_context() + observed = self.callbacks.firewall_group_deleted( + ctx, 'notfound') + self.assertTrue(observed) + + def test_firewall_group_deleted_error(self): + ctx = context.get_admin_context() + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + firewall_policy_id=fwp_id, + admin_state_up=self.ADMIN_STATE_UP, + ) as fwg: + fwg_id = fwg['firewall_group']['id'] + observed = self.callbacks.firewall_group_deleted( + ctx, fwg_id) + self.assertFalse(observed) + fwg_db = self.db._get_firewall_group(ctx, fwg_id) + self.assertEqual(nl_constants.ERROR, fwg_db['status']) + + def test_create_firewall_group_ports_not_specified(self): + """neutron firewall-create test-policy """ + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + name='test', + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.INACTIVE, + fwg1['firewall_group']['status']) + + def test_create_firewall_group_with_ports(self): + """neutron firewall_group create test-policy """ + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r, \ + self.subnet() as s1, \ + self.subnet(cidr='20.0.0.0/24') as s2: + + body = self._router_interface_action( + 'add', + r['router']['id'], + s1['subnet']['id'], + None) + port_id1 = body['port_id'] + + body = self._router_interface_action( + 'add', + r['router']['id'], + s2['subnet']['id'], + None) + port_id2 = body['port_id'] + fwg_ports = [port_id1, port_id2] + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + name='test', + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, ports=fwg_ports, + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.PENDING_CREATE, + fwg1['firewall_group']['status']) + + def test_create_firewall_group_with_ports_on_diff_routers(self): + """neutron firewall_group create test-policy """ + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r, \ + self.subnet() as s1, \ + self.subnet(cidr='20.0.0.0/24') as s2: + body = self._router_interface_action( + 'add', + r['router']['id'], + s1['subnet']['id'], + None) + port_id1 = body['port_id'] + body = self._router_interface_action( + 'add', + r['router']['id'], + s2['subnet']['id'], + None) + port_id2 = body['port_id'] + + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r2, \ + self.subnet() as s3: + + body = self._router_interface_action( + 'add', + r2['router']['id'], + s3['subnet']['id'], + None) + port_id3 = body['port_id'] + + fwg_ports = [port_id1, port_id2, port_id3] + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + name='test', + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, + ports=fwg_ports, + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.PENDING_CREATE, + fwg1['firewall_group']['status']) + + def test_create_firewall_group_with_ports_no_policy(self): + """neutron firewall_group create test-policy """ + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r, \ + self.subnet() as s1, \ + self.subnet(cidr='20.0.0.0/24') as s2: + + body = self._router_interface_action( + 'add', + r['router']['id'], + s1['subnet']['id'], + None) + port_id1 = body['port_id'] + + body = self._router_interface_action( + 'add', + r['router']['id'], + s2['subnet']['id'], + None) + port_id2 = body['port_id'] + fwg_ports = [port_id1, port_id2] + with self.firewall_group( + name='test', + default_policy=False, + ports=fwg_ports, + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.INACTIVE, + fwg1['firewall_group']['status']) + + def test_update_firewall_group_with_new_ports_no_policy(self): + """neutron firewall_group create test-policy """ + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r, \ + self.subnet() as s1, \ + self.subnet(cidr='20.0.0.0/24') as s2, \ + self.subnet(cidr='30.0.0.0/24') as s3: + + body = self._router_interface_action( + 'add', + r['router']['id'], + s1['subnet']['id'], + None) + port_id1 = body['port_id'] + + body = self._router_interface_action( + 'add', + r['router']['id'], + s2['subnet']['id'], + None) + port_id2 = body['port_id'] + + body = self._router_interface_action( + 'add', + r['router']['id'], + s3['subnet']['id'], + None) + port_id3 = body['port_id'] + + fwg_ports = [port_id1, port_id2] + with self.firewall_group( + name='test', + default_policy=False, + ports=fwg_ports, + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.INACTIVE, + fwg1['firewall_group']['status']) + data = {'firewall_group': {'ports': [port_id2, port_id3]}} + req = self.new_update_request('firewall_groups', data, + fwg1['firewall_group']['id'], + context=self._self_context) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + + self.assertEqual(sorted([port_id2, port_id3]), + sorted(res['firewall_group']['ports'])) + + self.assertEqual(nl_constants.INACTIVE, + res['firewall_group']['status']) + + def test_update_firewall_group_with_new_ports_status_pending(self): + """neutron firewall_group create test-policy """ + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r, \ + self.subnet() as s1, \ + self.subnet(cidr='20.0.0.0/24') as s2, \ + self.subnet(cidr='30.0.0.0/24') as s3: + + body = self._router_interface_action( + 'add', + r['router']['id'], + s1['subnet']['id'], + None) + port_id1 = body['port_id'] + + body = self._router_interface_action( + 'add', + r['router']['id'], + s2['subnet']['id'], + None) + port_id2 = body['port_id'] + fwg_ports = [port_id1, port_id2] + + body = self._router_interface_action( + 'add', + r['router']['id'], + s3['subnet']['id'], + None) + port_id3 = body['port_id'] + + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + name='test', + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, ports=fwg_ports, + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.PENDING_CREATE, + fwg1['firewall_group']['status']) + data = {'firewall_group': {'ports': [port_id2, port_id3]}} + req = self.new_update_request('firewall_groups', data, + fwg1['firewall_group']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(409, res.status_int) + + def test_update_firewall_group_with_new_ports_status_active(self): + """neutron firewall_group create test-policy """ + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r, \ + self.subnet() as s1, \ + self.subnet(cidr='20.0.0.0/24') as s2, \ + self.subnet(cidr='30.0.0.0/24') as s3: + + body = self._router_interface_action( + 'add', + r['router']['id'], + s1['subnet']['id'], + None) + port_id1 = body['port_id'] + + body = self._router_interface_action( + 'add', + r['router']['id'], + s2['subnet']['id'], + None) + port_id2 = body['port_id'] + fwg_ports = [port_id1, port_id2] + + body = self._router_interface_action( + 'add', + r['router']['id'], + s3['subnet']['id'], + None) + port_id3 = body['port_id'] + + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + name='test', + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, ports=fwg_ports, + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.PENDING_CREATE, + fwg1['firewall_group']['status']) + + ctx = context.get_admin_context() + self.callbacks.set_firewall_group_status(ctx, + fwg1['firewall_group']['id'], nl_constants.ACTIVE) + data = {'firewall_group': {'ports': [port_id2, port_id3]}} + req = self.new_update_request('firewall_groups', data, + fwg1['firewall_group']['id'], + context=self._self_context) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + self.assertEqual(sorted([port_id2, port_id3]), + sorted(res['firewall_group']['ports'])) + + def test_update_firewall_rule_on_active_fwg(self): + name = "new_firewall_rule1" + attrs = self._get_test_firewall_rule_attrs(name) + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r, \ + self.subnet() as s1: + + body = self._router_interface_action( + 'add', + r['router']['id'], + s1['subnet']['id'], + None) + port_id1 = body['port_id'] + with self.firewall_rule() as fwr: + with self.firewall_policy( + firewall_rules=[fwr['firewall_rule']['id']]) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + name='test', + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, ports=[port_id1], + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.PENDING_CREATE, + fwg1['firewall_group']['status']) + + ctx = context.get_admin_context() + self.callbacks.set_firewall_group_status(ctx, + fwg1['firewall_group']['id'], nl_constants.ACTIVE) + data = {'firewall_rule': {'name': name}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in six.iteritems(attrs): + self.assertEqual(v, res['firewall_rule'][k]) + + def test_update_firewall_rule_on_pending_create_fwg(self): + """update should fail""" + name = "new_firewall_rule1" + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r, \ + self.subnet() as s1: + + body = self._router_interface_action( + 'add', + r['router']['id'], + s1['subnet']['id'], + None) + port_id1 = body['port_id'] + with self.firewall_rule() as fwr: + with self.firewall_policy( + firewall_rules=[fwr['firewall_rule']['id']]) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + name='test', + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, ports=[port_id1], + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.PENDING_CREATE, + fwg1['firewall_group']['status']) + + data = {'firewall_rule': {'name': name}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = req.get_response(self.ext_api) + self.assertEqual(409, res.status_int) + + def test_update_firewall_group_with_non_exist_ports(self): + """neutron firewall_group create test-policy """ + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r, \ + self.subnet(cidr='30.0.0.0/24') as s: + body = self._router_interface_action( + 'add', + r['router']['id'], + s['subnet']['id'], + None) + port_id1 = body['port_id'] + foo_port_id = 'caef152d-b118-4b9b-bc77-800661bf082d' + fwg_ports = [port_id1] + with self.firewall_group( + name='test', + default_policy=False, + ports=fwg_ports, + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.INACTIVE, + fwg1['firewall_group']['status']) + data = {'firewall_group': {'ports': [foo_port_id]}} + req = self.new_update_request('firewall_groups', data, + fwg1['firewall_group']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + self.assertEqual('PortNotFound', + res['NeutronError']['type']) + + def test_update_firewall_group_with_ports_and_policy(self): + """neutron firewall_group create test-policy """ + with self.router(name='router1', admin_state_up=True, + tenant_id=self._tenant_id) as r,\ + self.subnet() as s1,\ + self.subnet(cidr='20.0.0.0/24') as s2: + + body = self._router_interface_action( + 'add', + r['router']['id'], + s1['subnet']['id'], + None) + port_id1 = body['port_id'] + + body = self._router_interface_action( + 'add', + r['router']['id'], + s2['subnet']['id'], + None) + port_id2 = body['port_id'] + + fwg_ports = [port_id1, port_id2] + with self.firewall_rule() as fwr: + with self.firewall_policy( + firewall_rules=[fwr['firewall_rule']['id']]) as fwp: + with self.firewall_group( + name='test', + default_policy=False, + ports=fwg_ports, + admin_state_up=True) as fwg1: + self.assertEqual(nl_constants.INACTIVE, + fwg1['firewall_group']['status']) + fwp_id = fwp["firewall_policy"]["id"] + + data = {'firewall_group': {'ports': fwg_ports}} + req = (self. + new_update_request('firewall_groups', data, + fwg1['firewall_group']['id'], + context=self._self_context)) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + self.assertEqual(nl_constants.INACTIVE, + res['firewall_group']['status']) + + data = {'firewall_group': { + 'ingress_firewall_policy_id': fwp_id}} + req = (self. + new_update_request('firewall_groups', data, + fwg1['firewall_group']['id'], + context=self._self_context)) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + self.assertEqual(nl_constants.PENDING_UPDATE, + res['firewall_group']['status']) + + def test_create_firewall_group_with_dvr(self): + cfg.CONF.set_override('router_distributed', True) + attrs = self._get_test_firewall_group_attrs("firewall1") + self._test_create_firewall_group(attrs) + + def test_create_firewall_group(self): + attrs = self._get_test_firewall_group_attrs("firewall1") + self._test_create_firewall_group(attrs) + + def test_create_firewall_group_with_empty_ports(self): + attrs = self._get_test_firewall_group_attrs("fwg1") + attrs['ports'] = [] + self._test_create_firewall_group(attrs) diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_firewall_agent_api.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_firewall_agent_api.py new file mode 100644 index 000000000..3115b09ab --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_firewall_agent_api.py @@ -0,0 +1,97 @@ +# Copyright (c) 2013 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. + +import mock + +from neutron_fwaas.services.firewall.service_drivers.agents.drivers \ + import fwaas_base +from neutron_fwaas.services.firewall.service_drivers.agents.drivers \ + import fwaas_base_v2 +from neutron_fwaas.services.firewall.service_drivers.agents \ + import firewall_agent_api as api +from neutron_fwaas.tests import base + + +class NoopFwaasDriver(fwaas_base.FwaasDriverBase): + """Noop Fwaas Driver. + + v1 firewall driver which does nothing. + This driver is for disabling Fwaas functionality. + """ + + def create_firewall_group(self, agent_mode, apply_list, firewall): + pass + + def delete_firewall_group(self, agent_mode, apply_list, firewall): + pass + + def update_firewall_group(self, agent_mode, apply_list, firewall): + pass + + def apply_default_policy(self, agent_mode, apply_list, firewall): + pass + + +class NoopFwaasDriverV2(fwaas_base_v2.FwaasDriverBase): + """Noop Fwaas Driver. + + v2 firewall driver which does nothing. + This driver is for disabling Fwaas functionality. + """ + + def create_firewall_group(self, agent_mode, apply_list, firewall): + pass + + def delete_firewall_group(self, agent_mode, apply_list, firewall): + pass + + def update_firewall_group(self, agent_mode, apply_list, firewall): + pass + + def apply_default_policy(self, agent_mode, apply_list, firewall): + pass + + +class TestFWaaSAgentApi(base.BaseTestCase): + def setUp(self): + super(TestFWaaSAgentApi, self).setUp() + + self.api = api.FWaaSPluginApiMixin( + 'topic', + 'host') + + def test_init(self): + self.assertEqual('host', self.api.host) + + def _test_firewall_method(self, method_name, **kwargs): + with mock.patch.object(self.api.client, 'call') as rpc_mock, \ + mock.patch.object(self.api.client, 'prepare') as prepare_mock: + + prepare_mock.return_value = self.api.client + getattr(self.api, method_name)(mock.sentinel.context, 'test', + **kwargs) + + prepare_args = {} + prepare_mock.assert_called_once_with(**prepare_args) + + rpc_mock.assert_called_once_with(mock.sentinel.context, method_name, + firewall_id='test', host='host', + **kwargs) + + def test_set_firewall_status(self): + self._test_firewall_method('set_firewall_status', status='fake_status') + + def test_firewall_deleted(self): + self._test_firewall_method('firewall_deleted') diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_firewall_service.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_firewall_service.py new file mode 100644 index 000000000..950cd4ed1 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/agents/test_firewall_service.py @@ -0,0 +1,61 @@ +# Copyright 2014 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. + +from neutron.tests import base +from oslo_config import cfg + +from neutron_fwaas.services.firewall.service_drivers.agents import\ + firewall_service + +FWAAS_NOP_DEVICE = ('neutron_fwaas.tests.unit.services.firewall.' + 'service_drivers.agents.test_firewall_agent_api.' + 'NoopFwaasDriver') + + +class TestFirewallDeviceDriverLoading(base.BaseTestCase): + + def setUp(self): + super(TestFirewallDeviceDriverLoading, self).setUp() + self.service = firewall_service.FirewallService() + + def test_loading_firewall_device_driver(self): + """Get the sole device driver for FWaaS.""" + cfg.CONF.set_override('driver', + FWAAS_NOP_DEVICE, + 'fwaas') + driver = self.service.load_device_drivers() + self.assertIsNotNone(driver) + self.assertIn(driver.__class__.__name__, FWAAS_NOP_DEVICE) + + def test_fail_no_such_firewall_device_driver(self): + """Failure test of import error for FWaaS device driver.""" + cfg.CONF.set_override('driver', + 'no.such.class', + 'fwaas') + self.assertRaises(ImportError, + self.service.load_device_drivers) + + def test_fail_firewall_no_device_driver_specified(self): + """Failure test when no FWaaS device driver is specified. + + This is a configuration error, as the user must specify a device + driver, when enabling the firewall service (and there is no default + configuration set. We'll simulate that by using an empty string. + """ + cfg.CONF.set_override('driver', + '', + 'fwaas') + self.assertRaises(ValueError, + self.service.load_device_drivers) diff --git a/neutron_fwaas/tests/unit/services/firewall/service_drivers/test_driver_api.py b/neutron_fwaas/tests/unit/services/firewall/service_drivers/test_driver_api.py new file mode 100644 index 000000000..60a5e2f76 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/service_drivers/test_driver_api.py @@ -0,0 +1,298 @@ +# Copyright (c) 2018 Fujitsu Limited. +# 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. + +import copy + +import mock +from neutron_lib.callbacks import events +from neutron_lib import context + +from neutron_fwaas.common import fwaas_constants as const +from neutron_fwaas.tests.unit.services.firewall import test_fwaas_plugin_v2 + + +class FireWallDriverDBMixinTestCase(test_fwaas_plugin_v2. + FirewallPluginV2TestCase): + + def setUp(self): + provider = ('neutron_fwaas.services.firewall.service_drivers.' + 'driver_api.FirewallDriverDB') + super(FireWallDriverDBMixinTestCase, self).setUp( + service_provider=provider) + self._mp_registry_publish = mock.patch( + 'neutron_lib.callbacks.registry.publish') + self.mock_registry_publish = self._mp_registry_publish.start() + self.driver_api = self.plugin.driver + self.ctx = context.get_admin_context() + self.firewall_db = self.plugin.driver.firewall_db + self.m_payload = mock.Mock() + self._mock_payload = mock.patch( + 'neutron_lib.callbacks.events.DBEventPayload') + m_db_event_payload = self._mock_payload.start() + m_db_event_payload.return_value = self.m_payload + self.fake_fwg = { + 'id': 'fake_fwg_id', + 'ingress_firewall_policy_id': 'fake_ifwp_id', + 'egress_firewall_policy_id': 'fake_efwp_id', + 'ports': [], + 'tenant_id': 'fake_tenant_id', + 'status': 'CREATED' + } + + self.fake_fwp = { + 'id': 'fake_fwp_id', + 'firewall_rules': [], + 'info': 'fake_rule_info', + 'project_id': 'fake_project_id' + } + + self.fake_fwr = { + 'id': 'fake_fwr_id', + 'firewall_policy_id': [], + 'project_id': 'fake_project_id' + } + + def tearDown(self): + self._mock_payload.stop() + self._mp_registry_publish.stop() + super(FireWallDriverDBMixinTestCase, self).tearDown() + + # Test Firewall Group + def test_create_firewall_group(self): + + with mock.patch.object(self.firewall_db, 'create_firewall_group', + return_value=self.fake_fwg): + self.driver_api.create_firewall_group_postcommit = mock.Mock() + self.driver_api.create_firewall_group(self.ctx, self.fake_fwg) + self.driver_api.create_firewall_group_postcommit.\ + assert_called_once_with(self.ctx, self.fake_fwg) + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_GROUP, + events.AFTER_CREATE, + self.driver_api, + payload=self.m_payload) + + def test_delete_firewall_group(self): + + with mock.patch.object(self.firewall_db, 'get_firewall_group', + return_value=self.fake_fwg): + self.driver_api.delete_firewall_group_postcommit = mock.Mock() + self.driver_api.delete_firewall_group(self.ctx, 'fake_fwg_id') + self.driver_api.delete_firewall_group_postcommit.\ + assert_called_once_with(self.ctx, self.fake_fwg) + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_GROUP, + events.AFTER_DELETE, + self.driver_api, + payload=self.m_payload) + + def test_update_firewall_group(self): + fake_fwg_delta = { + 'ingress_firewall_policy_id': 'fake_ifwp_delta_id', + 'egress_firewall_policy_id': 'fake_efwp_delta_id', + 'ports': [], + } + + old_fake_fwg = { + 'id': 'fake_fwg_id', + 'ingress_firewall_policy_id': 'old_fake_ifwp_id', + 'egress_firewall_policy_id': 'old_fake_efwp_id', + 'ports': [], + 'tenant_id': 'fake_tenant_id', + 'status': 'CREATED' + } + + with mock.patch.object(self.firewall_db, 'get_firewall_group', + return_value=old_fake_fwg): + new_fake_fwg = copy.deepcopy(old_fake_fwg) + new_fake_fwg.update(fake_fwg_delta) + + with mock.patch.object(self.firewall_db, 'update_firewall_group', + return_value=new_fake_fwg): + self.driver_api.\ + update_firewall_group_postcommit = mock.Mock() + self.driver_api.\ + update_firewall_group(self.ctx, 'fake_fwg_id', + fake_fwg_delta) + self.driver_api.update_firewall_group_postcommit.\ + assert_called_once_with(self.ctx, old_fake_fwg, + new_fake_fwg) + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_GROUP, + events.AFTER_UPDATE, + self.driver_api, + payload=self.m_payload) + + # Test Firewall Policy + def test_create_firewall_policy(self): + + with mock.patch.object(self.firewall_db, 'create_firewall_policy', + return_value=self.fake_fwp): + self.driver_api.create_firewall_policy_postcommit = mock.Mock() + self.driver_api.create_firewall_policy(self.ctx, self.fake_fwp) + self.driver_api.create_firewall_policy_postcommit.\ + assert_called_once_with(self.ctx, self.fake_fwp) + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_POLICY, + events.AFTER_CREATE, + self.driver_api, + payload=self.m_payload) + + def test_delete_firewall_policy(self): + + with mock.patch.object(self.firewall_db, 'delete_firewall_policy'): + with mock.patch.object(self.firewall_db, 'get_firewall_policy', + return_value=self.fake_fwp): + self.driver_api.\ + delete_firewall_policy_postcommit = mock.Mock() + self.driver_api.\ + delete_firewall_policy(self.ctx, 'fake_fwp_id') + self.driver_api.delete_firewall_policy_postcommit.\ + assert_called_once_with(self.ctx, self.fake_fwp) + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_POLICY, + events.AFTER_UPDATE, + self.driver_api, + payload=self.m_payload) + + def test_update_firewall_policy(self): + fake_fwp_delta = { + 'firewall_rules': [], + } + + old_fake_fwp = { + 'id': 'fake_fwp_id', + 'firewall_rules': [], + 'project_id': 'fake_project_id' + } + + with mock.patch.object(self.firewall_db, 'get_firewall_policy', + return_value=old_fake_fwp): + new_fake_fwp = copy.deepcopy(old_fake_fwp) + new_fake_fwp.update(fake_fwp_delta) + + with mock.patch.object(self.firewall_db, 'update_firewall_policy', + return_value=new_fake_fwp): + self.driver_api.\ + update_firewall_policy_postcommit = mock.Mock() + self.driver_api.\ + update_firewall_policy(self.ctx, 'fake_fwp_id', + fake_fwp_delta) + self.driver_api.update_firewall_policy_postcommit.\ + assert_called_once_with(self.ctx, old_fake_fwp, + new_fake_fwp) + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_POLICY, + events.AFTER_UPDATE, + self.driver_api, + payload=self.m_payload) + + # Test Firewall Rule + def test_create_firewall_rule(self): + + with mock.patch.object(self.firewall_db, 'create_firewall_rule', + return_value=self.fake_fwr): + self.driver_api.create_firewall_rule_postcommit = mock.Mock() + self.driver_api.create_firewall_rule(self.ctx, self.fake_fwr) + self.driver_api.create_firewall_rule_postcommit.\ + assert_called_once_with(self.ctx, self.fake_fwr) + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_RULE, + events.AFTER_CREATE, + self.driver_api, + payload=self.m_payload) + + def test_delete_firewall_rule(self): + + self.firewall_db.delete_firewall_rule = mock.Mock() + + with mock.patch.object(self.firewall_db, 'get_firewall_rule', + return_value=self.fake_fwr): + self.driver_api.\ + delete_firewall_rule_postcommit = mock.Mock() + self.driver_api.\ + delete_firewall_rule(self.ctx, 'fake_fwr_id') + self.driver_api.delete_firewall_rule_postcommit.\ + assert_called_once_with(self.ctx, self.fake_fwr) + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_RULE, + events.AFTER_DELETE, + self.driver_api, + payload=self.m_payload) + + def test_update_firewall_rule(self): + + fake_fwr_delta = { + 'firewall_policy_id': [], + } + + old_fake_fwr = { + 'id': 'fake_fwr_id', + 'firewall_policy_id': [], + 'project_id': 'fake_project_id' + } + + with mock.patch.object(self.firewall_db, 'get_firewall_rule', + return_value=old_fake_fwr): + new_fake_fwr = copy.deepcopy(old_fake_fwr) + new_fake_fwr.update(fake_fwr_delta) + + with mock.patch.object(self.firewall_db, 'update_firewall_rule', + return_value=new_fake_fwr): + self.driver_api.\ + update_firewall_rule_postcommit = mock.Mock() + self.driver_api. \ + update_firewall_rule(self.ctx, 'fake_fwr_id', + fake_fwr_delta) + self.driver_api.update_firewall_rule_postcommit.\ + assert_called_once_with(self.ctx, old_fake_fwr, + new_fake_fwr) + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_RULE, + events.AFTER_UPDATE, + self.driver_api, + payload=self.m_payload) + + def test_insert_rule(self): + + with mock.patch.object(self.firewall_db, 'insert_rule', + return_value=self.fake_fwp): + self.driver_api.insert_rule_postcommit = mock.Mock() + self.driver_api.insert_rule(self.ctx, 'fake_fwp_id', + 'fake_rule_info') + self.driver_api.insert_rule_postcommit.\ + assert_called_once_with(self.ctx, 'fake_fwp_id', + 'fake_rule_info') + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_POLICY, + events.AFTER_UPDATE, + self.driver_api, + payload=self.m_payload) + + def test_remove_rule(self): + + with mock.patch.object(self.firewall_db, 'remove_rule', + return_value=self.fake_fwp): + self.driver_api.remove_rule_postcommit = mock.Mock() + self.driver_api.remove_rule(self.ctx, 'fake_fwp_id', + 'fake_rule_info') + self.driver_api.remove_rule_postcommit.\ + assert_called_once_with(self.ctx, 'fake_fwp_id', + 'fake_rule_info') + self.mock_registry_publish.\ + assert_called_with(const.FIREWALL_POLICY, + events.AFTER_UPDATE, + self.driver_api, + payload=self.m_payload) diff --git a/neutron_fwaas/tests/unit/services/firewall/test_fwaas_plugin_v2.py b/neutron_fwaas/tests/unit/services/firewall/test_fwaas_plugin_v2.py new file mode 100644 index 000000000..5d6582d51 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/test_fwaas_plugin_v2.py @@ -0,0 +1,738 @@ +# Copyright 2016 +# 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. + +import contextlib + +import mock +import six +import webob.exc + +from neutron.api import extensions as api_ext +from neutron.db import servicetype_db as sdb +from neutron.tests.unit.db import test_db_base_plugin_v2 as test_db_plugin +from neutron_lib.api.definitions import firewall_v2 +from neutron_lib.callbacks import events +from neutron_lib import constants as nl_constants +from neutron_lib import context +from neutron_lib.exceptions import firewall_v2 as f_exc +from neutron_lib.plugins import directory +from oslo_utils import importutils + +from neutron_fwaas.common import fwaas_constants +from neutron_fwaas import extensions +from neutron_fwaas.services.firewall import fwaas_plugin_v2 +from neutron_fwaas.services.firewall.service_drivers.driver_api import \ + FirewallDriverDB + + +def http_client_error(req, res): + explanation = "Request '%s %s %s' failed: %s" % (req.method, req.url, + req.body, res.body) + return webob.exc.HTTPClientError(code=res.status_int, + explanation=explanation) + + +class DummyDriverDB(FirewallDriverDB): + def is_supported_l2_port(self, port): + return True + + def is_supported_l3_port(self, port): + return True + + +class FirewallPluginV2TestCase(test_db_plugin.NeutronDbPluginV2TestCase): + DESCRIPTION = 'default description' + PROTOCOL = 'tcp' + IP_VERSION = 4 + SOURCE_IP_ADDRESS_RAW = '1.1.1.1' + DESTINATION_IP_ADDRESS_RAW = '2.2.2.2' + SOURCE_PORT = '55000:56000' + DESTINATION_PORT = '56000:57000' + ACTION = 'allow' + AUDITED = True + ENABLED = True + ADMIN_STATE_UP = True + SHARED = True + + resource_prefix_map = dict( + (k, firewall_v2.API_PREFIX) + for k in firewall_v2.RESOURCE_ATTRIBUTE_MAP.keys() + ) + + def setUp(self, service_provider=None, core_plugin=None, + extra_service_plugins=None, extra_extension_paths=None): + provider = fwaas_constants.FIREWALL_V2 + if not service_provider: + provider += (':dummy:neutron_fwaas.tests.unit.services.firewall.' + 'test_fwaas_plugin_v2.DummyDriverDB:default') + else: + provider += ':test:' + service_provider + ':default' + + bits = provider.split(':') + provider = { + 'service_type': bits[0], + 'name': bits[1], + 'driver': bits[2], + 'default': True, + } + # override the default service provider + self.service_providers = ( + mock.patch.object(sdb.ServiceTypeManager, + 'get_service_providers').start()) + self.service_providers.return_value = [provider] + + plugin_str = ('neutron_fwaas.services.firewall.fwaas_plugin_v2.' + 'FirewallPluginV2') + service_plugins = {'fw_plugin_name': plugin_str} + service_plugins.update(extra_service_plugins or {}) + + # we need to provide a plugin instance, although the extension manager + # will create a new instance of the plugin + plugins = { + fwaas_constants.FIREWALL_V2: fwaas_plugin_v2.FirewallPluginV2(), + } + for plugin_name, plugin_str in (extra_service_plugins or {}).items(): + plugins[plugin_name] = importutils.import_object(plugin_str) + ext_mgr = api_ext.PluginAwareExtensionManager( + ':'.join(extensions.__path__ + (extra_extension_paths or [])), + plugins, + ) + + super(FirewallPluginV2TestCase, self).setUp( + plugin=core_plugin, + service_plugins=service_plugins, + ext_mgr=ext_mgr, + ) + + # find the Firewall plugin that was instantiated by the extension + # manager + self.plugin = directory.get_plugin(fwaas_constants.FIREWALL_V2) + + def _get_admin_context(self): + # FIXME NOTE(ivasilevskaya) seems that test framework treats context + # with user_id=None/tenant_id=None (return value of + # context._get_admin_context() method) in a somewhat special way. + # So as a workaround to have the framework behave properly right now + # let's implement our own _get_admin_context method and look into the + # matter some other time. + return context.Context(user_id='admin', + tenant_id='admin-tenant', + is_admin=True) + + def _get_nonadmin_context(self, user_id='non-admin', tenant_id='tenant1'): + return context.Context(user_id=user_id, tenant_id=tenant_id) + + def _test_list_resources(self, resource, items, + neutron_context=None, + query_params=None): + if resource.endswith('y'): + resource_plural = resource.replace('y', 'ies') + else: + resource_plural = resource + 's' + + res = self._list(resource_plural, + neutron_context=neutron_context, + query_params=query_params) + resource = resource.replace('-', '_') + self.assertEqual( + sorted([i[resource]['id'] for i in items]), + sorted([i['id'] for i in res[resource_plural]])) + + def _list_req(self, resource_plural, ctx=None): + if not ctx: + ctx = self._get_admin_context() + req = self.new_list_request(resource_plural) + req.environ['neutron.context'] = ctx + return self.deserialize( + self.fmt, req.get_response(self.ext_api))[resource_plural] + + def _show_req(self, resource_plural, obj_id, ctx=None): + req = self.new_show_request(resource_plural, obj_id, fmt=self.fmt) + if not ctx: + ctx = self._get_admin_context() + req.environ['neutron.context'] = ctx + res = self.deserialize( + self.fmt, req.get_response(self.ext_api)) + return res + + def _build_default_fwg(self, ctx=None, is_one=True): + res = self._list_req('firewall_groups', ctx=ctx) + if is_one: + self.assertEqual(1, len(res)) + return res[0] + return res + + def _get_test_firewall_rule_attrs(self, name='firewall_rule1'): + attrs = {'name': name, + 'tenant_id': self._tenant_id, + 'project_id': self._tenant_id, + 'protocol': self.PROTOCOL, + 'ip_version': self.IP_VERSION, + 'source_ip_address': self.SOURCE_IP_ADDRESS_RAW, + 'destination_ip_address': self.DESTINATION_IP_ADDRESS_RAW, + 'source_port': self.SOURCE_PORT, + 'destination_port': self.DESTINATION_PORT, + 'action': self.ACTION, + 'enabled': self.ENABLED, + 'shared': self.SHARED} + return attrs + + def _get_test_firewall_policy_attrs(self, name='firewall_policy1', + audited=AUDITED): + attrs = {'name': name, + 'description': self.DESCRIPTION, + 'tenant_id': self._tenant_id, + 'project_id': self._tenant_id, + 'firewall_rules': [], + 'audited': audited, + 'shared': self.SHARED} + return attrs + + def _get_test_firewall_group_attrs(self, name='firewall_1', + status=nl_constants.CREATED): + attrs = {'name': name, + 'tenant_id': self._tenant_id, + 'project_id': self._tenant_id, + 'admin_state_up': self.ADMIN_STATE_UP, + 'status': status} + + return attrs + + def _create_firewall_policy(self, fmt, name, description, shared, + firewall_rules, audited, + expected_res_status=None, **kwargs): + data = {'firewall_policy': {'name': name, + 'description': description, + 'firewall_rules': firewall_rules, + 'audited': audited, + 'shared': shared}} + ctx = kwargs.get('context', None) + if ctx is None or ctx.is_admin: + tenant_id = kwargs.get('tenant_id', self._tenant_id) + data['firewall_policy'].update({'tenant_id': tenant_id}) + data['firewall_policy'].update({'project_id': tenant_id}) + + req = self.new_create_request('firewall_policies', data, fmt, + context=ctx) + res = req.get_response(self.ext_api) + if expected_res_status: + self.assertEqual(expected_res_status, res.status_int) + elif res.status_int >= 400: + raise http_client_error(req, res) + + return res + + def _replace_firewall_status(self, attrs, old_status, new_status): + if attrs['status'] is old_status: + attrs['status'] = new_status + return attrs + + @contextlib.contextmanager + def firewall_policy(self, fmt=None, name='firewall_policy1', + description=DESCRIPTION, shared=SHARED, + firewall_rules=None, audited=True, + do_delete=True, **kwargs): + if firewall_rules is None: + firewall_rules = [] + if not fmt: + fmt = self.fmt + res = self._create_firewall_policy(fmt, name, description, shared, + firewall_rules, audited, **kwargs) + if res.status_int >= 400: + raise webob.exc.HTTPClientError(code=res.status_int) + firewall_policy = self.deserialize(fmt or self.fmt, res) + yield firewall_policy + if do_delete: + self._delete('firewall_policies', + firewall_policy['firewall_policy']['id']) + + def _create_firewall_rule(self, fmt, name, shared, protocol, + ip_version, source_ip_address, + destination_ip_address, source_port, + destination_port, action, enabled, + expected_res_status=None, **kwargs): + tenant_id = kwargs.get('tenant_id', self._tenant_id) + data = {'firewall_rule': {'name': name, + 'protocol': protocol, + 'ip_version': ip_version, + 'source_ip_address': source_ip_address, + 'destination_ip_address': + destination_ip_address, + 'source_port': source_port, + 'destination_port': destination_port, + 'action': action, + 'enabled': enabled, + 'shared': shared}} + ctx = kwargs.get('context', None) + if ctx is None or ctx.is_admin: + tenant_id = kwargs.get('tenant_id', self._tenant_id) + data['firewall_rule'].update({'tenant_id': tenant_id}) + data['firewall_rule'].update({'project_id': tenant_id}) + + req = self.new_create_request('firewall_rules', data, fmt, context=ctx) + res = req.get_response(self.ext_api) + if expected_res_status: + self.assertEqual(expected_res_status, res.status_int) + elif res.status_int >= 400: + raise http_client_error(req, res) + + return res + + @contextlib.contextmanager + def firewall_rule(self, fmt=None, name='firewall_rule1', + shared=SHARED, protocol=PROTOCOL, ip_version=IP_VERSION, + source_ip_address=SOURCE_IP_ADDRESS_RAW, + destination_ip_address=DESTINATION_IP_ADDRESS_RAW, + source_port=SOURCE_PORT, + destination_port=DESTINATION_PORT, + action=ACTION, enabled=ENABLED, + do_delete=True, **kwargs): + if not fmt: + fmt = self.fmt + res = self._create_firewall_rule(fmt, name, shared, protocol, + ip_version, source_ip_address, + destination_ip_address, + source_port, destination_port, + action, enabled, **kwargs) + if res.status_int >= 400: + raise webob.exc.HTTPClientError(code=res.status_int) + firewall_rule = self.deserialize(fmt or self.fmt, res) + yield firewall_rule + if do_delete: + self._delete('firewall_rules', + firewall_rule['firewall_rule']['id']) + + def _create_firewall_group(self, fmt, name, description, + ingress_firewall_policy_id=None, + egress_firewall_policy_id=None, + ports=None, admin_state_up=True, + expected_res_status=None, **kwargs): + if ingress_firewall_policy_id is None: + default_policy = kwargs.get('default_policy', True) + if default_policy: + res = self._create_firewall_policy( + fmt, + 'fwp', + description=self.DESCRIPTION, + shared=self.SHARED, + firewall_rules=[], + audited=self.AUDITED, + ) + firewall_policy = self.deserialize(fmt or self.fmt, res) + fwp_id = firewall_policy["firewall_policy"]["id"] + ingress_firewall_policy_id = fwp_id + data = {'firewall_group': {'name': name, + 'description': description, + 'ingress_firewall_policy_id': ingress_firewall_policy_id, + 'egress_firewall_policy_id': egress_firewall_policy_id, + 'admin_state_up': admin_state_up}} + ctx = kwargs.get('context', None) + if ctx is None or ctx.is_admin: + tenant_id = kwargs.get('tenant_id', self._tenant_id) + data['firewall_group'].update({'tenant_id': tenant_id}) + data['firewall_group'].update({'project_id': tenant_id}) + if ports is not None: + data['firewall_group'].update({'ports': ports}) + + req = self.new_create_request('firewall_groups', data, fmt, + context=ctx) + res = req.get_response(self.ext_api) + if expected_res_status: + self.assertEqual(expected_res_status, res.status_int) + elif res.status_int >= 400: + raise http_client_error(req, res) + return res + + @contextlib.contextmanager + def firewall_group(self, fmt=None, name='firewall_1', + description=DESCRIPTION, + ingress_firewall_policy_id=None, + egress_firewall_policy_id=None, + ports=None, admin_state_up=True, + do_delete=True, **kwargs): + if not fmt: + fmt = self.fmt + res = self._create_firewall_group(fmt, name, description, + ingress_firewall_policy_id, + egress_firewall_policy_id, + ports=ports, + admin_state_up=admin_state_up, + **kwargs) + if res.status_int >= 400: + raise webob.exc.HTTPClientError(code=res.status_int) + firewall_group = self.deserialize(fmt or self.fmt, res) + yield firewall_group + if do_delete: + self._delete('firewall_groups', + firewall_group['firewall_group']['id']) + + def _rule_action(self, action, id, firewall_rule_id, insert_before=None, + insert_after=None, expected_code=webob.exc.HTTPOk.code, + expected_body=None, body_data=None): + # We intentionally do this check for None since we want to distinguish + # from empty dictionary + if body_data is None: + if action == 'insert': + body_data = {'firewall_rule_id': firewall_rule_id, + 'insert_before': insert_before, + 'insert_after': insert_after} + else: + body_data = {'firewall_rule_id': firewall_rule_id} + + req = self.new_action_request('firewall_policies', + body_data, id, + "%s_rule" % action) + res = req.get_response(self.ext_api) + self.assertEqual(expected_code, res.status_int) + response = self.deserialize(self.fmt, res) + if expected_body: + self.assertEqual(expected_body, response) + return response + + def _compare_firewall_rule_lists(self, firewall_policy_id, + observed_list, expected_list): + position = 0 + for r1, r2 in zip(observed_list, expected_list): + rule = r1['firewall_rule'] + rule['firewall_policy_id'] = firewall_policy_id + position += 1 + rule['position'] = position + for k in rule: + self.assertEqual(r2[k], rule[k]) + + def _test_create_firewall_group(self, attrs): + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['ingress_firewall_policy_id'] = fwp_id + attrs['egress_firewall_policy_id'] = fwp_id + with self.firewall_group( + name=attrs['name'], + ingress_firewall_policy_id=fwp_id, + egress_firewall_policy_id=fwp_id, + admin_state_up=self.ADMIN_STATE_UP, + ports=attrs['ports'] if 'ports' in attrs else None, + ) as firewall_group: + for k, v in six.iteritems(attrs): + self.assertEqual(v, firewall_group['firewall_group'][k]) + + +class TestFirewallPluginBasev2(FirewallPluginV2TestCase): + + def _test_fwg_with_port(self, device_owner): + with self.port(device_owner=device_owner) as port: + with self.firewall_rule() as fwr: + fwr_id = fwr['firewall_rule']['id'] + with self.firewall_policy(firewall_rules=[fwr_id]) as fwp: + fwp_id = fwp['firewall_policy']['id'] + self.firewall_group( + self.fmt, + "firewall_group", + self.DESCRIPTION, + ports=[port['port']['id']], + ingress_firewall_policy_id=fwp_id, + ) + + def test_create_fwg_with_l3_ports(self): + for device_owner_for_l3 in nl_constants.ROUTER_INTERFACE_OWNERS: + self._test_fwg_with_port(device_owner_for_l3) + + def test_create_fwg_with_l2_port(self): + device_owner_for_l2 = nl_constants.DEVICE_OWNER_COMPUTE_PREFIX + 'nova' + self._test_fwg_with_port(device_owner_for_l2) + + def test_create_firewall_group_with_port_on_different_project(self): + with self.port(tenant_id='fake_project_id_1') as port: + admin_ctx = context.get_admin_context() + self._create_firewall_group( + self.fmt, + "firewall_group1", + self.DESCRIPTION, + context=admin_ctx, + ports=[port['port']['id']], + expected_res_status=webob.exc.HTTPConflict.code, + ) + + def test_update_firewall_group_with_port_on_different_project(self): + ctx = context.Context('not_admin', 'fake_project_id_1') + with self.firewall_group(ctx=ctx) as firewall_group: + with self.port(tenant_id='fake_project_id_2') as port: + data = { + 'firewall_group': { + 'ports': [port['port']['id']], + }, + } + req = self.new_update_request( + 'firewall_groups', + data, + firewall_group['firewall_group']['id'], + ) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) + + def test_create_firewall_group_with_with_wrong_type_port(self): + with self.port(device_owner="wrong port type") as port: + self._create_firewall_group( + self.fmt, + "firewall_group1", + self.DESCRIPTION, + ports=[port['port']['id']], + expected_res_status=webob.exc.HTTPConflict.code, + ) + + def test_update_firewall_group_with_with_wrong_type_port(self): + with self.firewall_group() as firewall_group: + with self.port(device_owner="wrong port type") as port: + data = { + 'firewall_group': { + 'ports': [port['port']['id']], + }, + } + req = self.new_update_request( + 'firewall_groups', + data, + firewall_group['firewall_group']['id'], + ) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) + + def test_create_firewall_group_with_router_port_already_in_use(self): + with self.port( + device_owner=nl_constants.DEVICE_OWNER_ROUTER_INTF) as port: + with self.firewall_group(ports=[port['port']['id']]): + self._create_firewall_group( + self.fmt, + "firewall_group2", + self.DESCRIPTION, + ports=[port['port']['id']], + expected_res_status=webob.exc.HTTPConflict.code, + ) + + def test_create_firewall_group_with_dvr_port_already_in_use(self): + with self.port( + device_owner=nl_constants.DEVICE_OWNER_DVR_INTERFACE) as port: + with self.firewall_group(ports=[port['port']['id']]): + self._create_firewall_group( + self.fmt, + "firewall_group2", + self.DESCRIPTION, + ports=[port['port']['id']], + expected_res_status=webob.exc.HTTPConflict.code, + ) + + def test_update_firewall_group_with_port_already_in_use(self): + with self.port( + device_owner=nl_constants.DEVICE_OWNER_ROUTER_INTF) as port: + with self.firewall_group(ports=[port['port']['id']]): + with self.firewall_group() as firewall_group: + data = { + 'firewall_group': { + 'ports': [port['port']['id']], + }, + } + req = self.new_update_request( + 'firewall_groups', + data, + firewall_group['firewall_group']['id'], + ) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPConflict.code, + res.status_int) + + def test_firewall_group_policy_rule_can_be_updated(self): + pending_status = [nl_constants.PENDING_CREATE, + nl_constants.PENDING_UPDATE, + nl_constants.PENDING_DELETE] + + for status in pending_status: + with self.firewall_rule() as fwr: + fwr_id = fwr['firewall_rule']['id'] + with self.firewall_policy(firewall_rules=[fwr_id]) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_group( + ingress_firewall_policy_id=fwp_id) as fwg: + self.plugin.driver.firewall_db.\ + update_firewall_group_status( + context.get_admin_context(), + fwg['firewall_group']['id'], + status + ) + data = { + 'firewall_rule': { + 'name': 'new_name', + }, + } + req = self.new_update_request( + 'firewall_rules', + data, + fwr_id, + ) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPConflict.code, + res.status_int) + + def test_create_firewall_policy_with_other_project_not_shared_rule(self): + project1_context = self._get_nonadmin_context(tenant_id='project1') + project2_context = self._get_nonadmin_context(tenant_id='project2') + with self.firewall_rule(context=project1_context, shared=False) as fwr: + fwr_id = fwr['firewall_rule']['id'] + self.firewall_policy( + context=project2_context, + firewall_rules=[fwr_id], + expected_res_status=webob.exc.HTTPNotFound.code, + ) + + def test_update_firewall_policy_with_other_project_not_shared_rule(self): + project1_context = self._get_nonadmin_context(tenant_id='project1') + project2_context = self._get_nonadmin_context(tenant_id='project2') + with self.firewall_rule(context=project1_context, shared=False) as fwr: + with self.firewall_policy(context=project2_context, + shared=False) as fwp: + fwr_id = fwr['firewall_rule']['id'] + fwp_id = fwp['firewall_policy']['id'] + data = { + 'firewall_policy': { + 'firewall_rules': [fwr_id], + }, + } + req = self.new_update_request('firewall_policy', data, fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(webob.exc.HTTPNotFound.code, res.status_int) + + def test_create_firewall_policy_with_other_project_shared_rule(self): + admin_context = self._get_admin_context() + project1_context = self._get_nonadmin_context(tenant_id='project1') + with self.firewall_rule(context=admin_context, shared=True) as fwr: + fwr_id = fwr['firewall_rule']['id'] + self.firewall_policy( + context=project1_context, + firewall_rules=[fwr_id], + expected_res_status=webob.exc.HTTPOk.code, + ) + + +class TestAutomaticAssociation(TestFirewallPluginBasev2): + def setUp(self): + # TODO(yushiro): Replace constant value for this test class + # Set auto association fwg + super(TestAutomaticAssociation, self).setUp() + + def test_vm_port(self): + port = { + "id": "fake_port", + "device_owner": "compute:nova", + "binding:vif_type": "ovs", + "binding:vif_details": {"ovs_hybrid_plug": False}, + "project_id": "fake_project", + "port_security_enabled": True, + } + self.plugin._core_plugin.get_port = mock.Mock(return_value=port) + fake_default_fwg = { + 'id': 'fake_id', + 'name': 'default', + 'ports': ['fake_port_id1'], + } + self.plugin.get_firewall_groups = \ + mock.Mock(return_value=[fake_default_fwg]) + self.plugin.update_firewall_group = mock.Mock() + kwargs = { + "context": mock.ANY, + "port": port, + "original_port": {"binding:vif_type": "unbound"} + } + states = (kwargs['original_port'], kwargs['port']) + payload = events.DBEventPayload(mock.ANY, states=states) + self.plugin.handle_update_port( + "PORT", "after_update", "test_plugin", payload=payload) + self.plugin.get_firewall_groups.assert_called_once_with( + mock.ANY, + filters={ + 'tenant_id': [kwargs['port']['project_id']], + 'name': [fake_default_fwg['name']], + }, + fields=['id', 'ports'], + ) + port_ids = fake_default_fwg['ports'] + [kwargs['port']['id']] + self.plugin.update_firewall_group.assert_called_once_with( + mock.ANY, + fake_default_fwg['id'], + {'firewall_group': {'ports': port_ids}}, + ) + + def test_vm_port_not_newly_created(self): + self.plugin.get_firewall_group = mock.Mock() + self.plugin.update_firewall_group = mock.Mock() + # Just updated for VM port(name or description...etc.) + kwargs = { + "context": mock.ANY, + "port": { + "id": "fake_port", + "device_owner": "compute:nova", + "binding:vif_type": "ovs", + "project_id": "fake_project" + }, + "original_port": { + "device_owner": "compute:nova", + "binding:vif_type": "ovs", + "project_id": "fake_project" + } + } + states = (kwargs['original_port'], kwargs['port']) + payload = events.DBEventPayload(mock.ANY, states=states) + self.plugin.handle_update_port( + "PORT", "after_update", "test_plugin", payload=payload) + self.plugin.get_firewall_group.assert_not_called() + self.plugin.update_firewall_group.assert_not_called() + + def test_not_vm_port(self): + self.plugin.get_firewall_group = mock.Mock() + self.plugin.update_firewall_group = mock.Mock() + for device_owner in ["network:router_interface", + "network:router_gateway", + "network:dhcp"]: + + states = ({"device_owner": device_owner, + "binding:vif_type": "unbound", + "project_id": "fake_project"}, + {"id": "fake_port", + "device_owner": device_owner, + "project_id": "fake_project"}) + payload = events.DBEventPayload(mock.ANY, states=states) + self.plugin.handle_update_port( + "PORT", "after_update", "test_plugin", payload=payload) + self.plugin.get_firewall_group.assert_not_called() + self.plugin.update_firewall_group.assert_not_called() + + def test_set_port_for_default_firewall_group_raised_port_in_use(self): + port_id = 'fake_port_id_already_associated_to_default_fw' + port = { + "id": port_id, + "device_owner": "compute:nova", + "binding:vif_type": "ovs", + "binding:vif_details": {"ovs_hybrid_plug": False}, + "project_id": "fake_project", + "port_security_enabled": True, + } + self.plugin._core_plugin.get_port = mock.Mock(return_value=port) + self.plugin.get_firewall_groups = mock.Mock(return_value=[]) + self.plugin.update_firewall_group = mock.Mock( + side_effect=f_exc.FirewallGroupPortInUse(port_ids=[port_id])) + states = ({"binding:vif_type": "unbound"}, port) + payload = events.DBEventPayload(mock.ANY, states=states) + try: + self.plugin.handle_update_port("PORT", "after_update", + "test_plugin", payload=payload) + except f_exc.FirewallGroupPortInUse: + self.fail("Associating port to default firewall group raises " + "'FirewallGroupPortInUse' while it should ignore it") diff --git a/neutron_fwaas/tests/unit/services/logapi/__init__.py b/neutron_fwaas/tests/unit/services/logapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/logapi/agents/__init__.py b/neutron_fwaas/tests/unit/services/logapi/agents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/logapi/agents/drivers/__init__.py b/neutron_fwaas/tests/unit/services/logapi/agents/drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/__init__.py b/neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/test_driver.py b/neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/test_driver.py new file mode 100644 index 000000000..ccb07564a --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/test_driver.py @@ -0,0 +1,53 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +from neutron.services.logapi.drivers import base as log_base_driver +from neutron_fwaas.tests import base + +SUPPORTED_LOGGING_TYPES = ['firewall_group'] + + +class FakeDriver(log_base_driver.DriverBase): + + @staticmethod + def create(): + return FakeDriver( + name='fake_driver', + vif_types=[], + vnic_types=[], + supported_logging_types=SUPPORTED_LOGGING_TYPES, + requires_rpc=True + ) + + +class TestDriverBase(base.BaseTestCase): + + def setUp(self): + super(TestDriverBase, self).setUp() + self.driver = FakeDriver.create() + + def test_is_vif_type_compatible(self): + self.assertFalse( + self.driver.is_vif_type_compatible([])) + + def test_is_vnic_compatible(self): + self.assertFalse( + self.driver.is_vnic_compatible([])) + + def test_is_logging_type_supported(self): + self.assertTrue( + self.driver.is_logging_type_supported('firewall_group')) + self.assertFalse( + self.driver.is_logging_type_supported('security_group')) diff --git a/neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/test_log.py b/neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/test_log.py new file mode 100644 index 000000000..db1627c10 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/agents/drivers/iptables/test_log.py @@ -0,0 +1,341 @@ +# Copyright (c) 2018 Fujitsu Limited. +# 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. + +from collections import defaultdict + +import mock +from neutron.tests.unit.api.v2 import test_base +from neutron_lib.services.logapi import constants as log_const + +from neutron_fwaas.privileged.netfilter_log import libnetfilter_log as libnflog +from neutron_fwaas.services.logapi.agents.drivers.iptables import log +from neutron_fwaas.tests import base + +FAKE_PROJECT_ID = 'fake_project_id' +FAKE_PORT_ID = 'fake_port_id' +FAKE_FWG_ID = 'fake_fwg_id' +FAKE_LOG_ID = 'fake_log_id' +FAKE_RESOURCE_TYPE = 'firewall_group' + +FAKE_RATE = 100 +FAKE_BURST = 25 + + +class TestLogPrefix(base.BaseTestCase): + + def setUp(self): + super(TestLogPrefix, self).setUp() + self.log_prefix = log.LogPrefix(FAKE_PORT_ID, + 'fake_event', + FAKE_PROJECT_ID) + self.log_prefix.log_object_refs = set([FAKE_LOG_ID]) + + def test_add_log_obj_ref(self): + added_log_id = test_base._uuid + expected_log_obj_ref = set([FAKE_LOG_ID, added_log_id]) + self.log_prefix.add_log_obj_ref(added_log_id) + self.assertEqual(expected_log_obj_ref, self.log_prefix.log_object_refs) + + def test_remove_log_obj_ref(self): + expected_log_obj_ref = set() + self.log_prefix.remove_log_obj_ref(FAKE_LOG_ID) + self.assertEqual(expected_log_obj_ref, self.log_prefix.log_object_refs) + + def test_is_empty(self): + self.log_prefix.remove_log_obj_ref(FAKE_LOG_ID) + result = self.log_prefix.is_empty + self.assertEqual(True, result) + + +class BaseIptablesLogTestCase(base.BaseTestCase): + + def setUp(self): + super(BaseIptablesLogTestCase, self).setUp() + self.iptables_manager_patch = mock.patch( + 'neutron.agent.linux.iptables_manager.IptablesManager') + self.iptables_manager_mock = self.iptables_manager_patch.start() + resource_rpc_mock = mock.Mock() + + self.iptables_mock = mock.Mock() + self.v4filter_mock = mock.Mock() + self.v6filter_mock = mock.Mock() + self.iptables_mock.ipv4 = {'filter': self.v4filter_mock} + self.iptables_mock.ipv6 = {'filter': self.v6filter_mock} + + self.log_driver = log.IptablesLoggingDriver(mock.Mock()) + self.log_driver.iptables_manager = self.iptables_mock + self.log_driver.resource_rpc = resource_rpc_mock + self.context = mock.Mock() + self.log_driver.agent_api = mock.Mock() + + def test_start_logging(self): + fake_router_info = mock.Mock() + fake_router_info.router_id = 'fake_router_id' + fake_router_info.ns_name = 'fake_namespace' + libnflog.run_nflog = mock.Mock() + self.log_driver._create_firewall_group_log = mock.Mock() + + # Test with router_info that has internal ports + fake_router_info.internal_ports = [ + {'id': 'fake_port1'}, + {'id': 'fake_port2'}, + ] + fake_kwargs = { + 'router_info': fake_router_info + } + self.log_driver.ports_belong_router = defaultdict(set) + self.log_driver.start_logging(self.context, **fake_kwargs) + self.log_driver._create_firewall_group_log.\ + assert_called_once_with(self.context, + FAKE_RESOURCE_TYPE, + ports=fake_router_info.internal_ports, + router_id=fake_router_info.router_id) + + # Test with log_resources + fake_kwargs = { + 'log_resources': 'fake' + } + self.log_driver._create_firewall_group_log.reset_mock() + self.log_driver.start_logging(self.context, **fake_kwargs) + self.log_driver._create_firewall_group_log. \ + assert_called_once_with(self.context, + FAKE_RESOURCE_TYPE, + **fake_kwargs) + + def test_stop_logging(self): + fake_kwargs = { + 'log_resources': 'fake' + } + self.log_driver._delete_firewall_group_log = mock.Mock() + self.log_driver.stop_logging(self.context, **fake_kwargs) + self.log_driver._delete_firewall_group_log.\ + assert_called_once_with(self.context, **fake_kwargs) + fake_kwargs = { + 'fake': 'fake' + } + self.log_driver._delete_firewall_group_log.reset_mock() + self.log_driver.stop_logging(self.context, **fake_kwargs) + self.log_driver._delete_firewall_group_log.assert_not_called() + + def test_clean_up_unused_ipt_mgrs(self): + f_router_ids = ['r1', 'r2', 'r3'] + self.log_driver.ipt_mgr_list = self._fake_ipt_mgr_list(f_router_ids) + + # Test with a port is delete from router + self.log_driver.unused_port_ids = set(['r1_port1']) + self.log_driver._cleanup_unused_ipt_mgrs() + self.assertEqual(set(), self.log_driver.unused_port_ids) + self.assertIsNone(self.log_driver.ipt_mgr_list['r1'].get('r1_port1')) + + # Test with all ports are deleted from router + self.log_driver.unused_port_ids = set(['r2_port1', 'r2_port2']) + self.log_driver._cleanup_unused_ipt_mgrs() + self.assertEqual(set(), self.log_driver.unused_port_ids) + self.assertIsNone(self.log_driver.ipt_mgr_list.get('r2')) + + def test_get_intf_name(self): + fake_router = mock.Mock() + fake_port_id = 'fake_router_port_id' + + # Test with legacy router + self.log_driver.conf.agent_mode = 'legacy' + fake_router.router = { + 'fake': 'fake_mode' + } + with mock.patch.object(self.log_driver.agent_api, + 'get_router_hosting_port', + return_value=fake_router): + intf_name = self.log_driver._get_intf_name(fake_port_id) + expected_name = 'qr-fake_router' + self.assertEqual(expected_name, intf_name) + + # Test with dvr router + self.log_driver.conf.agent_mode = 'dvr_snat' + fake_router.router = { + 'distributed': 'fake_mode' + } + with mock.patch.object(self.log_driver.agent_api, + 'get_router_hosting_port', + return_value=fake_router): + intf_name = self.log_driver._get_intf_name(fake_port_id) + expected_name = 'sg-fake_router' + self.assertEqual(expected_name, intf_name) + + # Test with fip dev + self.log_driver.conf.agent_mode = 'dvr_snat' + fake_router.router = { + 'distributed': 'fake_mode' + } + fake_router.rtr_fip_connect = 'fake' + self.log_driver.conf.agent_mode = 'fake' + with mock.patch.object(self.log_driver.agent_api, + 'get_router_hosting_port', + return_value=fake_router): + intf_name = self.log_driver._get_intf_name(fake_port_id) + expected_name = 'rfp-fake_route' + self.assertEqual(expected_name, intf_name) + + def test_setup_chains(self): + self.log_driver._add_nflog_rules_accepted = mock.Mock() + self.log_driver._add_log_rules_dropped = mock.Mock() + m_ipt_mgr = mock.Mock() + m_fwg_port_log = mock.Mock() + + # Test with ALL event + m_fwg_port_log.event = log_const.ALL_EVENT + self.log_driver._setup_chains(m_ipt_mgr, m_fwg_port_log) + + self.log_driver._add_nflog_rules_accepted.\ + assert_called_once_with(m_ipt_mgr, m_fwg_port_log) + self.log_driver._add_log_rules_dropped.\ + assert_called_once_with(m_ipt_mgr, m_fwg_port_log) + + # Test with ACCEPT event + self.log_driver._add_nflog_rules_accepted.reset_mock() + self.log_driver._add_log_rules_dropped.reset_mock() + + m_fwg_port_log.event = log_const.ACCEPT_EVENT + self.log_driver._setup_chains(m_ipt_mgr, m_fwg_port_log) + + self.log_driver._add_nflog_rules_accepted.\ + assert_called_once_with(m_ipt_mgr, m_fwg_port_log) + self.log_driver._add_log_rules_dropped.assert_not_called() + + # Test with DROP event + self.log_driver._add_nflog_rules_accepted.reset_mock() + self.log_driver._add_log_rules_dropped.reset_mock() + + m_fwg_port_log.event = log_const.DROP_EVENT + self.log_driver._setup_chains(m_ipt_mgr, m_fwg_port_log) + + self.log_driver._add_nflog_rules_accepted.assert_not_called() + self.log_driver._add_log_rules_dropped.\ + assert_called_once_with(m_ipt_mgr, m_fwg_port_log) + + def test_add_nflog_rules_accepted(self): + ipt_mgr = mock.Mock() + f_accept_prefix = log.LogPrefix(FAKE_PORT_ID, log_const. + ACCEPT_EVENT, + FAKE_PROJECT_ID) + + f_port_log = self._fake_port_log('fake_log_id', + log_const.ACCEPT_EVENT, + FAKE_PORT_ID) + + self.log_driver._add_rules_to_chain_v4v6 = mock.Mock() + self.log_driver._get_ipt_mgr_by_port = mock.Mock(return_value=ipt_mgr) + self.log_driver._get_intf_name = mock.Mock(return_value='fake_device') + + with mock.patch.object(self.log_driver, '_get_prefix', + side_effect=[f_accept_prefix, None]): + + # Test with prefix already added into prefixes_table + self.log_driver._add_nflog_rules_accepted(ipt_mgr, f_port_log) + self.log_driver._add_rules_to_chain_v4v6.assert_not_called() + self.assertEqual(set(['fake_log_id']), + f_accept_prefix.log_object_refs) + + # Test with prefixes_tables does not include the prefix + prefix = log.LogPrefix(FAKE_PORT_ID, log_const. + ACCEPT_EVENT, FAKE_PROJECT_ID) + with mock.patch.object(log, 'LogPrefix', return_value=prefix): + self.log_driver._add_nflog_rules_accepted(ipt_mgr, f_port_log) + v4_rules, v6_rules = self._fake_nflog_rule_v4v6('fake_device', + prefix.id) + + self.log_driver._add_rules_to_chain_v4v6.\ + assert_called_once_with(ipt_mgr, 'accepted', + v4_rules, v6_rules, + wrap=True, top=True, tag=prefix.id) + self.assertEqual(set(['fake_log_id']), + prefix.log_object_refs) + + def test_add_nflog_rules_dropped(self): + ipt_mgr = mock.Mock() + f_drop_prefix = log.LogPrefix(FAKE_PORT_ID, log_const. + DROP_EVENT, + FAKE_PROJECT_ID) + + f_port_log = self._fake_port_log('fake_log_id', + log_const.DROP_EVENT, + FAKE_PORT_ID) + + self.log_driver._add_rules_to_chain_v4v6 = mock.Mock() + self.log_driver._get_ipt_mgr_by_port = mock.Mock(return_value=ipt_mgr) + self.log_driver._get_intf_name = mock.Mock(return_value='fake_device') + + with mock.patch.object(self.log_driver, '_get_prefix', + side_effect=[f_drop_prefix, None]): + + # Test with prefix already added into prefixes_table + self.log_driver._add_log_rules_dropped(ipt_mgr, f_port_log) + self.log_driver._add_rules_to_chain_v4v6.assert_not_called() + self.assertEqual(set(['fake_log_id']), + f_drop_prefix.log_object_refs) + + # Test with prefixes_tables does not include the prefix + prefix = log.LogPrefix(FAKE_PORT_ID, log_const. + ACCEPT_EVENT, FAKE_PROJECT_ID) + with mock.patch.object(log, 'LogPrefix', return_value=prefix): + self.log_driver._add_log_rules_dropped(ipt_mgr, f_port_log) + v4_rules, v6_rules = self._fake_nflog_rule_v4v6('fake_device', + prefix.id) + + calls = [ + mock.call(ipt_mgr, 'dropped', v4_rules, v6_rules, + wrap=True, top=True, tag=prefix.id), + mock.call(ipt_mgr, 'rejected', v4_rules, v6_rules, + wrap=True, top=True, tag=prefix.id), + ] + self.log_driver._add_rules_to_chain_v4v6.\ + assert_has_calls(calls) + self.assertEqual(set(['fake_log_id']), + prefix.log_object_refs) + + def _fake_port_log(self, log_id, event, port_id): + f_log_info = { + 'event': event, + 'project_id': FAKE_PROJECT_ID, + 'id': log_id + } + return log.FWGPortLog(port_id, f_log_info) + + def _fake_nflog_rule_v4v6(self, device, tag): + v4_nflog_rule = ['-i %s -m limit --limit %s/s --limit-burst %s ' + '-j NFLOG --nflog-prefix %s' + % (device, FAKE_RATE, FAKE_BURST, tag)] + v4_nflog_rule += ['-o %s -m limit --limit %s/s --limit-burst %s ' + '-j NFLOG --nflog-prefix %s' + % (device, FAKE_RATE, FAKE_BURST, tag)] + v6_nflog_rule = ['-i %s -m limit --limit %s/s --limit-burst %s ' + '-j NFLOG --nflog-prefix %s' + % (device, FAKE_RATE, FAKE_BURST, tag)] + v6_nflog_rule += ['-o %s -m limit --limit %s/s --limit-burst %s ' + '-j NFLOG --nflog-prefix %s' + % (device, FAKE_RATE, FAKE_BURST, tag)] + return v4_nflog_rule, v6_nflog_rule + + def _fake_ipt_mgr_list(self, router_ids): + f_ipt_mgrs = defaultdict(dict) + + for router_id in router_ids: + f_port_id1 = router_id + '_port1' + f_port_id2 = router_id + '_port2' + ipt_mgr = mock.Mock() + ipt_mgr.ns_name = 'ns_' + router_id + f_ipt_mgrs[router_id][f_port_id1] = ipt_mgr + f_ipt_mgrs[router_id][f_port_id2] = ipt_mgr + + return f_ipt_mgrs diff --git a/neutron_fwaas/tests/unit/services/logapi/agents/l3/__init__.py b/neutron_fwaas/tests/unit/services/logapi/agents/l3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/logapi/agents/l3/test_fwg_log.py b/neutron_fwaas/tests/unit/services/logapi/agents/l3/test_fwg_log.py new file mode 100644 index 000000000..6a110d939 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/agents/l3/test_fwg_log.py @@ -0,0 +1,51 @@ +# Copyright (c) 2018 Fujitsu Limited. +# 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. + +import mock + +from neutron.api.rpc.callbacks.consumer import registry +from neutron.api.rpc.callbacks import resources +from neutron.api.rpc.handlers import resources_rpc +from neutron.tests.unit.services.logapi.agent.l3 import test_base as base +from neutron_lib import constants as lib_const + +from neutron_fwaas.services.logapi.agents.l3 import fwg_log + + +class FWaaSL3LoggingExtensionInitializeTestCase(base.L3LoggingExtBaseTestCase): + + def setUp(self): + super(FWaaSL3LoggingExtensionInitializeTestCase, self).setUp() + self.fw_l3_log_ext = fwg_log.FWaaSL3LoggingExtension() + self.fw_l3_log_ext.consume_api(self.agent_api) + + @mock.patch.object(registry, 'register') + @mock.patch.object(resources_rpc, 'ResourcesPushRpcCallback') + def test_initialize_subscribed_to_rpc(self, rpc_mock, subscribe_mock): + call_to_patch = 'neutron_lib.rpc.Connection' + with mock.patch(call_to_patch, + return_value=self.connection) as create_connection: + self.fw_l3_log_ext.initialize( + self.connection, lib_const.L3_AGENT_MODE) + create_connection.assert_has_calls([mock.call()]) + self.connection.create_consumer.assert_has_calls( + [mock.call( + resources_rpc.resource_type_versioned_topic( + resources.LOGGING_RESOURCE), + [rpc_mock()], + fanout=True)] + ) + subscribe_mock.assert_called_with( + mock.ANY, resources.LOGGING_RESOURCE) diff --git a/neutron_fwaas/tests/unit/services/logapi/base.py b/neutron_fwaas/tests/unit/services/logapi/base.py new file mode 100644 index 000000000..585387b49 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/base.py @@ -0,0 +1,38 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from neutron.api.rpc.callbacks.consumer import registry as cons_registry +from neutron.api.rpc.callbacks.producer import registry as prod_registry +from neutron.api.rpc.callbacks import resource_manager +from neutron.tests.unit import testlib_api + + +class BaseLogTestCase(testlib_api.SqlTestCase): + def setUp(self): + super(BaseLogTestCase, self).setUp() + + with mock.patch.object( + resource_manager.ResourceCallbacksManager, '_singleton', + new_callable=mock.PropertyMock(return_value=False)): + + self.cons_mgr = resource_manager.ConsumerResourceCallbacksManager() + self.prod_mgr = resource_manager.ProducerResourceCallbacksManager() + for mgr in (self.cons_mgr, self.prod_mgr): + mgr.clear() + + mock.patch.object( + cons_registry, '_get_manager', return_value=self.cons_mgr).start() + + mock.patch.object( + prod_registry, '_get_manager', return_value=self.prod_mgr).start() diff --git a/neutron_fwaas/tests/unit/services/logapi/common/__init__.py b/neutron_fwaas/tests/unit/services/logapi/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/logapi/common/test_fwg_callback.py b/neutron_fwaas/tests/unit/services/logapi/common/test_fwg_callback.py new file mode 100644 index 000000000..a5c5866f3 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/common/test_fwg_callback.py @@ -0,0 +1,225 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +import mock +from neutron.objects import ports as port_objects +from neutron.services.logapi.drivers import base as log_driver_base +from neutron.services.logapi.drivers import manager as driver_mgr +from neutron.tests import base +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib import constants as nl_const + +from neutron_fwaas.common import fwaas_constants as fw_const +from neutron_fwaas.services.logapi.common import fwg_callback +from neutron_fwaas.services.logapi.common import log_db_api + +FAKE_DRIVER = None + + +class FakeDriver(log_driver_base.DriverBase): + + @staticmethod + def create(): + return FakeDriver( + name='fake_driver', + vif_types=[], + vnic_types=[], + supported_logging_types=['firewall_group'], + requires_rpc=True + ) + + +def fake_register(): + global FAKE_DRIVER + if not FAKE_DRIVER: + FAKE_DRIVER = FakeDriver.create() + driver_mgr.register(fw_const.FIREWALL_GROUP, + fwg_callback.FirewallGroupCallBack) + + +class TestFirewallGroupRuleCallback(base.BaseTestCase): + + def setUp(self): + super(TestFirewallGroupRuleCallback, self).setUp() + self.driver_manager = driver_mgr.LoggingServiceDriverManager() + self.fwg_callback = fwg_callback.FirewallGroupCallBack(mock.Mock(), + mock.Mock()) + self.m_context = mock.Mock() + + @mock.patch.object(fwg_callback.FirewallGroupCallBack, 'handle_event') + def test_handle_event(self, mock_fwg_cb): + fake_register() + self.driver_manager.register_driver(FAKE_DRIVER) + + registry.publish( + fw_const.FIREWALL_GROUP, events.AFTER_CREATE, mock.ANY) + mock_fwg_cb.assert_called_once_with( + fw_const.FIREWALL_GROUP, events.AFTER_CREATE, mock.ANY, + payload=None) + + mock_fwg_cb.reset_mock() + registry.publish( + fw_const.FIREWALL_GROUP, events.AFTER_UPDATE, mock.ANY) + mock_fwg_cb.assert_called_once_with( + fw_const.FIREWALL_GROUP, events.AFTER_UPDATE, mock.ANY, + payload=None) + + mock_fwg_cb.reset_mock() + registry.publish( + 'non_registered_resource', events.AFTER_CREATE, mock.ANY) + mock_fwg_cb.assert_not_called() + + mock_fwg_cb.reset_mock() + registry.publish( + 'non_registered_resource', events.AFTER_UPDATE, mock.ANY) + mock_fwg_cb.assert_not_called() + + def test_need_to_notify(self): + port_objects.Port.get_object = \ + mock.Mock(side_effect=self._get_object_side_effect) + + # Test with router devices + for device in nl_const.ROUTER_INTERFACE_OWNERS: + result = self.fwg_callback.need_to_notify(self.m_context, [device]) + self.assertEqual(True, result) + # Test with non-router device + result = self.fwg_callback.need_to_notify(self.m_context, + ['fake_port']) + self.assertEqual(False, result) + + # Test with ports_delta is empty + result = self.fwg_callback.need_to_notify(self.m_context, []) + self.assertEqual(False, result) + + def test_trigger_logging(self): + m_payload = mock.Mock() + self.fwg_callback.resource_push_api = mock.Mock() + m_payload.resource_id = 'fake_resource_id' + ports_delta = ['fake_port_id'] + + # Test with log resource could be found from DB + with mock.patch.object(log_db_api, 'get_logs_for_fwg', + return_value={'fake': 'fake'}): + self.fwg_callback.trigger_logging(self.m_context, + m_payload.resource_id, + ports_delta) + self.fwg_callback.resource_push_api.assert_called() + + # Test with log resource could not be found from DB + self.fwg_callback.resource_push_api.reset_mock() + with mock.patch.object(log_db_api, 'get_logs_for_fwg', + return_value={}): + self.fwg_callback.trigger_logging(self.m_context, + m_payload.resource_id, + ports_delta) + self.fwg_callback.resource_push_api.assert_not_called() + + def _get_object_side_effect(self, context, id): + fake_port = { + 'id': 'fake_id', + 'device_owner': id, + } + return fake_port + + def test_handle_event_with_router_port(self): + with mock.patch.object(self.fwg_callback, 'need_to_notify', + return_value=True): + with mock.patch.object(self.fwg_callback, 'trigger_logging'): + # Test for firewall group creation with router port + m_payload = self._mock_payload(events.AFTER_CREATE, + 'fake_port_id') + self.fwg_callback.handle_event(mock.ANY, + events.AFTER_CREATE, + mock.ANY, + **{'payload': m_payload}) + self.fwg_callback.trigger_logging.assert_called() + + # Test for firewall group update with router port + self.fwg_callback.trigger_logging.reset_mock() + m_payload = self._mock_payload(events.AFTER_UPDATE, + 'fake_port_id') + self.fwg_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + **{'payload': m_payload}) + self.fwg_callback.trigger_logging.assert_called() + + def test_handle_event_with_non_router_port(self): + with mock.patch.object(self.fwg_callback, 'need_to_notify', + return_value=False): + with mock.patch.object(self.fwg_callback, 'trigger_logging'): + + # Test for firewall group creation with non router ports + m_payload = self._mock_payload(events.AFTER_CREATE, + 'fake_port_id') + self.fwg_callback.handle_event(mock.ANY, + events.AFTER_CREATE, + mock.ANY, + **{'payload': m_payload}) + self.fwg_callback.trigger_logging.assert_not_called() + + # Test for firewall group creation without ports + self.fwg_callback.trigger_logging.reset_mock() + m_payload = self._mock_payload(events.AFTER_CREATE) + self.fwg_callback.handle_event(mock.ANY, + events.AFTER_CREATE, + mock.ANY, + **{'payload': m_payload}) + self.fwg_callback.trigger_logging.assert_not_called() + + # Test for firewall group update with non router ports + self.fwg_callback.trigger_logging.reset_mock() + m_payload = self._mock_payload(events.AFTER_UPDATE, + 'fake_port_id') + self.fwg_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + **{'payload': m_payload}) + self.fwg_callback.trigger_logging.assert_not_called() + + # Test for firewall group update without ports + self.fwg_callback.trigger_logging.reset_mock() + m_payload = self._mock_payload(events.AFTER_UPDATE) + self.fwg_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + **{'payload': m_payload}) + self.fwg_callback.trigger_logging.assert_not_called() + + def _mock_payload(self, event, ports_delta=None): + m_payload = mock.Mock() + m_payload.context = self.m_context + if event == events.AFTER_CREATE: + if ports_delta: + m_payload.latest_state = { + 'ports': [ports_delta] + } + else: + m_payload.latest_state = { + 'ports': [] + } + if event == events.AFTER_UPDATE: + if ports_delta: + m_payload.states = [ + {'ports': [ports_delta]}, + {'ports': []} + ] + else: + m_payload.states = [ + {'ports': []}, + {'ports': []} + ] + return m_payload diff --git a/neutron_fwaas/tests/unit/services/logapi/common/test_log_db_api.py b/neutron_fwaas/tests/unit/services/logapi/common/test_log_db_api.py new file mode 100644 index 000000000..33b193f9b --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/common/test_log_db_api.py @@ -0,0 +1,329 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +import mock +from neutron.objects.logapi import logging_resource as log_object +from neutron.objects import ports as port_objects +from neutron.services.logapi.rpc import server as server_rpc +from neutron.tests import base +from neutron_lib import constants as nl_const +from oslo_utils import uuidutils + +from neutron_fwaas.services.logapi.common import log_db_api +from neutron_fwaas.services.logapi.rpc import log_server as fwg_rpc + +FWG = 'firewall_group' + + +def _create_log_object(tenant_id, resource_id=None, + target_id=None, event='ALL'): + + log_data = { + 'id': uuidutils.generate_uuid(), + 'name': 'fake_log_name', + 'resource_type': FWG, + 'project_id': tenant_id, + 'event': event, + 'enabled': True} + if resource_id: + log_data['resource_id'] = resource_id + if target_id: + log_data['target_id'] = target_id + return log_object.Log(**log_data) + + +def _fake_log_info(id, project_id, ports_id, event='ALL'): + expected = { + 'id': id, + 'project_id': project_id, + 'ports_log': ports_id, + 'event': event + } + return expected + + +def _fake_port_object(port_id, device_owner, status, + project_id=uuidutils.generate_uuid()): + port_data = { + 'id': port_id, + 'device_owner': device_owner, + 'project_id': project_id + } + if status: + port_data['status'] = status + return port_data + + +class LoggingRpcCallbackTestCase(base.BaseTestCase): + + def setUp(self): + super(LoggingRpcCallbackTestCase, self).setUp() + self.context = mock.Mock() + self.rpc_callback = server_rpc.LoggingApiSkeleton() + + log_db_api.fw_plugin_db = mock.Mock() + + self.vm_port = uuidutils.generate_uuid() + self.router_port = uuidutils.generate_uuid() + self.fake_vm_port = \ + _fake_port_object(self.vm_port, + nl_const.DEVICE_OWNER_COMPUTE_PREFIX, + nl_const.PORT_STATUS_ACTIVE) + + self.fake_router_port = \ + _fake_port_object(self.router_port, + nl_const.DEVICE_OWNER_ROUTER_INTF, + nl_const.PORT_STATUS_ACTIVE) + self.fake_router_ports = \ + [_fake_port_object(self.router_port, device, + nl_const.PORT_STATUS_ACTIVE) + for device in nl_const.ROUTER_INTERFACE_OWNERS] + + def test_get_fwg_log_info_for_log_resources(self): + fwg_id = uuidutils.generate_uuid() + tenant_id = uuidutils.generate_uuid() + log_obj = _create_log_object(tenant_id, resource_id=fwg_id) + + rpc_call = fwg_rpc.get_fwg_log_info_for_log_resources + with mock.patch.object(server_rpc, 'get_rpc_method', + return_value=rpc_call): + fake_ports = ['fake_port_1', 'fake_port_2'] + with mock.patch.object(log_db_api, '_get_ports_being_logged', + return_value=fake_ports): + expected_log_info = [ + _fake_log_info(log_obj['id'], tenant_id, fake_ports) + ] + + logs_info = self.rpc_callback.\ + get_sg_log_info_for_log_resources(self.context, + resource_type=FWG, + log_resources=[log_obj]) + self.assertEqual(expected_log_info, logs_info) + + def test_get_fwg_log_info_for_port(self): + fwg_id = uuidutils.generate_uuid() + port_id = uuidutils.generate_uuid() + tenant_id = uuidutils.generate_uuid() + + log_obj = _create_log_object(tenant_id, resource_id=fwg_id, + target_id=port_id) + + rpc_call = fwg_rpc.get_fwg_log_info_for_port + with mock.patch.object(server_rpc, 'get_rpc_method', + return_value=rpc_call): + with mock.patch.object(log_db_api, 'get_logs_for_port', + return_value=[log_obj]): + fake_ports = [port_id, 'fake_port2'] + with mock.patch.object(log_db_api, '_get_ports_being_logged', + return_value=fake_ports): + expected_log_info = [_fake_log_info(log_obj['id'], + tenant_id, + fake_ports)] + logs_info = self.rpc_callback.\ + get_sg_log_info_for_port(self.context, + resource_type=FWG, + port_id=port_id) + self.assertEqual(expected_log_info, logs_info) + + def test_get_ports_being_logged_with_target_id(self): + tenant_id = uuidutils.generate_uuid() + fwg_id = uuidutils.generate_uuid() + + # Test with VM port + log_obj = _create_log_object(tenant_id, resource_id=fwg_id, + target_id=self.vm_port) + with mock.patch.object(port_objects.Port, 'get_object', + return_value=self.fake_vm_port): + logged_port_ids = \ + log_db_api._get_ports_being_logged(self.context, log_obj) + self.assertEqual([], logged_port_ids) + + # Test with router ports + log_obj = _create_log_object(tenant_id, resource_id=fwg_id, + target_id=self.router_port) + + log_db_api.fw_plugin_db. \ + get_fwg_attached_to_port = mock.Mock(return_value='fwg_id') + with mock.patch.object(port_objects.Port, 'get_object', + side_effect=self.fake_router_ports): + + for port in self.fake_router_ports: + logged_port_ids = \ + log_db_api._get_ports_being_logged(self.context, log_obj) + self.assertEqual([self.router_port], logged_port_ids) + + # Test with inactive router port + self.fake_router_port['status'] = nl_const.PORT_STATUS_DOWN + log_obj = _create_log_object(tenant_id, resource_id=fwg_id, + target_id=self.router_port) + + log_db_api.fw_plugin_db. \ + get_fwg_attached_to_port = mock.Mock(return_value='fwg_id') + with mock.patch.object(port_objects.Port, 'get_object', + return_value=self.fake_router_port): + logged_port_ids = \ + log_db_api._get_ports_being_logged(self.context, log_obj) + self.assertEqual([], logged_port_ids) + + def test_get_ports_being_logged_with_resource_id(self): + tenant_id = uuidutils.generate_uuid() + fwg_id = uuidutils.generate_uuid() + log_obj = _create_log_object(tenant_id, resource_id=fwg_id) + + log_db_api.fw_plugin_db.get_ports_in_firewall_group = \ + mock.Mock(return_value=[self.vm_port]) + # Test with VM port + with mock.patch.object(port_objects.Port, 'get_object', + return_value=self.fake_vm_port): + logged_port_ids = \ + log_db_api._get_ports_being_logged(self.context, log_obj) + self.assertEqual([], logged_port_ids) + + # Test with router ports + router_ports = [self.router_port, self.router_port, self.router_port] + log_db_api.fw_plugin_db. \ + get_ports_in_firewall_group = mock.Mock(return_value=router_ports) + log_db_api.fw_plugin_db. \ + get_fwg_attached_to_port = mock.Mock(return_value='fwg_id') + + with mock.patch.object(port_objects.Port, 'get_object', + side_effect=self.fake_router_ports): + logged_port_ids = \ + log_db_api._get_ports_being_logged(self.context, log_obj) + self.assertEqual(router_ports, logged_port_ids) + + # Test with both vm port and router ports + log_db_api.fw_plugin_db.get_ports_in_firewall_group = \ + mock.Mock(return_value=[self.vm_port, self.router_port]) + log_db_api.fw_plugin_db. \ + get_fwg_attached_to_port = mock.Mock(return_value='fwg_id') + + with mock.patch.object(port_objects.Port, 'get_object', + side_effect=[self.fake_vm_port, + self.fake_router_port]): + logged_port_ids = \ + log_db_api._get_ports_being_logged(self.context, log_obj) + self.assertEqual([self.router_port], logged_port_ids) + + # Test with inactive router port + log_db_api.fw_plugin_db.get_ports_in_firewall_group = \ + mock.Mock(return_value=[self.router_port]) + log_db_api.fw_plugin_db. \ + get_fwg_attached_to_port = mock.Mock(return_value='fwg_id') + + with mock.patch.object(port_objects.Port, 'get_object', + return_value=self.fake_router_port): + logged_port_ids = \ + log_db_api._get_ports_being_logged(self.context, log_obj) + self.assertEqual([self.router_port], logged_port_ids) + + def test_get_ports_being_logged_with_ports_in_tenant(self): + tenant_id = uuidutils.generate_uuid() + log_obj = _create_log_object(tenant_id) + + log_db_api.fw_plugin_db.get_fwg_ports_in_tenant = \ + mock.Mock(return_value=[self.router_port]) + log_db_api.fw_plugin_db. \ + get_fwg_attached_to_port = mock.Mock(return_value='fwg_id') + + with mock.patch.object(port_objects.Port, 'get_object', + return_value=self.fake_router_port): + log_db_api._get_ports_being_logged(self.context, log_obj) + log_db_api.fw_plugin_db.get_fwg_ports_in_tenant.\ + assert_called_with(self.context, tenant_id) + + def test_logs_for_port_with_vm_port(self): + with mock.patch.object(port_objects.Port, 'get_object', + return_value=self.fake_vm_port): + logs = log_db_api.get_logs_for_port(self.context, self.vm_port) + self.assertEqual([], logs) + + def test_logs_for_port_with_router_port(self): + tenant_id = uuidutils.generate_uuid() + resource_id = uuidutils.generate_uuid() + target_id = uuidutils.generate_uuid() + log_db_api.fw_plugin_db.get_fwg_attached_to_port = \ + mock.Mock(side_effect=[[], resource_id, resource_id]) + with mock.patch.object(port_objects.Port, 'get_object', + return_value=self.fake_router_port): + + # Test with router port that did not attach to fwg + logs = log_db_api.get_logs_for_port(self.context, self.router_port) + self.assertEqual([], logs) + + # Test with router port that attached to fwg + # Fake log objects that bounds a given port + log = _create_log_object(tenant_id) + resource_log = _create_log_object(tenant_id, resource_id) + target_log = _create_log_object(tenant_id, resource_id, target_id) + log_objs = [log, target_log, resource_log] + + with mock.patch.object(log_object.Log, 'get_objects', + return_value=log_objs): + self.fake_router_port = mock.Mock(return_value=target_id) + logs = log_db_api.get_logs_for_port(self.context, + self.router_port) + self.assertEqual(log_objs, logs) + + # Fake log objects that does not bound a given port + unbound_resource = uuidutils.generate_uuid() + resource_log = _create_log_object(tenant_id, unbound_resource) + target_log = _create_log_object(tenant_id, unbound_resource, + target_id) + log_objs = [log, target_log, resource_log] + + with mock.patch.object(log_object.Log, 'get_objects', + return_value=log_objs): + self.fake_router_port = mock.Mock(return_value=target_id) + logs = log_db_api.get_logs_for_port(self.context, + self.router_port) + self.assertEqual([log], logs) + + def test_logs_for_fwg(self): + tenant_id = uuidutils.generate_uuid() + resource_id = uuidutils.generate_uuid() + target_id = uuidutils.generate_uuid() + + # Fake log objects that bounds a given fwg + log = _create_log_object(tenant_id) + resource_log = _create_log_object(tenant_id, resource_id) + target_log = _create_log_object(tenant_id, target_id=target_id) + ports_delta = [target_id] + + # Test with port that in ports_delta + log_db_api.fw_plugin_db.get_fwg_attached_to_port = \ + mock.Mock(return_value=None) + with mock.patch.object(log_object.Log, 'get_objects', + return_value=[target_log]): + logs = log_db_api.get_logs_for_fwg(self.context, + resource_id, + ports_delta) + self.assertEqual([target_log], logs) + + # Test with log that bound to a give fwg + with mock.patch.object(log_object.Log, 'get_objects', + return_value=[resource_log]): + logs = log_db_api.get_logs_for_fwg(self.context, + resource_id, + ports_delta) + self.assertEqual([resource_log], logs) + + # Test with log that does not bound to any fwg or port + with mock.patch.object(log_object.Log, 'get_objects', + return_value=[log]): + logs = log_db_api.get_logs_for_fwg(self.context, + resource_id, + ports_delta) + self.assertEqual([log], logs) diff --git a/neutron_fwaas/tests/unit/services/logapi/common/test_port_callback.py b/neutron_fwaas/tests/unit/services/logapi/common/test_port_callback.py new file mode 100644 index 000000000..5ee728177 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/common/test_port_callback.py @@ -0,0 +1,198 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +import mock +from neutron.objects import ports as port_objects +from neutron.services.logapi.drivers import base as log_driver_base +from neutron.services.logapi.drivers import manager as driver_mgr +from neutron.tests import base +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib import constants as nl_const +from oslo_utils import uuidutils + +from neutron_fwaas.services.logapi.common import log_db_api +from neutron_fwaas.services.logapi.common import port_callback + +FAKE_DRIVER = None + + +class FakeDriver(log_driver_base.DriverBase): + + @staticmethod + def create(): + return FakeDriver( + name='fake_driver', + vif_types=[], + vnic_types=[], + supported_logging_types=['firewall_group'], + requires_rpc=True + ) + + +def fake_register(): + global FAKE_DRIVER + if not FAKE_DRIVER: + FAKE_DRIVER = FakeDriver.create() + driver_mgr.register(resources.PORT, port_callback.NeutronPortCallBack) + + +class TestFirewallGroupRuleCallback(base.BaseTestCase): + + def setUp(self): + super(TestFirewallGroupRuleCallback, self).setUp() + self.driver_manager = driver_mgr.LoggingServiceDriverManager() + self.port_callback = port_callback.NeutronPortCallBack(mock.Mock(), + mock.Mock()) + self.m_context = mock.Mock() + + def _create_port_object(self, name=None, device_owner=None, + status=nl_const.PORT_STATUS_ACTIVE): + port_data = { + 'id': uuidutils.generate_uuid(), + 'project_id': 'fake_tenant_id', + 'status': status + } + if name: + port_data['name'] = name + if device_owner: + port_data['device_owner'] = device_owner + return port_objects.Port(**port_data) + + @mock.patch.object(port_callback.NeutronPortCallBack, 'handle_event') + def test_handle_event(self, m_port_cb_handler): + fake_register() + self.driver_manager.register_driver(FAKE_DRIVER) + payload = events.DBEventPayload(None) + registry.publish(resources.PORT, events.AFTER_CREATE, mock.ANY, + payload) + m_port_cb_handler.assert_called_once_with( + resources.PORT, events.AFTER_CREATE, mock.ANY, payload=payload) + + m_port_cb_handler.reset_mock() + registry.publish( + resources.PORT, events.AFTER_UPDATE, mock.ANY, payload) + m_port_cb_handler.assert_called_once_with( + resources.PORT, events.AFTER_UPDATE, mock.ANY, payload=payload) + + m_port_cb_handler.reset_mock() + registry.publish( + 'non_registered_resource', events.AFTER_CREATE, mock.ANY) + m_port_cb_handler.assert_not_called() + + m_port_cb_handler.reset_mock() + registry.publish( + 'non_registered_resource', events.AFTER_UPDATE, mock.ANY) + m_port_cb_handler.assert_not_called() + + def test_trigger_logging(self): + fake_log_obj = mock.Mock() + self.port_callback.resource_push_api = mock.Mock() + port = self._create_port_object(device_owner='fake_device_owner') + + # Test with log resource could be found from DB + with mock.patch.object(log_db_api, 'get_logs_for_port', + return_value=[fake_log_obj]): + self.port_callback.trigger_logging(self.m_context, port) + self.port_callback.resource_push_api.assert_called() + + # Test with log resource could not be found from DB + self.port_callback.resource_push_api.reset_mock() + with mock.patch.object(log_db_api, 'get_logs_for_port', + return_value=[]): + self.port_callback.trigger_logging(self.m_context, port) + self.port_callback.resource_push_api.assert_not_called() + + def test_handle_event_with_router_port(self): + with mock.patch.object(self.port_callback, 'trigger_logging'): + # Test for router port enabling + payload = self._fake_port_config( + nl_const.DEVICE_OWNER_ROUTER_INTF, action='enable') + self.port_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + payload=payload) + self.port_callback.trigger_logging.assert_called() + + # Test for router port disabling + self.port_callback.trigger_logging.reset_mock() + payload = self._fake_port_config( + nl_const.DEVICE_OWNER_ROUTER_INTF, action='disable') + self.port_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + payload=payload) + self.port_callback.trigger_logging.assert_called() + + # Test for router port status does not change + self.port_callback.trigger_logging.reset_mock() + payload = \ + self._fake_port_config(nl_const.DEVICE_OWNER_ROUTER_INTF) + self.port_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + payload=payload) + self.port_callback.trigger_logging.assert_not_called() + + def test_handle_event_with_non_router_port(self): + with mock.patch.object(self.port_callback, 'trigger_logging'): + # Test for port enabling + payload = self._fake_port_config('fake_port_type', + action='enable') + self.port_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + payload=payload) + self.port_callback.trigger_logging.assert_not_called() + + # Test for port disabling + self.port_callback.trigger_logging.reset_mock() + payload = self._fake_port_config('fake_port_type', + action='disable') + self.port_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + payload=payload) + self.port_callback.trigger_logging.assert_not_called() + + def _fake_port_config(self, device_owner, action=None): + if action == 'enable': + # Create original port with DOWN status + original_port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_DOWN) + + # Create port with ACTIVE status + port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE) + elif action == 'disable': + # Create original port with ACTIVE status + original_port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE) + + # Create port with DOWN status + port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_DOWN) + else: + # Create original port with ACTIVE status + original_port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE) + + # Create port with ACTIVE status + port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE) + payload = events.DBEventPayload(self.m_context, + states=[original_port, port]) + return payload diff --git a/neutron_fwaas/tests/unit/services/logapi/rpc/__init__.py b/neutron_fwaas/tests/unit/services/logapi/rpc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/logapi/rpc/test_log_server.py b/neutron_fwaas/tests/unit/services/logapi/rpc/test_log_server.py new file mode 100644 index 000000000..092a148c6 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/rpc/test_log_server.py @@ -0,0 +1,56 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +import mock + +from neutron.services.logapi.rpc import server as server_rpc +from neutron.tests import base + +from neutron_fwaas.services.logapi.rpc import log_server as fw_server_rpc + + +class FWGLoggingApiSkeletonTestCase(base.BaseTestCase): + @mock.patch("neutron_fwaas.services.logapi.common.log_db_api." + "get_fwg_log_info_for_port") + def test_get_fwg_log_info_for_port(self, mock_callback): + with mock.patch.object( + server_rpc, + 'get_rpc_method', + return_value=fw_server_rpc.get_fwg_log_info_for_port + ): + test_obj = server_rpc.LoggingApiSkeleton() + m_context = mock.Mock() + port_id = '123' + test_obj.get_sg_log_info_for_port(m_context, + resource_type='firewall_v2', + port_id=port_id) + mock_callback.assert_called_with(m_context, port_id) + + @mock.patch("neutron_fwaas.services.logapi.common.log_db_api." + "get_fwg_log_info_for_log_resources") + def test_get_fwg_log_info_for_log_resources(self, mock_callback): + with mock.patch.object( + server_rpc, + 'get_rpc_method', + return_value=fw_server_rpc.get_fwg_log_info_for_log_resources + ): + test_obj = server_rpc.LoggingApiSkeleton() + m_context = mock.Mock() + log_resources = [mock.Mock()] + test_obj.get_sg_log_info_for_log_resources( + m_context, + resource_type='firewall_v2', + log_resources=log_resources) + mock_callback.assert_called_with(m_context, log_resources) diff --git a/neutron_fwaas/tests/unit/services/logapi/test_fwg_validate.py b/neutron_fwaas/tests/unit/services/logapi/test_fwg_validate.py new file mode 100644 index 000000000..2ee9876fa --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/test_fwg_validate.py @@ -0,0 +1,157 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + +import mock +from neutron.objects import ports +from neutron.services.logapi.common import exceptions as log_exc +from neutron.services.logapi.common import validators +from neutron.tests import base +from neutron_lib import constants as nl_const +from sqlalchemy.orm import exc as orm_exc + +from neutron_fwaas.services.logapi import exceptions as fwg_log_exc +from neutron_fwaas.services.logapi import fwg_validate + + +class TestFWGLogRequestValidations(base.BaseTestCase): + """Test validator for a log creation request""" + + def setUp(self): + super(TestFWGLogRequestValidations, self).setUp() + fwg_validate.fwg_plugin = mock.Mock() + fwg_validate.fwg_plugin.driver = mock.Mock() + fwg_validate.fwg_plugin.driver.firewall_db = mock.Mock() + + def test_validate_fwg_request(self): + m_context = mock.Mock() + fake_data = { + 'resource_type': 'firewall_group', + 'resource_id': 'fake_fwg_id' + } + with mock.patch.object(fwg_validate, '_check_fwg'): + fwg_validate.validate_firewall_group_request(m_context, fake_data) + fwg_validate._check_fwg.\ + assert_called_with(m_context, fake_data['resource_id']) + fake_data = { + 'resource_type': 'firewall_group', + 'resource_id': 'fake_fwg_id', + 'target_id': 'fake_port_id' + } + with mock.patch.object(fwg_validate, + '_check_target_resource_bound_fwg'): + with mock.patch.object(fwg_validate, '_check_fwg'): + with mock.patch.object(fwg_validate, '_check_fwg_port'): + fwg_validate.validate_firewall_group_request(m_context, + fake_data) + fwg_validate._check_target_resource_bound_fwg.\ + assert_called_with(m_context, + fake_data['resource_id'], + fake_data['target_id']) + fwg_validate._check_fwg. \ + assert_called_with(m_context, + fake_data['resource_id']) + fwg_validate._check_fwg_port. \ + assert_called_with(m_context, + fake_data['target_id']) + + def test_validate_request_fwg_id_not_exists(self): + + with mock.patch.object(fwg_validate.fwg_plugin, 'get_firewall_group', + side_effect=orm_exc.NoResultFound): + self.assertRaises( + log_exc.ResourceNotFound, + fwg_validate._check_fwg, + mock.ANY, + 'fake_fwg_id') + + def test_validate_request_fwg_not_active(self): + fake_fwg = {'id': '1234', 'status': 'PENDING'} + with mock.patch.object(fwg_validate.fwg_plugin, 'get_firewall_group', + return_value=fake_fwg): + self.assertRaises( + fwg_log_exc.FWGIsNotReadyForLogging, + fwg_validate._check_fwg, + mock.ANY, + 'fake_fwg_id') + + def test_validate_request_router_or_port_id_not_exists(self): + with mock.patch.object(ports.Port, 'get_object', return_value=None): + self.assertRaises( + log_exc.TargetResourceNotFound, + fwg_validate._check_fwg_port, + mock.ANY, + 'fake_port_id') + + def test_validate_request_unsupported_fwg_log_on_vm_port(self): + + fake_port = {'device_owner': "compute:"} + with mock.patch.object(ports.Port, 'get_object', + return_value=fake_port): + with mock.patch.object(validators, 'validate_log_type_for_port', + return_value=False): + self.assertRaises( + log_exc.LoggingTypeNotSupported, + fwg_validate._check_fwg_port, + mock.ANY, + 'fake_port_id') + + def test_validate_request_router_port_is_not_active(self): + + non_active_status = [nl_const.PORT_STATUS_DOWN, + nl_const.PORT_STATUS_ERROR, + nl_const.PORT_STATUS_NOTAPPLICABLE, + nl_const.PORT_STATUS_BUILD] + fake_port = [{'device_owner': nl_const.DEVICE_OWNER_ROUTER_INTF, + 'status': status} + for status in non_active_status] + with mock.patch.object(ports.Port, 'get_object', + side_effect=fake_port): + for status in non_active_status: + self.assertRaises( + fwg_log_exc.PortIsNotReadyForLogging, + fwg_validate._check_fwg_port, + mock.ANY, + 'fake_port_id') + + def test_validate_request_router_port_was_not_associated_fwg(self): + + fake_port = {'device_owner': nl_const.DEVICE_OWNER_ROUTER_INTF, + 'status': nl_const.PORT_STATUS_ACTIVE} + + with mock.patch.object(ports.Port, 'get_object', + return_value=fake_port): + with mock.patch.object(fwg_validate.fwg_plugin.driver.firewall_db, + 'get_fwg_attached_to_port', + return_value=None): + self.assertRaises( + fwg_log_exc.TargetResourceNotAssociated, + fwg_validate._check_fwg_port, + mock.ANY, + 'fake_port_id') + + def test_validate_request_target_resource_not_bound_fwg(self): + + fake_ports_in_fwg = ['fake_port_id1, fake_port_id2'] + with mock.patch.object( + fwg_validate.fwg_plugin.driver.firewall_db, + 'get_ports_in_firewall_group', + return_value=fake_ports_in_fwg): + + self.assertRaises( + log_exc.InvalidResourceConstraint, + fwg_validate._check_target_resource_bound_fwg, + mock.ANY, + mock.ANY, + 'fake_target_id') diff --git a/neutron_fwaas/version.py b/neutron_fwaas/version.py new file mode 100644 index 000000000..6d5e0bf79 --- /dev/null +++ b/neutron_fwaas/version.py @@ -0,0 +1,17 @@ +# Copyright 2011 OpenStack Foundation +# +# 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') diff --git a/playbooks/configure_functional_job.yaml b/playbooks/configure_functional_job.yaml new file mode 100644 index 000000000..01dccba38 --- /dev/null +++ b/playbooks/configure_functional_job.yaml @@ -0,0 +1,4 @@ +- hosts: all + roles: + - setup_logdir + - configure_functional_tests diff --git a/releasenotes/notes/.placeholder b/releasenotes/notes/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/notes/adding-new-tables-for-future-consumption-ffd537c1f82e2e01.yaml b/releasenotes/notes/adding-new-tables-for-future-consumption-ffd537c1f82e2e01.yaml new file mode 100644 index 000000000..455f623b7 --- /dev/null +++ b/releasenotes/notes/adding-new-tables-for-future-consumption-ffd537c1f82e2e01.yaml @@ -0,0 +1,13 @@ +--- +prelude: > + Adding new tables for future consumption. +features: + - | + New tables ``ACCEPTED_EGRESS_TRAFFIC_TABLE=91`` + and ``ACCEPTED_INGRESS_TRAFFIC_TABLE=92`` & ``DROPPED_TRAFFIC_TABLE=93`` + are added to OVS based FWaaS L2 driver for future comsumption like logging + service. +fixes: + - | + The limitation related to logging for security group in case of + co-existence between SG and FWG is also fixed. \ No newline at end of file diff --git a/releasenotes/notes/auto-association-default-firewall-group-7e9faf1afca1df85.yaml b/releasenotes/notes/auto-association-default-firewall-group-7e9faf1afca1df85.yaml new file mode 100644 index 000000000..046ffcc4c --- /dev/null +++ b/releasenotes/notes/auto-association-default-firewall-group-7e9faf1afca1df85.yaml @@ -0,0 +1,14 @@ +--- +prelude: > + Associating default firewall group for new VM ports within a project + automatically. +features: + - | + The default firewall group won't be applied to all new VM ports as default. + However, if option ``auto_associate_default_firewall_group`` is enabled in + neutron_fwaas.conf like: + + [fwaas] + auto_associate_default_firewall_group = True + + Then, the default firewall group will be applied to all new VM ports. diff --git a/releasenotes/notes/bug-1702242-c917c832ac2fa4e1.yaml b/releasenotes/notes/bug-1702242-c917c832ac2fa4e1.yaml new file mode 100644 index 000000000..099a0cad6 --- /dev/null +++ b/releasenotes/notes/bug-1702242-c917c832ac2fa4e1.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + [`bug 1702242 `__] + Port range specification of a firewall rule now works expectedly + with the reference L3 agent based implementation. + Previously, when creating a firewall rule with port range like + ``8778:9000``, the rule was not deleted correctly and only entries + associated with the first port number were clean up. + Note that this bug is only applied to the reference L3 agent + based implementation. diff --git a/releasenotes/notes/bug-1746404-493a66faac333403.yaml b/releasenotes/notes/bug-1746404-493a66faac333403.yaml new file mode 100644 index 000000000..959d0eb88 --- /dev/null +++ b/releasenotes/notes/bug-1746404-493a66faac333403.yaml @@ -0,0 +1,10 @@ +--- +prelude: > + Taking security for VM instance into consideration, we've removed an option + to disable automatic association with default firewall group feature. + Therefore, `auto_associate_default_firewall_group` has been removed. +fixes: + - | + There is no validation to check if an updated port is for VM or not so far. + After this fix, default firewall group association is called only for + VM ports which are newly created. diff --git a/releasenotes/notes/bug-1799358-360c6ab27a32e0ac.yaml b/releasenotes/notes/bug-1799358-360c6ab27a32e0ac.yaml new file mode 100644 index 000000000..05a8774c8 --- /dev/null +++ b/releasenotes/notes/bug-1799358-360c6ab27a32e0ac.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + There was no way to define default firewall group rules. + Default firewall group rules can be now defined in neutron_fwaas.conf + in section ``default_fwg_rules``. + Default firewall group rules are same as hardcoded values before. diff --git a/releasenotes/notes/cisco-fwaas-driver-move-8f46325d13c93543.yaml b/releasenotes/notes/cisco-fwaas-driver-move-8f46325d13c93543.yaml new file mode 100644 index 000000000..ea175b27a --- /dev/null +++ b/releasenotes/notes/cisco-fwaas-driver-move-8f46325d13c93543.yaml @@ -0,0 +1,11 @@ +--- +prelude: > + The Cisco Firewall Driver is being moved from the + FWaaS repo to the Cisco specific repo: + https://github.com/openstack/networking-cisco +upgrade: + - The Cisco FWaaS driver will not be available from + the neutron-fwaas repo in Newton. For the Cisco + FWaaS driver, refer to the openstack/networking-cisco + repo. + diff --git a/releasenotes/notes/coexistence-between-sg-and-fwg-1f77a755539a9463.yaml b/releasenotes/notes/coexistence-between-sg-and-fwg-1f77a755539a9463.yaml new file mode 100644 index 000000000..aa695dcda --- /dev/null +++ b/releasenotes/notes/coexistence-between-sg-and-fwg-1f77a755539a9463.yaml @@ -0,0 +1,16 @@ +--- +prelude: > + Coexistence between security group and firewall group. +features: + - L2 firewall group driver based OVS can work in coexistence mode. + That means, if a port is associated with both firewall group and + security group, then a packet must be allowed by both features. +other: + - If a port is associated with both firewall group & security group and + there is a security group logging, which is enabled to collect ``DROP`` + events for this port, then most of invalid packets will be dropped at + firewall group for performance reason except first dropped packet, which + is allowed by firewall group but not accepted by security group. So not + every dropped packet will be logged (like in case of security group + works in standalone mode). + diff --git a/releasenotes/notes/config-file-generation-265c5256668a26bf.yaml b/releasenotes/notes/config-file-generation-265c5256668a26bf.yaml new file mode 100644 index 000000000..bb8749ce5 --- /dev/null +++ b/releasenotes/notes/config-file-generation-265c5256668a26bf.yaml @@ -0,0 +1,7 @@ +--- +prelude: > + Generation of sample Neutron FWaaS configuration files. +features: + - Neutron FWaaS no longer includes static example configuration files. + Instead, use tools/generate_config_file_samples.sh to generate them. + The files are generated with a .sample extension. diff --git a/releasenotes/notes/deprecate-neutron-fwaas-as-stadium-project-934d6acb3e824249.yaml b/releasenotes/notes/deprecate-neutron-fwaas-as-stadium-project-934d6acb3e824249.yaml new file mode 100644 index 000000000..f2c7125a1 --- /dev/null +++ b/releasenotes/notes/deprecate-neutron-fwaas-as-stadium-project-934d6acb3e824249.yaml @@ -0,0 +1,14 @@ +--- +prelude: > + Neutron-fwaas project is now deprecated in the Neutron stadium. +deprecations: + - | + Due to lack of maintainers neutron-fwaas project is now deprecated in the + Neutron stadium. There is no planned releases of this project in the + ``Victoria`` cycle. + In ``W`` cycle project will be moved out from the stadium to the unofficial + OpenStack projects. + If You want to step in and be maintainer of this project to keep it in the + Neutron stadium, please contact the ``neutron team`` via + openstack-discuss@lists.openstack.org or IRC channel #openstack-neutron + @freenode. diff --git a/releasenotes/notes/drop-python-2-7-73d3113c69d724c1.yaml b/releasenotes/notes/drop-python-2-7-73d3113c69d724c1.yaml new file mode 100644 index 000000000..cbffe4f47 --- /dev/null +++ b/releasenotes/notes/drop-python-2-7-73d3113c69d724c1.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Python 2.7 support has been dropped. The minimum version of Python now + supported by neutron-fwaas is Python 3.6. diff --git a/releasenotes/notes/enable-quotas-a3d0a21743bb1985.yaml b/releasenotes/notes/enable-quotas-a3d0a21743bb1985.yaml new file mode 100644 index 000000000..216f1ef0f --- /dev/null +++ b/releasenotes/notes/enable-quotas-a3d0a21743bb1985.yaml @@ -0,0 +1,20 @@ +--- +prelude: > + Enable quotas for FWaaS. +features: + - The FWaaS extension will register quotas. + The default values for quota_firewall and + quota_firewall_policy are set to 10. + The default value for quota_firewall_rule + is set to 100. + Quotas can be adjusted in the conf files, including + -1 values to allow unlimited. +issues: + - Tenants may receive a 409 Conflict error with a + message body containing a quota exceeded message + during resource creation if their quota is exceeded. +other: + - Operators that increase the default limit for quota_routers + from 10 may want to bump FWaaS quotas as well, since with + router insertion a tenant can potentially have a unique + policy and firewall for each router. diff --git a/releasenotes/notes/fwaas-config-9c780ccfb0e7887f.yaml b/releasenotes/notes/fwaas-config-9c780ccfb0e7887f.yaml new file mode 100644 index 000000000..9bba1bed3 --- /dev/null +++ b/releasenotes/notes/fwaas-config-9c780ccfb0e7887f.yaml @@ -0,0 +1,4 @@ +--- +features: + - Neutron Firewall as a Service can be configured by the users + with the newly introduced fwaas configuration file. diff --git a/releasenotes/notes/fwaas-v2-logging-79cbaa43ff17f47f.yaml b/releasenotes/notes/fwaas-v2-logging-79cbaa43ff17f47f.yaml new file mode 100644 index 000000000..00903d5db --- /dev/null +++ b/releasenotes/notes/fwaas-v2-logging-79cbaa43ff17f47f.yaml @@ -0,0 +1,22 @@ +--- +prelude: > + Resource type **firewall group** has been supported for neutron packet + logging framework. You can specify firewall group as ``--resource-type`` + for logging API. +features: + - | + Enable to collect network packet log for ACCEPT/DROP action from firewall + groups. Currently, packet logging supports only L3(router) ports. +issues: + - | + [`bug 1720727 `__] + Currently, we cannot specify the following combination on CLI due to + missing validation of --resource-type: + + - --resource-type firewall_group --resource + - --resource-type firewall_group --resource --target + + Therefore, you can only run with following combinations: + + - --resource-type firewall_group --target + - --resource-type firewall_group diff --git a/releasenotes/notes/fwaas_v2-374471c215af0ca0.yaml b/releasenotes/notes/fwaas_v2-374471c215af0ca0.yaml new file mode 100644 index 000000000..577d1a2d7 --- /dev/null +++ b/releasenotes/notes/fwaas_v2-374471c215af0ca0.yaml @@ -0,0 +1,18 @@ +--- +prelude: > + The FWaaS team is pleased to release FWaaS v2.0. This release of FWaaS + supports either the original FWaaS v1 or the new FWaaS v2. +features: + - In FWaaS v2 firewall policies are applied to router ports, as opposed to + applying to routers in FWaaS v1. + - Earlier the FWaaS agent integrated with the L3 agent by having the L3 Agent + class inherit from the FWaaS Agent class. This meant that other service + agents could not also integrate with the L3 agent. Now, using the L3 agent + extensions mechanism, FWaaS (v1 and v2) plugs in to the L3 agent. This + means that it can interoperate peacefully with other L3 advanced services + that also implement the L3 agent extension mechanism, all without any code + changes to Neutron. +upgrade: + - There is not currently a defined upgrade path from FWaaS v1 to FWaaS v2. + - FWaaS v1 can not be enabled at the same time as FWaaS v2; one or the other + must be chosen. diff --git a/releasenotes/notes/mcafee-fwaas-driver-removal-8915271e5d4288cf.yaml b/releasenotes/notes/mcafee-fwaas-driver-removal-8915271e5d4288cf.yaml new file mode 100644 index 000000000..d8239fc4f --- /dev/null +++ b/releasenotes/notes/mcafee-fwaas-driver-removal-8915271e5d4288cf.yaml @@ -0,0 +1,7 @@ +--- +prelude: > + - The McAfee Firewall Driver is being removed from the FwaaS repo, + due to lack of active maintainers. +upgrade: + - The McAfee Firewall Driver will not be available for use in the + Newton release. diff --git a/releasenotes/notes/ovs-firewall-driver-c347ea0a560b7e38.yaml b/releasenotes/notes/ovs-firewall-driver-c347ea0a560b7e38.yaml new file mode 100644 index 000000000..ba6c5162f --- /dev/null +++ b/releasenotes/notes/ovs-firewall-driver-c347ea0a560b7e38.yaml @@ -0,0 +1,16 @@ +--- +issues: + - | + Currently, the FWaaSv2 L2 driver can be configured as: + + ``firewall_driver = ovs`` + + And the Security Group driver is specified as: + + ``firewall_driver = openvswitch`` + + If both are configured, the packet will still only hit the FWaaS table in + OVS and will not traverse the rules in the SG table. There are some fixes + needed to support this model which are being tested and will be merged + shortly. Currently there are no checks to allow only one of FWaaS L2 or SG + to be configured. diff --git a/releasenotes/notes/remove_fwaas_v1-15c6e19484f46d1b.yaml b/releasenotes/notes/remove_fwaas_v1-15c6e19484f46d1b.yaml new file mode 100644 index 000000000..3412ede54 --- /dev/null +++ b/releasenotes/notes/remove_fwaas_v1-15c6e19484f46d1b.yaml @@ -0,0 +1,8 @@ +--- +prelude: > + - FWaaS V1 is being removed from the neutron-fwaas repo. Because FWaaS V2 + has been available since the Newton release. +upgrade: + - The FWaaS V1 source code will not be available in neutron-fwaas repo from + Stein. + neutron-fwaas-migrate-v1-to-v2 can be used for migrating V1 object to V2 model. diff --git a/releasenotes/notes/validation_if_port_is_supported-639d0df705eb67f9.yaml b/releasenotes/notes/validation_if_port_is_supported-639d0df705eb67f9.yaml new file mode 100644 index 000000000..4a24db60a --- /dev/null +++ b/releasenotes/notes/validation_if_port_is_supported-639d0df705eb67f9.yaml @@ -0,0 +1,8 @@ +--- +prelude: > + Validating if a port is supported by FWaaS V2 +fixes: + - | + [`bug 1746855 `__] + Now, FWaaS V2 will validate if a port is supported before adding it + to a FWG. This helps to make sure FWaaS V2 API works as expected. \ No newline at end of file diff --git a/releasenotes/notes/varmour-fwaas-driver-removal-f7aa304a4544134a.yaml b/releasenotes/notes/varmour-fwaas-driver-removal-f7aa304a4544134a.yaml new file mode 100644 index 000000000..0c90e0339 --- /dev/null +++ b/releasenotes/notes/varmour-fwaas-driver-removal-f7aa304a4544134a.yaml @@ -0,0 +1,7 @@ +--- +prelude: > + - The vArmour Firewall Driver is being removed from the FwaaS repo, + as per decision to remove vendor drivers from the community repo. +upgrade: + - The vArmour Firewall Driver will not be available for use in the + Newton release. diff --git a/releasenotes/notes/vyatta-fwaas-driver-removal-e38e6ecde5105084.yaml b/releasenotes/notes/vyatta-fwaas-driver-removal-e38e6ecde5105084.yaml new file mode 100644 index 000000000..ef2fb710d --- /dev/null +++ b/releasenotes/notes/vyatta-fwaas-driver-removal-e38e6ecde5105084.yaml @@ -0,0 +1,7 @@ +--- +prelude: > + - The vyatta Firewall Driver is being removed from the FwaaS repo, + as per decision to remove vendor drivers from the community repo. +upgrade: + - The vyatta Firewall Driver will not be available for use in the + Newton release from the community repo. diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 000000000..58b1e388c --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,279 @@ +# -*- 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. + +# Neutron FWaaS 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 = [ + 'openstackdocstheme', + '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 Release Notes' +copyright = u'2015, Neutron FWaaS Developers' + +# Release notes are version independent. +# The full version, including alpha/beta/rc tags. +release = '' +# The short X.Y version. +version = '' + +# 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 = 'openstackdocs' + +# 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 = ['_static'] + +# 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' +html_last_updated_fmt = '%Y-%m-%d %H:%M' + +# 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 = 'NeutronFWaaSReleaseNotesdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'NeutronFWaaSReleaseNotes.tex', + u'Neutron FWaaS Release Notes Documentation', + u'Neutron FWaaS Developers', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'neutronfwaasreleasenotes', u'Neutron FWaaS Release Notes ' + 'Documentation', [u'Neutron FWaaS Developers'], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'NeutronFWaaSReleaseNotes', u'Neutron FWaaS Release Notes ' + 'Documentation', + u'Neutron FWaaS Developers', 'NeutronFWaaSReleaseNotes', + 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + +# -- Options for Internationalization output ------------------------------ +locale_dirs = ['locale/'] + +# -- Options for openstackdocstheme ------------------------------------------- +repository_name = 'openstack/neutron-fwaas' +bug_project = 'neutron' +bug_tag = 'doc' diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 000000000..890a78928 --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,16 @@ +============================= + Neutron FWaaS Release Notes +============================= + +.. toctree:: + :maxdepth: 1 + + unreleased + stein + rocky + queens + pike + ocata + newton + mitaka + liberty diff --git a/releasenotes/source/liberty.rst b/releasenotes/source/liberty.rst new file mode 100644 index 000000000..36217be84 --- /dev/null +++ b/releasenotes/source/liberty.rst @@ -0,0 +1,6 @@ +============================== + Liberty Series Release Notes +============================== + +.. release-notes:: + :branch: origin/stable/liberty diff --git a/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po new file mode 100644 index 000000000..5b219d9f0 --- /dev/null +++ b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po @@ -0,0 +1,473 @@ +# Andi Chandler , 2017. #zanata +# Andi Chandler , 2018. #zanata +# Andi Chandler , 2020. #zanata +msgid "" +msgstr "" +"Project-Id-Version: neutron-fwaas\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-01-24 00:17+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2020-04-16 12:40+0000\n" +"Last-Translator: Andi Chandler \n" +"Language-Team: English (United Kingdom)\n" +"Language: en_GB\n" +"X-Generator: Zanata 4.3.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "--resource-type firewall_group" +msgstr "--resource-type firewall_group" + +msgid "--resource-type firewall_group --resource " +msgstr "--resource-type firewall_group --resource " + +msgid "" +"--resource-type firewall_group --resource --target " +"" +msgstr "" +"--resource-type firewall_group --resource --target " +"" + +msgid "--resource-type firewall_group --target " +msgstr "--resource-type firewall_group --target " + +msgid "11.0.0" +msgstr "11.0.0" + +msgid "12.0.0" +msgstr "12.0.0" + +msgid "13.0.0" +msgstr "13.0.0" + +msgid "14.0.0" +msgstr "14.0.0" + +msgid "15.0.0-9" +msgstr "15.0.0-9" + +msgid "7.0.2" +msgstr "7.0.2" + +msgid "7.1.1" +msgstr "7.1.1" + +msgid "8.0.0" +msgstr "8.0.0" + +msgid "9.0.0" +msgstr "9.0.0" + +msgid "9.0.0.0b2" +msgstr "9.0.0.0b2" + +msgid "9.0.0.0b3" +msgstr "9.0.0.0b3" + +msgid "9.0.0.0rc1" +msgstr "9.0.0.0rc1" + +msgid "Adding new tables for future consumption." +msgstr "Adding new tables for future consumption." + +msgid "And the Security Group driver is specified as:" +msgstr "And the Security Group driver is specified as:" + +msgid "" +"Associating default firewall group for new VM ports within a project " +"automatically." +msgstr "" +"Associating default firewall group for new VM ports within a project " +"automatically." + +msgid "Bug Fixes" +msgstr "Bug Fixes" + +msgid "Coexistence between security group and firewall group." +msgstr "Coexistence between security group and firewall group." + +msgid "Current Series Release Notes" +msgstr "Current Series Release Notes" + +msgid "Currently, the FWaaSv2 L2 driver can be configured as:" +msgstr "Currently, the FWaaSv2 L2 driver can be configured as:" + +msgid "" +"Earlier the FWaaS agent integrated with the L3 agent by having the L3 Agent " +"class inherit from the FWaaS Agent class. This meant that other service " +"agents could not also integrate with the L3 agent. Now, using the L3 agent " +"extensions mechanism, FWaaS (v1 and v2) plugs in to the L3 agent. This " +"means that it can interoperate peacefully with other L3 advanced services " +"that also implement the L3 agent extension mechanism, all without any code " +"changes to Neutron." +msgstr "" +"Earlier the FWaaS agent integrated with the L3 agent by having the L3 Agent " +"class inherit from the FWaaS Agent class. This meant that other service " +"agents could not also integrate with the L3 agent. Now, using the L3 agent " +"extensions mechanism, FWaaS (v1 and v2) plugs in to the L3 agent. This " +"means that it can interoperate peacefully with other L3 advanced services " +"that also implement the L3 agent extension mechanism, all without any code " +"changes to Neutron." + +msgid "Enable quotas for FWaaS." +msgstr "Enable quotas for FWaaS." + +msgid "" +"Enable to collect network packet log for ACCEPT/DROP action from firewall " +"groups. Currently, packet logging supports only L3(router) ports." +msgstr "" +"Enable to collect network packet log for ACCEPT/DROP action from firewall " +"groups. Currently, packet logging supports only L3(router) ports." + +msgid "" +"FWaaS V1 is being removed from the neutron-fwaas repo. Because FWaaS V2 has " +"been available since the Newton release." +msgstr "" +"FWaaS V1 is being removed from the neutron-fwaas repo. Because FWaaS V2 has " +"been available since the Newton release." + +msgid "" +"FWaaS v1 can not be enabled at the same time as FWaaS v2; one or the other " +"must be chosen." +msgstr "" +"FWaaS v1 can not be enabled at the same time as FWaaS v2; one or the other " +"must be chosen." + +msgid "Generation of sample Neutron FWaaS configuration files." +msgstr "Generation of sample Neutron FWaaS configuration files." + +msgid "" +"If a port is associated with both firewall group & security group and there " +"is a security group logging, which is enabled to collect ``DROP`` events for " +"this port, then most of invalid packets will be dropped at firewall group " +"for performance reason except first dropped packet, which is allowed by " +"firewall group but not accepted by security group. So not every dropped " +"packet will be logged (like in case of security group works in standalone " +"mode)." +msgstr "" +"If a port is associated with both firewall group & security group and there " +"is a security group logging, which is enabled to collect ``DROP`` events for " +"this port, then most of invalid packets will be dropped at firewall group " +"for performance reason except first dropped packet, which is allowed by " +"firewall group but not accepted by security group. So not every dropped " +"packet will be logged (like in case of security group works in standalone " +"mode)." + +msgid "" +"If both are configured, the packet will still only hit the FWaaS table in " +"OVS and will not traverse the rules in the SG table. There are some fixes " +"needed to support this model which are being tested and will be merged " +"shortly. Currently there are no checks to allow only one of FWaaS L2 or SG " +"to be configured." +msgstr "" +"If both are configured, the packet will still only hit the FWaaS table in " +"OVS and will not traverse the rules in the SG table. There are some fixes " +"needed to support this model which are being tested and will be merged " +"shortly. Currently there are no checks to allow only one of FWaaS L2 or SG " +"to be configured." + +msgid "" +"In FWaaS v2 firewall policies are applied to router ports, as opposed to " +"applying to routers in FWaaS v1." +msgstr "" +"In FWaaS v2 firewall policies are applied to router ports, as opposed to " +"applying to routers in FWaaS v1." + +msgid "Known Issues" +msgstr "Known Issues" + +msgid "" +"L2 firewall group driver based OVS can work in coexistence mode. That means, " +"if a port is associated with both firewall group and security group, then a " +"packet must be allowed by both features." +msgstr "" +"L2 firewall group driver based OVS can work in coexistence mode. That means, " +"if a port is associated with both firewall group and security group, then a " +"packet must be allowed by both features." + +msgid "Liberty Series Release Notes" +msgstr "Liberty Series Release Notes" + +msgid "Mitaka Series Release Notes" +msgstr "Mitaka Series Release Notes" + +msgid "Neutron FWaaS Release Notes" +msgstr "Neutron FWaaS Release Notes" + +msgid "" +"Neutron FWaaS no longer includes static example configuration files. " +"Instead, use tools/generate_config_file_samples.sh to generate them. The " +"files are generated with a .sample extension." +msgstr "" +"Neutron FWaaS no longer includes static example configuration files. " +"Instead, use tools/generate_config_file_samples.sh to generate them. The " +"files are generated with a .sample extension." + +msgid "" +"Neutron Firewall as a Service can be configured by the users with the newly " +"introduced fwaas configuration file." +msgstr "" +"Neutron Firewall as a Service can be configured by the users with the newly " +"introduced FWaaS configuration file." + +msgid "New Features" +msgstr "New Features" + +msgid "" +"New tables ``ACCEPTED_EGRESS_TRAFFIC_TABLE=91`` and " +"``ACCEPTED_INGRESS_TRAFFIC_TABLE=92`` & ``DROPPED_TRAFFIC_TABLE=93`` are " +"added to OVS based FWaaS L2 driver for future comsumption like logging " +"service." +msgstr "" +"New tables ``ACCEPTED_EGRESS_TRAFFIC_TABLE=91`` and " +"``ACCEPTED_INGRESS_TRAFFIC_TABLE=92`` & ``DROPPED_TRAFFIC_TABLE=93`` are " +"added to OVS based FWaaS L2 driver for future consumption like logging " +"service." + +msgid "Newton Series Release Notes" +msgstr "Newton Series Release Notes" + +msgid "Ocata Series Release Notes" +msgstr "Ocata Series Release Notes" + +msgid "" +"Operators that increase the default limit for quota_routers from 10 may want " +"to bump FWaaS quotas as well, since with router insertion a tenant can " +"potentially have a unique policy and firewall for each router." +msgstr "" +"Operators that increase the default limit for quota_routers from 10 may want " +"to bump FWaaS quotas as well, since with router insertion a tenant can " +"potentially have a unique policy and firewall for each router." + +msgid "Other Notes" +msgstr "Other Notes" + +msgid "Pike Series Release Notes" +msgstr "Pike Series Release Notes" + +msgid "Prelude" +msgstr "Prelude" + +msgid "" +"Python 2.7 support has been dropped. The minimum version of Python now " +"supported by neutron-fwaas is Python 3.6." +msgstr "" +"Python 2.7 support has been dropped. The minimum version of Python now " +"supported by neutron-fwaas is Python 3.6." + +msgid "Queens Series Release Notes" +msgstr "Queens Series Release Notes" + +msgid "" +"Resource type **firewall group** has been supported for neutron packet " +"logging framework. You can specify firewall group as ``--resource-type`` " +"for logging API." +msgstr "" +"Resource type **firewall group** has been supported for neutron packet " +"logging framework. You can specify firewall group as ``--resource-type`` " +"for logging API." + +msgid "Rocky Series Release Notes" +msgstr "Rocky Series Release Notes" + +msgid "Start using reno to manage release notes." +msgstr "Start using Reno to manage release notes." + +msgid "Stein Series Release Notes" +msgstr "Stein Series Release Notes" + +msgid "" +"Taking security for VM instance into consideration, we've removed an option " +"to disable automatic association with default firewall group feature. " +"Therefore, `auto_associate_default_firewall_group` has been removed." +msgstr "" +"Taking security for VM instance into consideration, we've removed an option " +"to disable automatic association with default firewall group feature. " +"Therefore, `auto_associate_default_firewall_group` has been removed." + +msgid "" +"Tenants may receive a 409 Conflict error with a message body containing a " +"quota exceeded message during resource creation if their quota is exceeded." +msgstr "" +"Tenants may receive a 409 Conflict error with a message body containing a " +"quota exceeded message during resource creation if their quota is exceeded." + +msgid "" +"The Cisco FWaaS driver will not be available from the neutron-fwaas repo in " +"Newton. For the Cisco FWaaS driver, refer to the openstack/networking-cisco " +"repo." +msgstr "" +"The Cisco FWaaS driver will not be available from the neutron-fwaas repo in " +"Newton. For the Cisco FWaaS driver, refer to the openstack/networking-cisco " +"repo." + +msgid "" +"The Cisco Firewall Driver is being moved from the FWaaS repo to the Cisco " +"specific repo: https://github.com/openstack/networking-cisco" +msgstr "" +"The Cisco Firewall Driver is being moved from the FWaaS repo to the Cisco " +"specific repo: https://github.com/openstack/networking-cisco" + +msgid "" +"The FWaaS V1 source code will not be available in neutron-fwaas repo from " +"Stein. neutron-fwaas-migrate-v1-to-v2 can be used for migrating V1 object to " +"V2 model." +msgstr "" +"The FWaaS V1 source code will not be available in neutron-fwaas repo from " +"Stein. neutron-fwaas-migrate-v1-to-v2 can be used for migrating V1 object to " +"V2 model." + +msgid "" +"The FWaaS extension can register quotas. The default values for " +"quota_firewall, quota_firewall_policy, and quota_firewall_rule are set to -1 " +"(unlimited)." +msgstr "" +"The FWaaS extension can register quotas. The default values for " +"quota_firewall, quota_firewall_policy, and quota_firewall_rule are set to -1 " +"(unlimited)." + +msgid "" +"The FWaaS extension will register quotas. The default values for " +"quota_firewall and quota_firewall_policy are set to 10. The default value " +"for quota_firewall_rule is set to 100. Quotas can be adjusted in the conf " +"files, including -1 values to allow unlimited." +msgstr "" +"The FWaaS extension will register quotas. The default values for " +"quota_firewall and quota_firewall_policy are set to 10. The default value " +"for quota_firewall_rule is set to 100. Quotas can be adjusted in the conf " +"files, including -1 values to allow unlimited." + +msgid "" +"The FWaaS team is pleased to release FWaaS v2.0. This release of FWaaS " +"supports either the original FWaaS v1 or the new FWaaS v2." +msgstr "" +"The FWaaS team is pleased to release FWaaS v2.0. This release of FWaaS " +"supports either the original FWaaS v1 or the new FWaaS v2." + +msgid "" +"The McAfee Firewall Driver is being removed from the FwaaS repo, due to lack " +"of active maintainers." +msgstr "" +"The McAfee Firewall Driver is being removed from the FWaaS repo, due to lack " +"of active maintainers." + +msgid "" +"The McAfee Firewall Driver will not be available for use in the Newton " +"release." +msgstr "" +"The McAfee Firewall Driver will not be available for use in the Newton " +"release." + +msgid "" +"The default firewall group won't be applied to all new VM ports as default. " +"However, if option ``auto_associate_default_firewall_group`` is enabled in " +"neutron_fwaas.conf like:" +msgstr "" +"The default firewall group won't be applied to all new VM ports as default. " +"However, if option ``auto_associate_default_firewall_group`` is enabled in " +"neutron_fwaas.conf like:" + +msgid "" +"The limitation related to logging for security group in case of co-existence " +"between SG and FWG is also fixed." +msgstr "" +"The limitation related to logging for security group in case of co-existence " +"between SG and FWG is also fixed." + +msgid "" +"The vArmour Firewall Driver is being removed from the FwaaS repo, as per " +"decision to remove vendor drivers from the community repo." +msgstr "" +"The vArmour Firewall Driver is being removed from the FWaaS repo, as per " +"decision to remove vendor drivers from the community repo." + +msgid "" +"The vArmour Firewall Driver will not be available for use in the Newton " +"release." +msgstr "" +"The vArmour Firewall Driver will not be available for use in the Newton " +"release." + +msgid "The vyatta Firewall Driver is being removed from the FwaaS repo," +msgstr "The Vyatta Firewall Driver is being removed from the FWaaS repo," + +msgid "" +"The vyatta Firewall Driver will not be available for use in the Newton " +"release from the community repo." +msgstr "" +"The Vyatta Firewall Driver will not be available for use in the Newton " +"release from the community repo." + +msgid "Then, the default firewall group will be applied to all new VM ports." +msgstr "Then, the default firewall group will be applied to all new VM ports." + +msgid "" +"There is no validation to check if an updated port is for VM or not so far. " +"After this fix, default firewall group association is called only for VM " +"ports which are newly created." +msgstr "" +"There is no validation to check if an updated port is for VM or not so far. " +"After this fix, default firewall group association is called only for VM " +"ports which are newly created." + +msgid "" +"There is not currently a defined upgrade path from FWaaS v1 to FWaaS v2." +msgstr "" +"There is not currently a defined upgrade path from FWaaS v1 to FWaaS v2." + +msgid "Therefore, you can only run with following combinations:" +msgstr "Therefore, you can only run with following combinations:" + +msgid "Upgrade Notes" +msgstr "Upgrade Notes" + +msgid "Validating if a port is supported by FWaaS V2" +msgstr "Validating if a port is supported by FWaaS V2" + +msgid "" +"[`bug 1702242 `__] Port " +"range specification of a firewall rule now works expectedly with the " +"reference L3 agent based implementation. Previously, when creating a " +"firewall rule with port range like ``8778:9000``, the rule was not deleted " +"correctly and only entries associated with the first port number were clean " +"up. Note that this bug is only applied to the reference L3 agent based " +"implementation." +msgstr "" +"[`bug 1702242 `__] Port " +"range specification of a firewall rule now works expectedly with the " +"reference L3 agent based implementation. Previously, when creating a " +"firewall rule with port range like ``8778:9000``, the rule was not deleted " +"correctly and only entries associated with the first port number were clean " +"up. Note that this bug is only applied to the reference L3 agent based " +"implementation." + +msgid "" +"[`bug 1720727 `__] " +"Currently, we cannot specify the following combination on CLI due to missing " +"validation of --resource-type:" +msgstr "" +"[`bug 1720727 `__] " +"Currently, we cannot specify the following combination on CLI due to missing " +"validation of --resource-type:" + +msgid "" +"[`bug 1746855 `__] Now, " +"FWaaS V2 will validate if a port is supported before adding it to a FWG. " +"This helps to make sure FWaaS V2 API works as expected." +msgstr "" +"[`bug 1746855 `__] Now, " +"FWaaS V2 will validate if a port is supported before adding it to a FWG. " +"This helps to make sure FWaaS V2 API works as expected." + +msgid "[fwaas] auto_associate_default_firewall_group = True" +msgstr "[fwaas] auto_associate_default_firewall_group = True" + +msgid "``firewall_driver = openvswitch``" +msgstr "``firewall_driver = openvswitch``" + +msgid "``firewall_driver = ovs``" +msgstr "``firewall_driver = ovs``" + +msgid "as per decision to remove vendor drivers from the community repo." +msgstr "as per decision to remove vendor drivers from the community repo." diff --git a/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po new file mode 100644 index 000000000..250b7f911 --- /dev/null +++ b/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po @@ -0,0 +1,66 @@ +# Gérald LONLAS , 2016. #zanata +msgid "" +msgstr "" +"Project-Id-Version: Neutron FWaaS Release Notes 11.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-16 20:31+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2016-10-22 05:48+0000\n" +"Last-Translator: Gérald LONLAS \n" +"Language-Team: French\n" +"Language: fr\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" + +msgid "7.0.2" +msgstr "7.0.2" + +msgid "7.1.1" +msgstr "7.1.1" + +msgid "8.0.0" +msgstr "8.0.0" + +msgid "9.0.0" +msgstr "9.0.0" + +msgid "9.0.0.0b2" +msgstr "9.0.0.0b2" + +msgid "9.0.0.0b3" +msgstr "9.0.0.0b3" + +msgid "9.0.0.0rc1" +msgstr "9.0.0.0rc1" + +msgid "Current Series Release Notes" +msgstr "Note de la release actuelle" + +msgid "Known Issues" +msgstr "Problèmes connus" + +msgid "Liberty Series Release Notes" +msgstr "Note de release pour Liberty" + +msgid "Mitaka Series Release Notes" +msgstr "Note de release pour Mitaka" + +msgid "Neutron FWaaS Release Notes" +msgstr "Note de release de Neutron FWaaS" + +msgid "New Features" +msgstr "Nouvelles fonctionnalités" + +msgid "Newton Series Release Notes" +msgstr "Note de release pour Newton" + +msgid "Other Notes" +msgstr "Autres notes" + +msgid "Start using reno to manage release notes." +msgstr "Commence à utiliser reno pour la gestion des notes de release" + +msgid "Upgrade Notes" +msgstr "Notes de mises à jours" diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst new file mode 100644 index 000000000..e54560965 --- /dev/null +++ b/releasenotes/source/mitaka.rst @@ -0,0 +1,6 @@ +=================================== + Mitaka Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/mitaka diff --git a/releasenotes/source/newton.rst b/releasenotes/source/newton.rst new file mode 100644 index 000000000..97036ed25 --- /dev/null +++ b/releasenotes/source/newton.rst @@ -0,0 +1,6 @@ +=================================== + Newton Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/newton diff --git a/releasenotes/source/ocata.rst b/releasenotes/source/ocata.rst new file mode 100644 index 000000000..ebe62f42e --- /dev/null +++ b/releasenotes/source/ocata.rst @@ -0,0 +1,6 @@ +=================================== + Ocata Series Release Notes +=================================== + +.. release-notes:: + :branch: origin/stable/ocata diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst new file mode 100644 index 000000000..e43bfc0ce --- /dev/null +++ b/releasenotes/source/pike.rst @@ -0,0 +1,6 @@ +=================================== + Pike Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/pike diff --git a/releasenotes/source/queens.rst b/releasenotes/source/queens.rst new file mode 100644 index 000000000..36ac6160c --- /dev/null +++ b/releasenotes/source/queens.rst @@ -0,0 +1,6 @@ +=================================== + Queens Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/queens diff --git a/releasenotes/source/rocky.rst b/releasenotes/source/rocky.rst new file mode 100644 index 000000000..40dd517b7 --- /dev/null +++ b/releasenotes/source/rocky.rst @@ -0,0 +1,6 @@ +=================================== + Rocky Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/rocky diff --git a/releasenotes/source/stein.rst b/releasenotes/source/stein.rst new file mode 100644 index 000000000..efaceb667 --- /dev/null +++ b/releasenotes/source/stein.rst @@ -0,0 +1,6 @@ +=================================== + Stein Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/stein diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 000000000..cd22aabcc --- /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 000000000..9af33288f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,28 @@ +# 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>=4.0.0 # Apache-2.0 + +eventlet!=0.18.3,!=0.20.1,>=0.18.2 # MIT +netaddr>=0.7.18 # BSD +SQLAlchemy>=1.4.23 # MIT +alembic>=1.6.5 # MIT +six>=1.10.0 # MIT +neutron-lib>=1.26.0 # Apache-2.0 +os-ken >= 0.3.0 # Apache-2.0 +oslo.config>=5.2.0 # Apache-2.0 +oslo.db>=4.37.0 # Apache-2.0 +oslo.log>=3.36.0 # Apache-2.0 +oslo.messaging>=5.29.0 # Apache-2.0 +oslo.service!=1.28.1,>=1.24.0 # Apache-2.0 +oslo.utils>=3.33.0 # Apache-2.0 +oslo.privsep>=1.32.0 # Apache-2.0 +pyroute2>=0.5.3;sys_platform!='win32' # Apache-2.0 (+ dual licensed GPL2) +neutron>=13.0.0.0b1 # Apache-2.0 +pyzmq>=14.3.1 # LGPL+BSD + +# The comment below indicates this project repo is current with neutron-lib +# and should receive neutron-lib consumption patches as they are released +# in neutron-lib. It also implies the project will stay current with TC +# and infra initiatives ensuring consumption patches can land. +# neutron-lib-current diff --git a/roles/configure_functional_tests/README.rst b/roles/configure_functional_tests/README.rst new file mode 100644 index 000000000..f6bf0d61b --- /dev/null +++ b/roles/configure_functional_tests/README.rst @@ -0,0 +1,24 @@ +Configure host to run on it Neutron functional/fullstack tests + +**Role Variables** + +.. zuul:rolevar:: tests_venv + :default: {{ tox_envlist }} + +.. zuul:rolevar:: project_name + :default: neutron + +.. zuul:rolevar:: base_dir + :default: {{ ansible_user_dir }}/src/opendev.org + +.. zuul:rolevar:: gate_dest_dir + :default: {{ base_dir }}/openstack + +.. zuul:rolevar:: devstack_dir + :default: {{ base_dir }}/openstack/devstack + +.. zuul:rolevar:: neutron_dir + :default: {{ gate_dest_dir }}/neutron + +.. zuul:rolevar:: neutron_fwaas_dir + :default: {{ gate_dest_dir }}/neutron-fwaas diff --git a/roles/configure_functional_tests/defaults/main.yaml b/roles/configure_functional_tests/defaults/main.yaml new file mode 100644 index 000000000..fd43c27b6 --- /dev/null +++ b/roles/configure_functional_tests/defaults/main.yaml @@ -0,0 +1,7 @@ +tests_venv: "{{ tox_envlist }}" +project_name: "neutron" +base_dir: "{{ ansible_user_dir }}/src/opendev.org" +gate_dest_dir: "{{ base_dir }}/openstack" +devstack_dir: "{{ base_dir }}/openstack/devstack" +neutron_dir: "{{ gate_dest_dir }}/neutron" +neutron_fwaas_dir: "{{ gate_dest_dir }}/neutron-fwaas" diff --git a/roles/configure_functional_tests/tasks/main.yaml b/roles/configure_functional_tests/tasks/main.yaml new file mode 100644 index 000000000..8ef553dc8 --- /dev/null +++ b/roles/configure_functional_tests/tasks/main.yaml @@ -0,0 +1,21 @@ +- shell: + cmd: | + set -e + set -x + GATE_STACK_USER={{ ansible_user }} + IS_GATE=True + + BASE_DIR={{ base_dir }} + GATE_DEST={{ gate_dest_dir }} + PROJECT_NAME={{ project_name }} + NEUTRON_PATH={{ neutron_dir }} + NEUTRON_FWAAS_PATH={{ neutron_fwaas_dir }} + DEVSTACK_PATH={{ devstack_dir }} + VENV={{ tests_venv }} + + source $DEVSTACK_PATH/functions + source $DEVSTACK_PATH/lib/neutron_plugins/ovs_source + source $NEUTRON_FWAAS_PATH/tools/configure_for_fwaas_func_testing.sh + + configure_host_for_fwaas_func_testing + executable: /bin/bash diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..1aae59f85 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,75 @@ +[metadata] +name = neutron-fwaas +summary = OpenStack Networking FWaaS +description-file = + README.rst +author = OpenStack +author-email = openstack-discuss@lists.openstack.org +home-page = https://docs.openstack.org/neutron-fwaas/latest/ +python-requires = >=3.6 +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 :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3 :: Only + +[files] +packages = + neutron_fwaas + +data_files = + etc/neutron/rootwrap.d = + etc/neutron/rootwrap.d/fwaas-privsep.filters + +[entry_points] +firewall_drivers = + iptables_v2 = neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.iptables_fwaas_v2:IptablesFwaasDriver +neutron.service_plugins = + firewall_v2 = neutron_fwaas.services.firewall.fwaas_plugin_v2:FirewallPluginV2 + +neutron.db.alembic_migrations = + neutron-fwaas = neutron_fwaas.db.migration:alembic_migrations +oslo.config.opts = + neutron.fwaas = neutron_fwaas.opts:list_opts + firewall.agent = neutron_fwaas.opts:list_agent_opts +oslo.policy.policies = + neutron-fwaas = neutron_fwaas.policies:list_rules +neutron.policies = + neutron-fwaas = neutron_fwaas.policies:list_rules +neutron.agent.l2.extensions = + fwaas_v2 = neutron_fwaas.services.firewall.service_drivers.agents.l2.fwaas_v2:FWaaSV2AgentExtension +neutron.agent.l2.firewall_drivers = + noop = neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.noop.noop_driver:NoopFirewallL2Driver + ovs = neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.openvswitch_firewall.firewall:OVSFirewallDriver +neutron.agent.l3.extensions = + fwaas_v2 = neutron_fwaas.services.firewall.service_drivers.agents.l3reference.firewall_l3_agent_v2:L3WithFWaaS + fwaas_v2_log = neutron_fwaas.services.logapi.agents.l3.fwg_log:FWaaSL3LoggingExtension +neutron.agent.l3.firewall_drivers = + conntrack = neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.legacy_conntrack:ConntrackLegacy + netlink_conntrack = neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.netlink_conntrack:ConntrackNetlink +neutron.services.logapi.drivers = + fwaas_v2_log = neutron_fwaas.services.logapi.agents.drivers.iptables.log:IptablesLoggingDriver +console_scripts = + neutron-fwaas-migrate-v1-to-v2 = neutron_fwaas.cmd.v1_to_v2_db_migration:main +neutron.status.upgrade.checks = + neutron_fwaas = neutron_fwaas.cmd.upgrade_checks.checks:Checks + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = neutron_fwaas/locale/neutron_fwaas.pot + +[compile_catalog] +directory = neutron_fwaas/locale +domain = neutron_fwaas + +[update_catalog] +domain = neutron_fwaas +output_dir = neutron_fwaas/locale +input_file = neutron_fwaas/locale/neutron_fwaas.pot diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..566d84432 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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 000000000..7973648e5 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,22 @@ +# 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>=3.0.1,<3.1.0 # Apache-2.0 + +coverage!=4.4,>=4.0 # Apache-2.0 +flake8-import-order==0.12 # LGPLv3 +mock>=2.0.0 # BSD +python-subunit>=1.0.0 # Apache-2.0/BSD +requests-mock>=1.2.0 # Apache-2.0 +oslo.concurrency>=3.26.0 # Apache-2.0 +stestr>=1.0.0 # Apache-2.0 +testresources>=2.0.0 # Apache-2.0/BSD +testtools>=2.2.0 # MIT +testscenarios>=0.4 # Apache-2.0/BSD +WebOb>=1.8.2 # MIT +WebTest>=2.0.27 # MIT +oslotest>=3.2.0 # Apache-2.0 +PyMySQL>=0.7.6 # MIT License +psycopg2>=2.7.3 # LGPL/ZPL +doc8>=0.6.0 # Apache-2.0 +Pygments>=2.2.0 # BSD diff --git a/tools/check_unit_test_structure.sh b/tools/check_unit_test_structure.sh new file mode 100755 index 000000000..4e6d58d9f --- /dev/null +++ b/tools/check_unit_test_structure.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# This script identifies the unit test modules that do not correspond +# directly with a module in the code tree. See TESTING.rst for the +# intended structure. + +neutron_path=$(cd "$(dirname "$0")/.." && pwd) +base_test_path=neutron_fwaas/tests/unit +test_path=$neutron_path/$base_test_path + +test_files=$(find ${test_path} -iname 'test_*.py') + +ignore_regexes=( + "^plugins.*$", + "^misc.*$" +) + +error_count=0 +ignore_count=0 +total_count=0 +for test_file in ${test_files[@]}; do + relative_path=${test_file#$test_path/} + expected_path=$(dirname $neutron_path/neutron_fwaas/$relative_path) + test_filename=$(basename "$test_file") + expected_filename=${test_filename#test_} + # Module filename (e.g. foo/bar.py -> foo/test_bar.py) + filename=$expected_path/$expected_filename + # Package dir (e.g. foo/ -> test_foo.py) + package_dir=${filename%.py} + if [ ! -f "$filename" ] && [ ! -d "$package_dir" ]; then + for ignore_regex in ${ignore_regexes[@]}; do + if [[ "$relative_path" =~ $ignore_regex ]]; then + ((ignore_count++)) + continue 2 + fi + done + echo "Unexpected test file: $base_test_path/$relative_path" + ((error_count++)) + fi + ((total_count++)) +done + +if [ "$ignore_count" -ne 0 ]; then + echo "$ignore_count unmatched test modules were ignored" +fi + +if [ "$error_count" -eq 0 ]; then + echo 'Success! All test modules match targets in the code tree.' + exit 0 +else + echo "Failure! $error_count of $total_count test modules do not match targets in the code tree." + exit 1 +fi diff --git a/tools/clean.sh b/tools/clean.sh new file mode 100755 index 000000000..b79f03526 --- /dev/null +++ b/tools/clean.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +rm -rf ./*.deb ./*.tar.gz ./*.dsc ./*.changes +rm -rf */*.deb +rm -rf ./plugins/**/build/ ./plugins/**/dist +rm -rf ./plugins/**/lib/neutron_*_plugin.egg-info ./plugins/neutron-* diff --git a/tools/configure_for_func_testing.sh b/tools/configure_for_func_testing.sh new file mode 100755 index 000000000..84869292f --- /dev/null +++ b/tools/configure_for_func_testing.sh @@ -0,0 +1,281 @@ +#!/usr/bin/env bash + +# 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. + + +set -e + + +# Control variable used to determine whether to execute this script +# directly or allow the gate_hook to import. +IS_GATE=${IS_GATE:-False} +USE_CONSTRAINT_ENV=${USE_CONSTRAINT_ENV:-True} + + +if [[ "$IS_GATE" != "True" ]] && [[ "$#" -lt 1 ]]; then + >&2 echo "Usage: $0 /path/to/devstack [-i] +Configure a host to run Neutron's functional test suite. + +-i Install Neutron's package dependencies. By default, it is assumed + that devstack has already been used to deploy neutron-fwaas to the + target host and that package dependencies need not be installed. + +Warning: This script relies on devstack to perform extensive +modification to the underlying host. It is recommended that it be +invoked only on a throw-away VM." + exit 1 +fi + + +# Skip the first argument +OPTIND=2 +while getopts ":i" opt; do + case $opt in + i) + INSTALL_BASE_DEPENDENCIES=True + ;; + esac + +done + +# Default to environment variables to permit the gate_hook to override +# when sourcing. +VENV=${VENV:-dsvm-functional} +DEVSTACK_PATH=${DEVSTACK_PATH:-$1} +PROJECT_NAME=${PROJECT_NAME:-neutron-fwaas} +REPO_BASE=${GATE_DEST:-$(cd $(dirname "$0")/../.. && pwd)} +INSTALL_MYSQL_ONLY=${INSTALL_MYSQL_ONLY:-False} +# The gate should automatically install dependencies. +INSTALL_BASE_DEPENDENCIES=${INSTALL_BASE_DEPENDENCIES:-$IS_GATE} + + +if [ ! -f "$DEVSTACK_PATH/stack.sh" ]; then + >&2 echo "Unable to find devstack at '$DEVSTACK_PATH'. Please verify that the specified path points to a valid devstack repo." + exit 1 +fi + + +set -x + + +function _init { + # Subsequently-called devstack functions depend on the following variables. + HOST_IP=127.0.0.1 + FILES=$DEVSTACK_PATH/files + TOP_DIR=$DEVSTACK_PATH + + source $DEVSTACK_PATH/stackrc + + # Allow the gate to override values set by stackrc. + DEST=${GATE_DEST:-$DEST} + STACK_USER=${GATE_STACK_USER:-$STACK_USER} +} + + +function _install_base_deps { + echo_summary "Installing base dependencies" + + INSTALL_TESTONLY_PACKAGES=True + PACKAGES=$(get_packages general,neutron,q-agt,q-l3) + # Do not install 'python-' prefixed packages other than + # python-dev*. Neutron's functional testing relies on deployment + # to a tox env so there is no point in installing python + # dependencies system-wide. + PACKAGES=$(echo $PACKAGES | perl -pe 's|python-(?!dev)[^ ]*||g') + install_package $PACKAGES +} + + +function _install_rpc_backend { + echo_summary "Installing rabbitmq" + + RABBIT_USERID=${RABBIT_USERID:-stackrabbit} + RABBIT_HOST=${RABBIT_HOST:-$SERVICE_HOST} + RABBIT_PASSWORD=${RABBIT_HOST:-secretrabbit} + + source $DEVSTACK_PATH/lib/rpc_backend + + enable_service rabbit + install_rpc_backend + restart_rpc_backend +} + + +# _install_databases [install_pg] +function _install_databases { + local install_pg=${1:-True} + + echo_summary "Installing databases" + + # Avoid attempting to configure the db if it appears to already + # have run. The setup as currently defined is not idempotent. + if mysql openstack_citest > /dev/null 2>&1 < /dev/null; then + echo_summary "DB config appears to be complete, skipping." + return 0 + fi + + MYSQL_PASSWORD=${MYSQL_PASSWORD:-secretmysql} + DATABASE_PASSWORD=${DATABASE_PASSWORD:-secretdatabase} + + source $DEVSTACK_PATH/lib/database + + enable_service mysql + initialize_database_backends + install_database + configure_database_mysql + + if [[ "$install_pg" == "True" ]]; then + enable_service postgresql + initialize_database_backends + install_database + configure_database_postgresql + fi + + # Set up the 'openstack_citest' user and database in each backend + tmp_dir=$(mktemp -d) + trap "rm -rf $tmp_dir" EXIT + + cat << EOF > $tmp_dir/mysql.sql +CREATE DATABASE openstack_citest; +CREATE USER 'openstack_citest'@'localhost' IDENTIFIED BY 'openstack_citest'; +CREATE USER 'openstack_citest' IDENTIFIED BY 'openstack_citest'; +GRANT ALL PRIVILEGES ON *.* TO 'openstack_citest'@'localhost'; +GRANT ALL PRIVILEGES ON *.* TO 'openstack_citest'; +FLUSH PRIVILEGES; +EOF + /usr/bin/mysql -u root < $tmp_dir/mysql.sql + + if [[ "$install_pg" == "True" ]]; then + cat << EOF > $tmp_dir/postgresql.sql +CREATE USER openstack_citest WITH CREATEDB LOGIN PASSWORD 'openstack_citest'; +CREATE DATABASE openstack_citest WITH OWNER openstack_citest; +EOF + + # User/group postgres needs to be given access to tmp_dir + setfacl -m g:postgres:rwx $tmp_dir + sudo -u postgres /usr/bin/psql --file=$tmp_dir/postgresql.sql + fi +} + + +function _install_agent_deps { + echo_summary "Installing agent dependencies" + + ENABLED_SERVICES=q-agt,q-dhcp,q-l3 + install_neutron_agent_packages +} + + +# Set up the rootwrap sudoers for neutron to target the rootwrap +# configuration deployed in the venv. +function _install_rootwrap_sudoers { + echo_summary "Installing rootwrap sudoers file" + + PROJECT_VENV=$REPO_BASE/$PROJECT_NAME/.tox/$VENV + ROOTWRAP_SUDOER_CMD="$PROJECT_VENV/bin/neutron-rootwrap $PROJECT_VENV/etc/neutron/rootwrap.conf *" + ROOTWRAP_DAEMON_SUDOER_CMD="$PROJECT_VENV/bin/neutron-rootwrap-daemon $PROJECT_VENV/etc/neutron/rootwrap.conf" + TEMPFILE=$(mktemp) + cat << EOF > $TEMPFILE +# A bug in oslo.rootwrap [1] prevents commands executed with 'ip netns +# exec' from being automatically qualified with a prefix from +# rootwrap's configured exec_dirs. To work around this problem, add +# the venv bin path to a user-specific secure_path. +# +# While it might seem preferable to set a command-specific +# secure_path, this would only ensure the correct path for 'ip netns +# exec' and the command targeted for execution in the namespace would +# not inherit the path. +# +# 1: https://bugs.launchpad.net/oslo.rootwrap/+bug/1417331 +# +Defaults:$STACK_USER secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PROJECT_VENV/bin" +$STACK_USER ALL=(root) NOPASSWD: $ROOTWRAP_SUDOER_CMD +$STACK_USER ALL=(root) NOPASSWD: $ROOTWRAP_DAEMON_SUDOER_CMD +EOF + chmod 0440 $TEMPFILE + sudo chown root:root $TEMPFILE + # Name the functional testing rootwrap to ensure that it will be + # loaded after the devstack rootwrap (50_stack_sh if present) so + # that the functional testing secure_path (a superset of what + # devstack expects) will not be overwritten. + sudo mv $TEMPFILE /etc/sudoers.d/60-neutron-func-test-rootwrap +} + + +function _install_post_devstack { + echo_summary "Performing post-devstack installation" + + _install_databases + _install_rootwrap_sudoers + + if is_ubuntu; then + install_package isc-dhcp-client + install_package netcat-openbsd + elif is_fedora; then + install_package dhclient + else + exit_distro_not_supported "installing dhclient package" + fi + + # Installing python-openvswitch from packages is a stop-gap while + # python-openvswitch remains unavailable from pypi. This also + # requires that sitepackages=True be set in tox.ini to allow the + # venv to use the installed package. Once python-openvswitch + # becomes available on pypi, this will no longer be required. + # + # NOTE: the package name 'python-openvswitch' is common across + # supported distros. + install_package python-openvswitch + + enable_kernel_bridge_firewall +} + + +function _configure_iptables_rules { + # For linuxbridge agent fullstack tests we need to add special rules to + # iptables for connection of agents to rabbitmq: + CHAIN_NAME="openstack-INPUT" + sudo iptables -n --list $CHAIN_NAME 1> /dev/null 2>&1 || CHAIN_NAME="INPUT" + sudo iptables -I $CHAIN_NAME -s 240.0.0.0/8 -p tcp -m tcp -d 240.0.0.0/8 --dport 5672 -j ACCEPT +} + + +function configure_host_for_func_testing { + echo_summary "Configuring host for functional testing" + + if [[ "$INSTALL_BASE_DEPENDENCIES" == "True" ]]; then + # Installing of the following can be achieved via devstack by + # installing neutron, so their installation is conditional to + # minimize the work to do on a devstack-configured host. + _install_base_deps + _install_agent_deps + _install_rpc_backend + fi + _install_post_devstack +} + + +_init + + +if [[ "$IS_GATE" != "True" ]]; then + if [[ "$INSTALL_MYSQL_ONLY" == "True" ]]; then + _install_databases nopg + else + configure_host_for_func_testing + fi +fi + +if [[ "$VENV" =~ "dsvm-fullstack" ]]; then + _configure_iptables_rules +fi diff --git a/tools/configure_for_fwaas_func_testing.sh b/tools/configure_for_fwaas_func_testing.sh new file mode 100755 index 000000000..b2635209a --- /dev/null +++ b/tools/configure_for_fwaas_func_testing.sh @@ -0,0 +1,34 @@ +set -e + + +IS_GATE=${IS_GATE:-False} +USE_CONSTRAINT_ENV=${USE_CONSTRAINT_ENV:-False} +PROJECT_NAME=${PROJECT_NAME:-neutron-fwaas} +REPO_BASE=${GATE_DEST:-$(cd $(dirname "$BASH_SOURCE")/../.. && pwd)} + +source $REPO_BASE/neutron/tools/configure_for_func_testing.sh +NEUTRON_FWAAS_DIR=$REPO_BASE/neutron-fwaas +source $NEUTRON_FWAAS_DIR/devstack/plugin.sh + +function _install_fw_package { + echo_summary "Installing fw packs" + if is_ubuntu; then + install_package conntrack + else + # EPEL + install_package conntrack-tools + fi +} + +function configure_host_for_fwaas_func_testing { + echo_summary "Configuring for Fwaas functional testing" + if [ "$IS_GATE" == "True" ]; then + configure_host_for_func_testing + fi + _install_fw_package +} + + +if [ "$IS_GATE" != "True" ]; then + configure_host_for_fwaas_func_testing +fi diff --git a/tools/deploy_rootwrap.sh b/tools/deploy_rootwrap.sh new file mode 100755 index 000000000..b261aee2c --- /dev/null +++ b/tools/deploy_rootwrap.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +# 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. + +set -eu + +if [ "$#" -ne 3 ]; then + >&2 echo "Usage: $0 /path/to/neutron_fwaas /path/to/target/etc /path/to/target/bin +Deploy Neutron FWaaS's rootwrap configuration. + +Warning: Any existing rootwrap files at the specified etc path will be +removed by this script. + +Optional: set OS_SUDO_TESTING=1 to deploy the filters required by +Neutron's functional testing suite." + exit 1 +fi + +OS_SUDO_TESTING=${OS_SUDO_TESTING:-0} + +neutron_path=${OS_NEUTRON_PATH} +fwaas_path=$1 +target_etc_path=$2 +target_bin_path=$3 + +src_conf_path=${neutron_path}/etc +src_conf=${src_conf_path}/rootwrap.conf +src_rootwrap_path=${src_conf_path}/neutron/rootwrap.d + +fwaas_src_conf_path=${fwaas_path}/etc +fwaas_src_rootwrap_path=${fwaas_src_conf_path}/neutron/rootwrap.d + +dst_conf_path=${target_etc_path}/neutron +dst_conf=${dst_conf_path}/rootwrap.conf +dst_rootwrap_path=${dst_conf_path}/rootwrap.d + +if [[ -d "$dst_rootwrap_path" ]]; then + rm -rf ${dst_rootwrap_path} +fi +mkdir -p -m 755 ${dst_rootwrap_path} + +cp -p ${src_rootwrap_path}/* ${fwaas_src_rootwrap_path}/* ${dst_rootwrap_path}/ +cp -p ${src_conf} ${dst_conf} +sed -i "s:^filters_path=.*$:filters_path=${dst_rootwrap_path}:" ${dst_conf} +sed -i "s:^\(exec_dirs=.*\)$:\1,${target_bin_path}:" ${dst_conf} + +if [[ "$OS_SUDO_TESTING" = "1" ]]; then + sed -i 's/use_syslog=False/use_syslog=True/g' ${dst_conf} + sed -i 's/syslog_log_level=ERROR/syslog_log_level=DEBUG/g' ${dst_conf} + cp -p ${neutron_path}/neutron/tests/contrib/testing.filters \ + ${dst_rootwrap_path}/ + cp -p ${fwaas_path}/neutron_fwaas/tests/contrib/functional-testing.filters \ + ${dst_rootwrap_path}/ +fi diff --git a/tools/generate_config_file_samples.sh b/tools/generate_config_file_samples.sh new file mode 100755 index 000000000..6b0f4ec2e --- /dev/null +++ b/tools/generate_config_file_samples.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# +# 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. + +set -e + +GEN_CMD=oslo-config-generator + +if ! type "$GEN_CMD" > /dev/null; then + echo "ERROR: $GEN_CMD not installed on the system." + exit 1 +fi + +for file in `ls etc/oslo-config-generator/*`; do + $GEN_CMD --config-file=$file +done + +set -x diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..afcb46888 --- /dev/null +++ b/tox.ini @@ -0,0 +1,212 @@ +[tox] +envlist = py37,pep8,pylint +minversion = 3.1.1 +skipsdist = True + +[testenv] +basepython = python3 +setenv = VIRTUAL_ENV={envdir} + PYTHONWARNINGS=default::DeprecationWarning + OS_LOG_CAPTURE={env:OS_LOG_CAPTURE:true} + OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:true} + OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:true} +usedevelop = True +deps = -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +whitelist_externals = + sh + find +commands = + find . -type f -name "*.py[c|o]" -delete + find . -path "*/__pycache__*" -delete + stestr run {posargs} + +[testenv:common] +# Fake job to define environment variables shared between dsvm/non-dsvm jobs +setenv = OS_TEST_TIMEOUT=180 +commands = false + +[testenv:dsvm] +# Fake job to define environment variables shared between dsvm jobs +setenv = OS_SUDO_TESTING=1 + OS_ROOTWRAP_CMD=sudo {envdir}/bin/neutron-rootwrap {envdir}/etc/neutron/rootwrap.conf + OS_ROOTWRAP_DAEMON_CMD=sudo {envdir}/bin/neutron-rootwrap-daemon {envdir}/etc/neutron/rootwrap.conf + OS_FAIL_ON_MISSING_DEPS=1 + OS_LOG_PATH={env:OS_LOG_PATH:/opt/stack/logs} +commands = false + +[testenv:functional] +setenv = {[testenv]setenv} + {[testenv:common]setenv} + OS_TEST_PATH=./neutron_fwaas/tests/functional + OS_LOG_PATH={env:OS_LOG_PATH:/opt/stack/logs} +commands = + stestr run {posargs} + +[testenv:dsvm-fullstack] +setenv = {[testenv]setenv} + {[testenv:common]setenv} + {[testenv:dsvm]setenv} + OS_NEUTRON_PATH={env:OS_NEUTRON_PATH:/home/zuul/src/opendev.org/openstack/neutron} + # workaround for DB teardown lock contention (bug/1541742) + OS_TEST_TIMEOUT={env:OS_TEST_TIMEOUT:600} + OS_TEST_PATH=./neutron_fwaas/tests/fullstack +commands = + {toxinidir}/tools/deploy_rootwrap.sh {toxinidir} {envdir}/etc {envdir}/bin + stestr run --concurrency 4 {posargs} + + +[testenv:api] +sitepackages=True +setenv = + OS_TEST_PATH=./neutron_fwaas/tests/tempest_plugin/tests/api/ + OS_TESTR_CONCURRENCY=1 + TEMPEST_CONFIG_DIR={env:TEMPEST_CONFIG_DIR:/opt/stack/tempest/etc} +commands = + stestr run {posargs} + +[testenv:scenario] +sitepackages=True +setenv = + OS_TEST_PATH=./neutron_fwaas/tests/tempest_plugin/tests/scenario/ + OS_TESTR_CONCURRENCY=1 + TEMPEST_CONFIG_DIR={env:TEMPEST_CONFIG_DIR:/opt/stack/tempest/etc} +commands = + stestr run {posargs} + +[testenv:dsvm-functional] +setenv = + OS_TEST_PATH=./neutron_fwaas/tests/functional + OS_SUDO_TESTING=1 + OS_ROOTWRAP_CMD=sudo {envdir}/bin/neutron-rootwrap {envdir}/etc/neutron/rootwrap.conf + OS_ROOTWRAP_DAEMON_CMD=sudo {envdir}/bin/neutron-rootwrap-daemon {envdir}/etc/neutron/rootwrap.conf + OS_FAIL_ON_MISSING_DEPS=1 + OS_NEUTRON_PATH={env:OS_NEUTRON_PATH:/home/zuul/src/opendev.org/openstack/neutron} +whitelist_externals = + sh + cp + sudo +commands = + {toxinidir}/tools/deploy_rootwrap.sh {toxinidir} {envdir}/etc {envdir}/bin + stestr run {posargs} + +[testenv:releasenotes] +deps = -r{toxinidir}/doc/requirements.txt +commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + +[testenv:pep8] +commands = + flake8 + doc8 {posargs} + {toxinidir}/tools/check_unit_test_structure.sh + neutron-db-manage --subproject neutron-fwaas --database-connection sqlite:// check_migration + {[testenv:genconfig]commands} + {[testenv:genpolicy]commands} +whitelist_externals = sh + +[testenv:cover] +setenv = VIRTUAL_ENV={envdir} + LANGUAGE=en_US + PYTHON=coverage run --source neutron_fwaas --omit='*tests*' --parallel-mode +commands = + coverage erase + stestr run {posargs} + coverage combine + coverage report --skip-covered --omit='*test*' + coverage html -d cover + coverage xml -o cover/coverage.xml + +[testenv:venv] +commands = {posargs} +deps = -r{toxinidir}/doc/requirements.txt + +[testenv:docs] +deps = -r{toxinidir}/doc/requirements.txt +whitelist_externals = + rm +commands = + rm -rf doc/source/contributor/api + sphinx-build -W -b html doc/source doc/build/html + +[testenv:pdf-docs] +envdir = {toxworkdir}/docs +deps = {[testenv:docs]deps} +whitelist_externals = + rm + make +commands = + rm -rf doc/source/contributor/api + sphinx-build -W -b latex doc/source doc/build/pdf + make -C doc/build/pdf + +[doc8] +ignore = D000 +ignore-path = .venv,.git,.tox,.tmp,*neutron_fwaas/locale*,*lib/python*,neutron_fwaas.egg*,doc/build,releasenotes/*,doc/source/contributor/api,requirements.txt,test-requirements.txt + +[flake8] +# E125 continuation line does not distinguish itself from next logical line +# E126 continuation line over-indented for hanging indent +# E128 continuation line under-indented for visual indent +# E129 visually indented line with same indent as next logical line +# E265 block comment should start with '# ' +# H404 multi line docstring should start with a summary +# H405 multi line docstring summary not separated with an empty line +# TODO(dougwig) -- uncomment this to test for remaining linkages +# N530 direct neutron imports not allowed +# TODO(ihrachys) -- reenable N537 when new neutron-lib release is available +# H106: Do not put vim configuration in source files +# H203: Use assertIs(Not)None to check for None +# H204: Use assert(Not)Equal to check for equality +# H205: Use assert(Greater|Less)(Equal) for comparison +# H904: Delay string interpolations at logging calls +# N521: jsonutils.loads must be used instead of json.loads +# W504 line break after binary operator +# (W503 and W504 are incompatible and we need to choose one of them. +# Existing codes follows W503, so we disable W504.) +ignore = E125,E126,E128,E129,E265,H404,H405,N530,N521,W504 +enable-extensions=H106,H203,H204,H205,H904 +show-source = true +exclude = .venv,.git,.tox,dist,doc,*lib/python*,.tmp,*egg,build,tools,.ropeproject,rally-scenarios +import-order-style = pep8 + +[testenv:pylint] +deps = + {[testenv]deps} + pylint +commands = + pylint --rcfile=.pylintrc --output-format=colorized {posargs:neutron_fwaas} + +[hacking] +import_exceptions = neutron_fwaas._i18n +local-check-factory = neutron_lib.hacking.checks.factory + +[testenv:genconfig] +commands = {toxinidir}/tools/generate_config_file_samples.sh + +[testenv:genpolicy] +commands = oslopolicy-sample-generator --config-file=etc/oslo-policy-generator/policy.conf + +[testenv:lower-constraints] +deps = + -c{toxinidir}/lower-constraints.txt + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt + +[testenv:dev] +# run locally (not in the gate) using editable mode +# https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs +commands = + pip install -q -e "git+https://git.openstack.org/openstack/neutron#egg=neutron" + +[testenv:py3-dev] +commands = + {[testenv:dev]commands} + {[testenv]commands} + +[testenv:pep8-dev] +deps = + {[testenv]deps} +commands = + {[testenv:dev]commands} + {[testenv:pep8]commands}