From e6050ad44bc68b7ec1e71190c989c1e2c61faa2b Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 19 Dec 2024 11:12:24 -0600 Subject: [PATCH 1/6] chore(ironic-understack): fix lint issues --- .../redfish_inspect_understack.py | 19 ++++++------------- .../ironic_understack/resource_class.py | 15 +++++++++------ 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/python/ironic-understack/ironic_understack/redfish_inspect_understack.py b/python/ironic-understack/ironic_understack/redfish_inspect_understack.py index dd575e505..63785e7e9 100644 --- a/python/ironic-understack/ironic_understack/redfish_inspect_understack.py +++ b/python/ironic-understack/ironic_understack/redfish_inspect_understack.py @@ -9,9 +9,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -""" -Redfish Inspect Interface modified for Understack -""" +"""Redfish Inspect Interface modified for Understack.""" import re @@ -23,10 +21,11 @@ from ironic.drivers.modules.inspect_utils import get_inspection_data from ironic.drivers.modules.redfish.inspect import RedfishInspect from ironic.drivers.redfish import RedfishHardware -from ironic_understack.conf import CONF from oslo_log import log from oslo_utils import units +from ironic_understack.conf import CONF + LOG = log.getLogger(__name__) FLAVORS = FlavorSpec.from_directory(CONF.ironic_understack.flavors_dir) LOG.info(f"Loaded {len(FLAVORS)} flavor specifications.") @@ -111,10 +110,8 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) patched_ifaces = RedfishHardware().supported_inspect_interfaces patched_ifaces.append(UnderstackDracRedfishInspect) - setattr( - RedfishHardware, - "supported_inspect_interfaces", - property(lambda _: patched_ifaces), + RedfishHardware.supported_inspect_interfaces = property( + lambda _: patched_ifaces ) @@ -123,8 +120,4 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) patched_ifaces = IDRACHardware().supported_inspect_interfaces patched_ifaces.append(UnderstackDracRedfishInspect) - setattr( - IDRACHardware, - "supported_inspect_interfaces", - property(lambda _: patched_ifaces), - ) + IDRACHardware.supported_inspect_interfaces = property(lambda _: patched_ifaces) diff --git a/python/ironic-understack/ironic_understack/resource_class.py b/python/ironic-understack/ironic_understack/resource_class.py index 3f5955c9f..08785789b 100644 --- a/python/ironic-understack/ironic_understack/resource_class.py +++ b/python/ironic-understack/ironic_understack/resource_class.py @@ -1,12 +1,14 @@ # from ironic.drivers.modules.inspector.hooks import base -from ironic.common import exception -from ironic.drivers.modules.inspector.hooks import base -from ironic_understack.conf import CONF +import re + from flavor_matcher.flavor_spec import FlavorSpec from flavor_matcher.machine import Machine from flavor_matcher.matcher import Matcher +from ironic.common import exception +from ironic.drivers.modules.inspector.hooks import base from oslo_log import log as logging -import re + +from ironic_understack.conf import CONF LOG = logging.getLogger(__name__) @@ -23,7 +25,6 @@ class UndercloudResourceClassHook(base.InspectionHook): def __call__(self, task, inventory, plugin_data): """Update node resource_class with deducted flavor.""" - try: memory_mb = inventory["memory"]["physical_mb"] disk_size_gb = int(int(inventory["disks"][0]["size"]) / 10**9) @@ -60,7 +61,9 @@ def __call__(self, task, inventory, plugin_data): f"Inventory has missing hardware information for node {task.node.uuid}." ) LOG.error(msg) - raise exception.InvalidNodeInventory(node=task.node.uuid, reason=msg) + raise exception.InvalidNodeInventory( + node=task.node.uuid, reason=msg + ) from None except NoMatchError: msg = f"No matching flavor found for {task.node.uuid}" LOG.error(msg) From 89d818127aa9de17650e3a4a159e277d6510ff17 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 19 Dec 2024 11:24:36 -0600 Subject: [PATCH 2/6] chore(neutron-understack): fix lint issues --- .../neutron_understack/neutron_understack_mech.py | 10 ++++------ .../tests/test_neutron_understack_mech.py | 3 ++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/python/neutron-understack/neutron_understack/neutron_understack_mech.py b/python/neutron-understack/neutron_understack/neutron_understack_mech.py index 1a595d870..8abb5a02c 100644 --- a/python/neutron-understack/neutron_understack/neutron_understack_mech.py +++ b/python/neutron-understack/neutron_understack/neutron_understack_mech.py @@ -6,12 +6,10 @@ import neutron_lib.api.definitions.portbindings as portbindings from neutron_lib import constants as p_const from neutron_lib.plugins.ml2 import api -from neutron_lib.plugins.ml2.api import ( - MechanismDriver, - NetworkContext, - PortContext, - SubnetContext, -) +from neutron_lib.plugins.ml2.api import MechanismDriver +from neutron_lib.plugins.ml2.api import NetworkContext +from neutron_lib.plugins.ml2.api import PortContext +from neutron_lib.plugins.ml2.api import SubnetContext from oslo_config import cfg from neutron_understack import config diff --git a/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py b/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py index 6f632a870..b646b965a 100644 --- a/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py +++ b/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py @@ -1,5 +1,6 @@ import json -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock +from unittest.mock import patch import pytest From 5020fd8874ba6c4b868f589e25ee2de0c444975f Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 19 Dec 2024 11:32:54 -0600 Subject: [PATCH 3/6] chore(understack-flavor-matcher): fix lint issues --- .../flavor_matcher/flavor_spec.py | 11 +++++++---- .../flavor_matcher/matcher.py | 14 ++++++-------- .../tests/test_flavor_spec.py | 6 ++++-- .../tests/test_matcher.py | 1 + 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py index b79699082..cf16c3452 100644 --- a/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py +++ b/python/understack-flavor-matcher/flavor_matcher/flavor_spec.py @@ -3,6 +3,7 @@ from dataclasses import dataclass import yaml + from flavor_matcher.machine import Machine @@ -50,7 +51,7 @@ def baremetal_nova_resource_class(self): @property def memory_mib(self): - """Returns memory size in MiB""" + """Returns memory size in MiB.""" return self.memory_gb * 1024 @staticmethod @@ -61,7 +62,7 @@ def from_directory(directory: str = "/etc/flavors/") -> list["FlavorSpec"]: if filename.endswith(".yaml") or filename.endswith(".yml"): filepath = os.path.join(root, filename) try: - with open(filepath, "r") as file: + with open(filepath) as file: yaml_content = file.read() flavor_spec = FlavorSpec.from_yaml(yaml_content) flavor_specs.append(flavor_spec) @@ -97,11 +98,13 @@ def score_machine(self, machine: Machine): ): return 100 - # Rule 2: If machine has less memory than specified in the flavor, it cannot be used + # Rule 2: If machine has less memory than specified in the + # flavor, it cannot be used if machine.memory_gb < self.memory_gb: return 0 - # Rule 3: If machine has smaller disk than specified in the flavor, it cannot be used + # Rule 3: If machine has smaller disk than specified in the + # flavor, it cannot be used if any(machine.disk_gb < drive for drive in self.drives): return 0 diff --git a/python/understack-flavor-matcher/flavor_matcher/matcher.py b/python/understack-flavor-matcher/flavor_matcher/matcher.py index 5e009f31b..fac5be3be 100644 --- a/python/understack-flavor-matcher/flavor_matcher/matcher.py +++ b/python/understack-flavor-matcher/flavor_matcher/matcher.py @@ -1,5 +1,5 @@ -from flavor_matcher.machine import Machine from flavor_matcher.flavor_spec import FlavorSpec +from flavor_matcher.machine import Machine class Matcher: @@ -7,9 +7,7 @@ def __init__(self, flavors: list[FlavorSpec]): self.flavors = flavors def match(self, machine: Machine) -> list[FlavorSpec]: - """ - Find list of all flavors that the machine is eligible for. - """ + """Find list of all flavors that the machine is eligible for.""" results = [] for flavor in self.flavors: score = flavor.score_machine(machine) @@ -18,11 +16,11 @@ def match(self, machine: Machine) -> list[FlavorSpec]: return results def pick_best_flavor(self, machine: Machine) -> FlavorSpec | None: - """ - Obtains list of all flavors that particular machine can be classified - as, then tries to select "the best" one. - """ + """Selects the best patching flavor. + Obtains list of all flavors that particular machine can be classified as, + then tries to select "the best" one. + """ possible = self.match(machine) if len(possible) == 0: diff --git a/python/understack-flavor-matcher/tests/test_flavor_spec.py b/python/understack-flavor-matcher/tests/test_flavor_spec.py index f0b5af755..f158f6bd8 100644 --- a/python/understack-flavor-matcher/tests/test_flavor_spec.py +++ b/python/understack-flavor-matcher/tests/test_flavor_spec.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest + from flavor_matcher.flavor_spec import FlavorSpec from flavor_matcher.machine import Machine @@ -56,7 +57,7 @@ def test_from_yaml(valid_yaml): def test_from_yaml_invalid(invalid_yaml): - with pytest.raises(Exception): + with pytest.raises(Exception): # noqa:B017 FlavorSpec.from_yaml(invalid_yaml) @@ -312,7 +313,8 @@ def test_cpu_model_not_exact_but_memory_and_disk_match(flavors): def test_large_flavor_memory_slightly_less_disk_exact(flavors): - # Machine with slightly less memory than required for the medium flavor, exact disk space + # Machine with slightly less memory than required for the medium + # flavor, exact disk space machine = Machine( memory_mb=204600, cpu="Intel 80386DX", disk_gb=1800, model="Dell XPS1319" ) diff --git a/python/understack-flavor-matcher/tests/test_matcher.py b/python/understack-flavor-matcher/tests/test_matcher.py index 452008849..fd0189e7e 100644 --- a/python/understack-flavor-matcher/tests/test_matcher.py +++ b/python/understack-flavor-matcher/tests/test_matcher.py @@ -1,4 +1,5 @@ import pytest + from flavor_matcher.flavor_spec import FlavorSpec from flavor_matcher.machine import Machine from flavor_matcher.matcher import Matcher From 4ffb090c8f7eba1815611fbbcf98f960b60ab9f2 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 19 Dec 2024 11:02:55 -0600 Subject: [PATCH 4/6] chore(python): get our ruff rules in sync Get all of our ruff rules in sync between our projects. Moved some other sections to line up the same. Got rid of the old isort we don't use. --- python/ironic-understack/pyproject.toml | 37 ++++++++++++-- python/neutron-understack/pyproject.toml | 16 ++++--- .../understack-flavor-matcher/pyproject.toml | 48 +++++++++++++++++-- python/understack-workflows/pyproject.toml | 4 +- 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/python/ironic-understack/pyproject.toml b/python/ironic-understack/pyproject.toml index 65a2ed6d5..11810bae6 100644 --- a/python/ironic-understack/pyproject.toml +++ b/python/ironic-understack/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + [tool.poetry] name = "ironic-understack" version = "0.0.0" @@ -10,8 +14,9 @@ packages = [ ] [tool.poetry.dependencies] -ironic = ">=24.1" +# keep this python field in sync with the tool.ruff.target-version python = "^3.10" +ironic = ">=24.1" pyyaml = "^6.0" understack-flavor-matcher = {path = "../understack-flavor-matcher"} @@ -20,10 +25,6 @@ pytest = "^8.3.2" pytest-github-actions-annotate-failures = "*" pytest-cov = "^5.0.0" -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - [tool.poetry.plugins."ironic.inspection.hooks"] "resource-class" = "ironic_understack.resource_class:UndercloudResourceClassHook" @@ -44,9 +45,35 @@ fix = true [tool.ruff.lint] select = [ + "D", # pydocstyle + "E", # pycodestyle (error) + "F", # pyflakes + "B", # flake8-bugbear + "I", # isort "S", # flake8-bandit + "UP", # pyupgrade + "ASYNC", # flake8-async +] + +ignore = [ + "D100", # don't require docs for every module + "D101", # don't require docs for every class + "D102", # don't require docs for every class method + "D103", # don't require docs for every function + "D104", # don't require docs for every package + "D106", # don't require docs for every nested class + "D107", # don't require docs for __init__ + "D417" # don't require docs for every function parameter ] +[tool.ruff.lint.isort] +force-single-line = true + + +[tool.ruff.lint.pydocstyle] +# enable the google doc style rules by default +convention = "google" + [tool.ruff.lint.per-file-ignores] "ironic_understack/tests/*.py" = [ "S101", # allow 'assert' for pytest diff --git a/python/neutron-understack/pyproject.toml b/python/neutron-understack/pyproject.toml index f9299a3ff..1887ae5f6 100644 --- a/python/neutron-understack/pyproject.toml +++ b/python/neutron-understack/pyproject.toml @@ -23,11 +23,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", ] -[tool.ruff] -target-version = "py310" -fix = true - [tool.poetry.dependencies] +# keep this python field in sync with the tool.ruff.target-version python = "^3.10" requests = "^2" neutron-lib = "^3" @@ -42,9 +39,9 @@ minversion = "6.0" addopts = "-ra" filterwarnings = "ignore::DeprecationWarning" - -[tool.isort] -profile = "open_stack" +[tool.ruff] +target-version = "py310" +fix = true [tool.ruff.lint] select = [ @@ -64,9 +61,14 @@ ignore = [ "D102", # don't require docs for every class method "D103", # don't require docs for every function "D104", # don't require docs for every package + "D106", # don't require docs for every nested class + "D107", # don't require docs for __init__ "D417" # don't require docs for every function parameter ] +[tool.ruff.lint.isort] +force-single-line = true + [tool.ruff.lint.pydocstyle] # enable the google doc style rules by default convention = "google" diff --git a/python/understack-flavor-matcher/pyproject.toml b/python/understack-flavor-matcher/pyproject.toml index 09a9a91e7..5eb1fd2b1 100644 --- a/python/understack-flavor-matcher/pyproject.toml +++ b/python/understack-flavor-matcher/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + [tool.poetry] name = "understack-flavor-matcher" version = "0.0.0" @@ -10,6 +14,7 @@ packages = [ ] [tool.poetry.dependencies] +# keep this python field in sync with the tool.ruff.target-version python = "^3.10" pyyaml = "^6.0" @@ -18,13 +23,48 @@ pytest = "^8.3.2" pytest-github-actions-annotate-failures = "*" pytest-cov = "^5.0.0" -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - [tool.pytest.ini_options] minversion = "6.0" addopts = "-ra --cov=flavor_matcher" testpaths = [ "tests", ] + +[tool.ruff] +target-version = "py310" +fix = true + +[tool.ruff.lint] +select = [ + "D", # pydocstyle + "E", # pycodestyle (error) + "F", # pyflakes + "B", # flake8-bugbear + "I", # isort + "S", # flake8-bandit + "UP", # pyupgrade + "ASYNC", # flake8-async +] + +ignore = [ + "D100", # don't require docs for every module + "D101", # don't require docs for every class + "D102", # don't require docs for every class method + "D103", # don't require docs for every function + "D104", # don't require docs for every package + "D106", # don't require docs for every nested class + "D107", # don't require docs for __init__ + "D417" # don't require docs for every function parameter +] + +[tool.ruff.lint.isort] +force-single-line = true + +[tool.ruff.lint.pydocstyle] +# enable the google doc style rules by default +convention = "google" + +[tool.ruff.lint.per-file-ignores] +"tests/**/*.py" = [ + "S101", # allow 'assert' for pytest +] diff --git a/python/understack-workflows/pyproject.toml b/python/understack-workflows/pyproject.toml index 231e65f05..ca0d4ac29 100644 --- a/python/understack-workflows/pyproject.toml +++ b/python/understack-workflows/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ ] [tool.poetry.dependencies] +# keep this python field in sync with the tool.ruff.target-version python = "~3.11.0" pydantic = "^2" @@ -66,9 +67,6 @@ filterwarnings = [ target-version = "py311" fix = true -[tool.isort] -profile = "open_stack" - [tool.ruff.lint] select = [ "D", # pydocstyle From de5c2e1bbc01d6be3fb9de6514f5a22a6bf5356c Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 12 Dec 2024 19:31:27 -0600 Subject: [PATCH 5/6] feat(neutron): add l3 service plugin for the ASAs Rough L3 service plugin to let us communicate to the ASAs. https://docs.openstack.org/neutron/latest/admin/config-router-flavor-ovn.html Adds the ability to associate and disassociate floating IPs to Cisco ASA given: - the management interface is accessible - the external network that has a description with `mgmt=$IP:$PORT` of the Cisco ASA - an interface on the Cisco ASA which has it's `nameif` set to the network UUID of the internal network being attached --- components/neutron/aio-values.yaml | 2 + .../neutron_understack/cisco_asa.py | 108 ++++++++++++++ .../neutron_understack/config.py | 25 ++++ .../l3_service_cisco_asa.py | 135 ++++++++++++++++++ python/neutron-understack/pyproject.toml | 1 + 5 files changed, 271 insertions(+) create mode 100644 python/neutron-understack/neutron_understack/cisco_asa.py create mode 100644 python/neutron-understack/neutron_understack/l3_service_cisco_asa.py diff --git a/components/neutron/aio-values.yaml b/components/neutron/aio-values.yaml index 5e8533d6e..9b6a70ed0 100644 --- a/components/neutron/aio-values.yaml +++ b/components/neutron/aio-values.yaml @@ -58,6 +58,8 @@ conf: # we aren't using availability zones so having calls attempt to add things to # availability zones won't work. default_availability_zones: "" + service_providers: + service_provider: "L3_ROUTER_NAT:cisco-asa:neutron_understack.l3_service_cisco_asa.CiscoAsa" # disable the neutron-ironic-agent from loading a non-existent config pod: diff --git a/python/neutron-understack/neutron_understack/cisco_asa.py b/python/neutron-understack/neutron_understack/cisco_asa.py new file mode 100644 index 000000000..539fde414 --- /dev/null +++ b/python/neutron-understack/neutron_understack/cisco_asa.py @@ -0,0 +1,108 @@ +# Copyright (c) 2024 Rackspace Technology +# 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. + +# This aims to provide a basic abstraction of Cisco ASA ASDM commands +# needed to support a basic router and floating IP. Anything more +# should really use ntc-templates + +import ssl +from urllib.parse import quote_plus + +import requests +import urllib3 +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +class _CustomHttpAdapter(requests.adapters.HTTPAdapter): + """Custom adapter for bad ASA SSL.""" + + def __init__(self, ssl_context=None, **kwargs): + """Init to match requests HTTPAdapter.""" + self.ssl_context = ssl_context + super().__init__(**kwargs) + + def init_poolmanager(self, connections, maxsize, block=False): + self.poolmanager = urllib3.poolmanager.PoolManager( + num_pools=connections, + maxsize=maxsize, + block=block, + ssl_context=self.ssl_context, + ) + + +def _get_legacy_session(): + """Support bad ASA SSL.""" + ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + ctx.check_hostname = False + ctx.options |= 0x4 # OP_LEGACY_SERVER_CONNECT + session = requests.session() + session.mount("https://", _CustomHttpAdapter(ctx)) + return session + + +def _cmd_str(cmds: list[str]) -> str: + """Handles encoding of a list of commands to the URL string.""" + encoded_cmds = [quote_plus(cmd) for cmd in cmds] + return "/".join(encoded_cmds) + + +class CiscoAsaAsdm: + def __init__( + self, mgmt_url: str, username: str, password: str, user_agent: str + ) -> None: + self.mgmt_url = mgmt_url + self.s = _get_legacy_session() + self.s.headers.update({"User-Agent": user_agent}) + self.s.auth = requests.auth.HTTPBasicAuth(username, password) + self.s.verify = False # these things are gross + + def _make_url(self, cmd_str: str) -> str: + return f"{self.mgmt_url}/admin/exec/{cmd_str}" + + def _make_request(self, op: str, cmds: list[str]) -> bool: + url = self._make_url(_cmd_str(cmds)) + LOG.debug("Cisco ASA ASDM request(%s): %s", op, url) + try: + r = self.s.get(url, timeout=20) + except Exception: + LOG.exception("Failed on %s", url) + return False + + LOG.debug("ASA response: %d / %s", r.status_code, r.text) + return True + + def create_nat( + self, + float_ip_addr: str, + asa_outside_inf: str, + inside_ip_addr: str, + asa_inside_inf: str, + ) -> bool: + cmds = [ + f"object network OBJ-{inside_ip_addr}", + f"host {inside_ip_addr}", + f"nat ({asa_inside_inf},{asa_outside_inf}) static {float_ip_addr}", + ] + + return self._make_request("create_nat", cmds) + + def delete_nat(self, inside_ip_addr: str) -> bool: + cmds = [ + f"no object network OBJ-{inside_ip_addr}", + ] + + return self._make_request("delete_nat", cmds) diff --git a/python/neutron-understack/neutron_understack/config.py b/python/neutron-understack/neutron_understack/config.py index 75c68f68d..6d4749d97 100644 --- a/python/neutron-understack/neutron_understack/config.py +++ b/python/neutron-understack/neutron_understack/config.py @@ -45,6 +45,27 @@ ), ] +l3_svc_cisco_asa_opts = [ + cfg.StrOpt( + "user_agent", + help="User-Agent for requests to Cisco ASA", + default="ASDM", + ), + cfg.StrOpt( + "username", + help="username for requests to the Cisco ASA", + ), + cfg.StrOpt( + "password", + help="password for requests to the Cisco ASA", + ), + cfg.StrOpt( + "outside_interface", + help="ASA interface for outside connections", + default="OUTSIDE", + ), +] + def register_ml2_type_understack_opts(config): config.register_opts(type_understack_opts, "ml2_type_understack") @@ -52,3 +73,7 @@ def register_ml2_type_understack_opts(config): def register_ml2_understack_opts(config): config.register_opts(mech_understack_opts, "ml2_understack") + + +def register_l3_svc_cisco_asa_opts(config): + config.register_opts(l3_svc_cisco_asa_opts, "l3_service_cisco_asa") diff --git a/python/neutron-understack/neutron_understack/l3_service_cisco_asa.py b/python/neutron-understack/neutron_understack/l3_service_cisco_asa.py new file mode 100644 index 000000000..32b82a45e --- /dev/null +++ b/python/neutron-understack/neutron_understack/l3_service_cisco_asa.py @@ -0,0 +1,135 @@ +# inspired from +# https://docs.openstack.org/neutron/latest/admin/config-router-flavor-ovn.html + +from neutron.services.l3_router.service_providers import base +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib.plugins import directory +from oslo_config import cfg +from oslo_log import log as logging + +from neutron_understack import config +from neutron_understack.cisco_asa import CiscoAsaAsdm + +LOG = logging.getLogger(__name__) +config.register_l3_svc_cisco_asa_opts(cfg.CONF) + + +@registry.has_registry_receivers +class CiscoAsa(base.L3ServiceProvider): + use_integrated_agent_scheduler = True + + def __init__(self, l3plugin): + super().__init__(l3plugin) + self.core_plugin = directory.get_plugin() + + @registry.receives(resources.ROUTER_INTERFACE, [events.AFTER_CREATE]) + def _process_router_interface_create(self, resource, event, trigger, payload): + router = payload.states[0] + context = payload.context + port = payload.metadata["port"] + subnets = payload.metadata["subnets"] + LOG.debug( + "router_interface_create1 %s / %s / %s / %s", router, context, port, subnets + ) + LOG.debug( + "router_interface_create2 %s / %s / %s / %s", + resource, + event, + trigger, + payload, + ) + + @registry.receives(resources.FLOATING_IP, [events.AFTER_CREATE]) + def _process_floatingip_create(self, resource, event, trigger, payload): + LOG.debug( + "floatingip_create %s / %s / %s / %s", resource, event, trigger, payload + ) + + @registry.receives(resources.FLOATING_IP, [events.AFTER_UPDATE]) + def _process_floatingip_update(self, resource, event, trigger, payload): + conf = cfg.CONF.l3_service_cisco_asa + + # read the state, state[0] is previous and state[1] is current + context = payload.context + # are we associating (True) or disassociating (False) + assoc_disassoc = payload.metadata["association_event"] + # associating we want the current state while disassociating + # we want the previous + fip = payload.states[1] if assoc_disassoc else payload.states[0] + + # what is the floating IP we are trying to use + float_ip_addr = fip["floating_ip_address"] + # what is the router ID + router_id = fip["router_id"] + # inside IP + inside_ip_addr = fip["fixed_ip_address"] + inside_port_info = fip["port_details"] + asa_inside_inf = None + asa_outside_inf = conf.outside_interface + if inside_port_info: + # we will use the UUID of the network as our internal interface name + asa_inside_inf = inside_port_info["network_id"] + + # Since our network blocks need to be routed to the firewalls + # explicitly we'll store information about which firewall in the + # floating IP's network rather than the router object. The real + # behavior should be that the router object maps to the firewall + # but in this case the network is likely more correct. Plus + # the network is 'external' and cannot be mucked with by a + # normal user. + LOG.debug( + "Looking up floating IP's network (%s) description", + fip["floating_network_id"], + ) + if not fip["floating_network_id"]: + return + try: + float_ip_net = self.core_plugin.get_network( + context, fip["floating_network_id"], fields=["description"] + ) + except Exception: + LOG.exception( + "Unable to lookup floating IP's network %s", fip["floating_network_id"] + ) + return + + try: + asa_mgmt = float_ip_net["description"].split("=")[-1] + except Exception: + LOG.exception( + "Unable to parse firewall mgmt IP and port from floating IP " + "network description" + ) + return + + action_msg = "associate" if assoc_disassoc else "disassociate" + + LOG.debug( + "Request to %s floating IP %s via router %s/%s/%s to %s on %s", + action_msg, + float_ip_addr, + router_id, + asa_mgmt, + asa_outside_inf, + inside_ip_addr, + asa_inside_inf, + ) + + if asa_mgmt and asa_inside_inf and inside_ip_addr and float_ip_addr: + asa = CiscoAsaAsdm( + f"https://{asa_mgmt}", conf.username, conf.password, conf.user_agent + ) + if assoc_disassoc: + ret = asa.create_nat( + float_ip_addr, asa_outside_inf, inside_ip_addr, asa_inside_inf + ) + else: + ret = asa.delete_nat(inside_ip_addr) + + if not ret: + LOG.error( + "Unable to make change on ASA device for router %s", + fip["router_id"], + ) diff --git a/python/neutron-understack/pyproject.toml b/python/neutron-understack/pyproject.toml index 1887ae5f6..38991326a 100644 --- a/python/neutron-understack/pyproject.toml +++ b/python/neutron-understack/pyproject.toml @@ -87,3 +87,4 @@ understack_vxlan = "neutron_understack.type_understack_vxlan:UnderstackVxlanType [tool.poetry.plugins."neutron.service_plugins"] l3_understack = "neutron_understack.l3_service_plugin:UnderStackL3ServicePlugin" +l3_service_cisco_asa = "neutron_understack.l3_service_cisco_asa:CiscoAsa" From a627a21699742e79de155ab465186794d0a9fe70 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 19 Dec 2024 10:59:30 -0600 Subject: [PATCH 6/6] DNM: use PR container --- components/openstack-2024.2-jammy.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/components/openstack-2024.2-jammy.yaml b/components/openstack-2024.2-jammy.yaml index 70a307465..6f522639d 100644 --- a/components/openstack-2024.2-jammy.yaml +++ b/components/openstack-2024.2-jammy.yaml @@ -35,18 +35,18 @@ images: ironic_retrive_swift_config: "docker.io/openstackhelm/heat:2024.2-ubuntu_jammy" # neutron - neutron_db_sync: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_dhcp: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_l3: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_l2gw: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_linuxbridge_agent: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_metadata: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_ovn_metadata: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_openvswitch_agent: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_server: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_rpc_server: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_bagpipe_bgp: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" - neutron_netns_cleanup_cron: "ghcr.io/rackerlabs/understack/neutron:2024.2-ubuntu_jammy" + neutron_db_sync: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_dhcp: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_l3: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_l2gw: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_linuxbridge_agent: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_metadata: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_ovn_metadata: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_openvswitch_agent: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_server: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_rpc_server: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_bagpipe_bgp: "ghcr.io/rackerlabs/understack/neutron:pr-554" + neutron_netns_cleanup_cron: "ghcr.io/rackerlabs/understack/neutron:pr-554" # nova nova_api: "docker.io/openstackhelm/nova:2024.2-ubuntu_jammy"