-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
5 changed files
with
269 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
python/neutron-understack/neutron_understack/cisco_asa.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
python/neutron-understack/neutron_understack/l3_service_cisco_asa.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters