Skip to content

Commit

Permalink
feat(neutron): add l3 service plugin for the ASAs
Browse files Browse the repository at this point in the history
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
  • Loading branch information
cardoe committed Dec 19, 2024
1 parent 961c2c4 commit 85d297d
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 0 deletions.
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
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)
24 changes: 24 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,34 @@
),
]

l3_svc_cisco_asa_opts = [
cfg.StrOpt(
"user_agent",
help="User-Agent for requests to Cisco ASA",
),
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")
133 changes: 133 additions & 0 deletions python/neutron-understack/neutron_understack/l3_service_cisco_asa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 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, registry, 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"],
)
2 changes: 2 additions & 0 deletions python/neutron-understack/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ 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
"D107", # don't require docs for all __init__'s
"D417" # don't require docs for every function parameter
]

Expand All @@ -85,3 +86,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"

0 comments on commit 85d297d

Please sign in to comment.