Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(neutron): experimental service plugin for Cisco ASA #554

Merged
merged 6 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions components/neutron/aio-values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
24 changes: 12 additions & 12 deletions components/openstack-2024.2-jammy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.")
Expand Down Expand Up @@ -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
)


Expand All @@ -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)
15 changes: 9 additions & 6 deletions python/ironic-understack/ironic_understack/resource_class.py
Original file line number Diff line number Diff line change
@@ -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__)

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
37 changes: 32 additions & 5 deletions python/ironic-understack/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"}

Expand All @@ -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"

Expand All @@ -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
Expand Down
108 changes: 108 additions & 0 deletions python/neutron-understack/neutron_understack/cisco_asa.py
Original file line number Diff line number Diff line change
@@ -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)
25 changes: 25 additions & 0 deletions python/neutron-understack/neutron_understack/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,35 @@
),
]

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


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")
Loading
Loading