diff --git a/net/pfSense-pkg-ocicarp/Makefile b/net/pfSense-pkg-ocicarp/Makefile new file mode 100644 index 000000000000..59a71ebd14eb --- /dev/null +++ b/net/pfSense-pkg-ocicarp/Makefile @@ -0,0 +1,58 @@ +PORTNAME= pfSense-pkg-ocicarp +PORTVERSION= 0.1 +PORTREVISION= 1 +CATEGORIES= net +MASTER_SITES= # empty +DISTFILES= # empty +EXTRACT_ONLY= # empty + +MAINTAINER= ce_coe_infra_code_assist_ww_grp@oracle.com +COMMENT= pfSense package Oracle Cloud Infrastructure CARP + +LICENSE= APACHE20 + +RUN_DEPENDS= oci:devel/oci-cli + +USES= python:3.9-3.11 shebangfix + +NO_BUILD= yes +NO_MTREE= yes + +SUB_FILES= pkg-install pkg-deinstall +SUB_LIST= PORTNAME=${PORTNAME} + +SHEBANG_FILES= ${FILESDIR}${PREFIX}/bin/ocicarp.py + +do-extract: + ${MKDIR} ${WRKSRC} + +do-install: + ${MKDIR} ${STAGEDIR}/etc/inc/priv + ${MKDIR} ${STAGEDIR}${PREFIX}/bin + ${MKDIR} ${STAGEDIR}${PREFIX}/pkg/ocicarp + ${MKDIR} ${STAGEDIR}${PREFIX}/www/shortcuts + ${MKDIR} ${STAGEDIR}${DATADIR} + + ${INSTALL_DATA} ${FILESDIR}/etc/inc/priv/ocicarp.priv.inc \ + ${STAGEDIR}/etc/inc/priv + ${INSTALL_SCRIPT} ${FILESDIR}${PREFIX}/bin/ocicarp.py \ + ${STAGEDIR}${PREFIX}/bin + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/ocicarp.xml \ + ${STAGEDIR}${PREFIX}/pkg + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/pkg/ocicarp/ocicarp.inc \ + ${STAGEDIR}${PREFIX}/pkg/ocicarp + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/www/ocicarp_settings.php \ + ${STAGEDIR}${PREFIX}/www + ${INSTALL_DATA} ${FILESDIR}${PREFIX}/www/shortcuts/pkg_ocicarp.inc \ + ${STAGEDIR}${PREFIX}/www/shortcuts + ${INSTALL_DATA} ${FILESDIR}${DATADIR}/info.xml \ + ${STAGEDIR}${DATADIR} + + @${REINPLACE_CMD} -i '' -e "s|%%PKGVERSION%%|${PKGVERSION}|" \ + ${STAGEDIR}${DATADIR}/info.xml + +post-install: + ${MKDIR} ${STAGEDIR}/usr/local/etc/devd + ${MKDIR} ${STAGEDIR}/usr/local/etc/ocicarp + +.include diff --git a/net/pfSense-pkg-ocicarp/files/etc/inc/priv/ocicarp.priv.inc b/net/pfSense-pkg-ocicarp/files/etc/inc/priv/ocicarp.priv.inc new file mode 100644 index 000000000000..3bbdfa453207 --- /dev/null +++ b/net/pfSense-pkg-ocicarp/files/etc/inc/priv/ocicarp.priv.inc @@ -0,0 +1,30 @@ + diff --git a/net/pfSense-pkg-ocicarp/files/pkg-deinstall.in b/net/pfSense-pkg-ocicarp/files/pkg-deinstall.in new file mode 100644 index 000000000000..83d1c3dcab9e --- /dev/null +++ b/net/pfSense-pkg-ocicarp/files/pkg-deinstall.in @@ -0,0 +1,3 @@ +#!/bin/sh + +/usr/local/bin/php -f /etc/rc.packages %%PORTNAME%% ${2} diff --git a/net/pfSense-pkg-ocicarp/files/pkg-install.in b/net/pfSense-pkg-ocicarp/files/pkg-install.in new file mode 100644 index 000000000000..c1cf1e1f21c8 --- /dev/null +++ b/net/pfSense-pkg-ocicarp/files/pkg-install.in @@ -0,0 +1,7 @@ +#!/bin/sh + +if [ "${2}" != 'POST-INSTALL' ]; then + exit 0 +fi + +${PKG_ROOTDIR}/usr/local/bin/php -f ${PKG_ROOTDIR}/etc/rc.packages %%PORTNAME%% ${2} diff --git a/net/pfSense-pkg-ocicarp/files/usr/local/bin/ocicarp.py b/net/pfSense-pkg-ocicarp/files/usr/local/bin/ocicarp.py new file mode 100644 index 000000000000..620cdc5a6b76 --- /dev/null +++ b/net/pfSense-pkg-ocicarp/files/usr/local/bin/ocicarp.py @@ -0,0 +1,308 @@ +#!/bin/python3 +# +# coding: utf-8 +# +# part of pfSense (https://www.pfsense.org) +# Copyright (c) 2023, Oracle and/or its affiliates. +# +# 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 oci + +import argparse +import json +import logging +import logging.handlers +from http import HTTPStatus +from platform import system +from typing import Any, TextIO + + +""" +Return codes: + 0 success + 1 set up failure + 2 JSON file read failure + 10 API call issue(s) +""" +RET_SUCCESS = 0 +RET_SETUP_FAIL = 1 +RET_FILE_FAIL = 2 +RET_API_FAIL = 10 + + +def _init_logging() -> None: + # Grab logger, set level and create formatter. + logger = logging.getLogger() + logger.setLevel(logging.INFO) + formatter = logging.Formatter('ocicarp.py %(message)s') + + # Add a console handler and set its formatter. + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + os_system: str = system() + log_address: str = '' + if os_system == 'Linux': + log_address = '/dev/log' + elif os_system =='FreeBSD': + log_address = '/var/run/log' + + # Optionally add a syslog handler (FreeBSD / Linux). + if log_address: + syslog_handler = logging.handlers.SysLogHandler(address=log_address) + syslog_handler.setFormatter(formatter) + logger.addHandler(syslog_handler) + + +def reassign_v4ip(vn_client: oci.core.VirtualNetworkClient, vnic: str, ocid: str) -> tuple[int, str]: + """ + Invokes OCI API to relocate an IP v4 secondary IP address. + + Parameters: + vn_client (VirtualNetworkClient): The virtual network client object. + vnic (str): The OCID of the vNIC to assign the IP to. + vocid (str): The OCID of the IP to assign to the vNIC. + + Returns: + int: The HTTP return code of the call. + str: The result of the call. + """ + + ret_data: str = '' + ret_code: int = -1 + try: + response: oci.core.modles.Response = vn_client.update_private_ip(ocid, + oci.core.models.UpdatePrivateIpDetails( + vnic_id=vnic + ) + ) + ret_code = response.status + ret_data = response.data + + except oci.exceptions.ServiceError as e: + ret_code = -1 + ret_data = str(e) + + return ret_code, ret_data + + +def reassign_v6ip(vn_client: oci.core.VirtualNetworkClient, vnic: str, ocid: str) -> tuple[int, str]: + """ + Invokes OCI API to relocate an IP v6 secondary IP address. + + Parameters: + vn_client (VirtualNetworkClient): The virtual network client object. + vnic (str): The OCID of the vNIC to assign the IP to. + vocid (str): The OCID of the IP to assign to the vNIC. + + Returns: + int: The HTTP return code of the call. + str: The result of the call. + """ + + ret_data: str = '' + ret_code: int = -1 + try: + response: oci.core.modles.Response = vn_client.update_ipv6(ocid, + oci.core.models.UpdateIpv6Details( + vnic_id=vnic + ) + ) + ret_code = response.status + ret_data = response.data + + except oci.exceptions.ServiceError as e: + ret_code = -1 + ret_data = str(e) + + return ret_code, ret_data + + +def process_json(args: argparse.Namespace) -> tuple[int, list[str]]: + """ + Processes the provided JSON file and attempt relocation of IP addresses + to vNICs. The structure of the JSON is expected to be: + { + "vnic-ocid-here": { + "ipv4": [ + "ipv4-ocid-here", + ... + ], + "ipv6": [ + "ipv6-ocid-here", + ... + ] + }, + ... + } + + Parameters: + args (Namespace): All command-line arguements. + + Returns: + int: Script return code. + str[]: List of messages to return. + """ + + verbose: bool = args.verbose + vnic_ips: dict[str, dict[str, list[str]]] = {} + try: + json_file: TextIO = open(args.json_file) + vnic_ips = json.load(json_file) + json_file.close + except json.JSONDecodeError as jde: + logging.error(f"Problem loading '{args.json_file}': {jde}") + raise SystemExit(RET_FILE_FAIL) + except FileNotFoundError as fnf: + logging.error(fnf) + raise SystemExit(RET_FILE_FAIL) + except Exception as e: + logging.exception('Encountered an unexpected exception reading JSON file') + raise SystemExit(RET_FILE_FAIL) + + # Specifying to use a profile is an options intended mostly for manyal + # execution and troubleshooting/debugging; the intention of this script is + # to primarily be run using instance principals. Using a profile (and + # thus local API keys) is *slightly* faster than using an instance + # principal, though it is far less convenient. + # config: type[dict[Any, Any]] = dict[Any, Any] + config: dict[str, Any] = {} + if args.use_profile: + try: + config = oci.config.from_file(file_location=args.config_file, profile_name=args.profile_name) + oci.config.validate_config(config) + except oci.exceptions.ConfigFileNotFound as cfnf: + logging.error(f"Unable to locate configuration '{args.config_file}'") + raise SystemExit(RET_SETUP_FAIL) + except oci.exceptions.ProfileNotFound as pnf: + logging.error(f"Unable to locate profile '{args.profile_name}'") + raise SystemExit(RET_SETUP_FAIL) + except Exception as e: + logging.exception(f"Encountered an unexpected exception processing '{args.config_file} - '{args.profile_name}'") + raise SystemExit(RET_SETUP_FAIL) + + vn_client: oci.core.VirtualNetworkClient = None + try: + if config: + # A config was specified, use that to connect. + vn_client = oci.core.VirtualNetworkClient(config=config, + retry_strategy = oci.retry.DEFAULT_RETRY_STRATEGY) + else: + # No config was provided, attempt to use an instance principal. + signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner() + vn_client = oci.core.VirtualNetworkClient(config={}, signer=signer, + retry_strategy = oci.retry.DEFAULT_RETRY_STRATEGY) + except Exception as e: + logging.exception('Unable to create OCI client') + raise SystemExit(RET_SETUP_FAIL) + + exit_code: int = RET_SUCCESS + exit_msgs: list[str] = [] + vnic: str = '' + ips: dict[str, list[str]] = dict() + for vnic, ips in vnic_ips.items(): + ret_code: int = -1 + ret_data: str = '' + for ipv4 in ips['ipv4']: + ret_code, ret_data = reassign_v4ip(vn_client, vnic, ipv4) + if ret_code != HTTPStatus.OK: + exit_code = RET_API_FAIL + exit_msgs.append(f'{ipv4} failed') + else: + if isinstance(ret_data, oci.core.models.PrivateIp): + exit_msgs.append(f'{ret_data.ip_address} success') + else: + # If for some reason the call succeeded but we didn't get + # the expected object back(?!), report the OCID instead. + exit_msgs.append(f'{ipv4} success') + if verbose: exit_msgs.append(ret_data) + + for ipv6 in ips['ipv6']: + ret_code, ret_data = reassign_v6ip(vn_client, vnic, ipv6) + if ret_code != HTTPStatus.OK: + exit_code = RET_API_FAIL + exit_msgs.append(f'{ipv6} failed') + else: + if isinstance(ret_data, oci.core.models.Ipv6): + exit_msgs.append(f'{ret_data.ip_address} success') + else: + # If for some reason the call succeeded but we didn't get + # the expected object back(?!), report the OCID instead. + exit_msgs.append(f'{ipv6} success') + if verbose: exit_msgs.append(ret_data) + + return exit_code, exit_msgs + + +def get_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description='Bulk reassignment of IP addresses.') + parser.add_argument( + '--use-profile', + help='use OCI profile rather than instance principal', + action='store_true') + parser.add_argument( + '--config', + help=f'the OCI config file to use (default: {oci.config.DEFAULT_LOCATION})', + metavar='config', + dest='config_file', + action='store', + default=oci.config.DEFAULT_LOCATION) + parser.add_argument( + '--profile', + help=f'the profile to use from the config file (default: {oci.config.DEFAULT_PROFILE})', + metavar='name', + dest='profile_name', + action='store', + default=oci.config.DEFAULT_PROFILE) + parser.add_argument( + '--json', + help='JSON file containing vNIC and IP/OCID details', + metavar='json', + required=True, + dest='json_file', + action='store') + parser.add_argument( + '--verbose', + help='be very verbose with API output', + action='store_true') + + args: argparse.Namespace = parser.parse_args() + + if args.verbose: + if args.use_profile: + logging.info(f'Config: {args.config_file}') + logging.info(f'Profile: {args.profile_name}') + else: + logging.info('Config: (instance principal)') + logging.info(f'JSON: {args.json_file}') + logging.info(f'Verbose: {args.verbose}') + + return args + + +def main() -> None: + _init_logging() + # Parse args + args: argparse.Namespace = get_args() + exit_code: int + exit_msgs: list[str] + exit_code, exit_msgs = process_json(args) + for msg in exit_msgs: + logging.error(msg) + raise SystemExit(exit_code) + +if __name__ == '__main__': + main() diff --git a/net/pfSense-pkg-ocicarp/files/usr/local/pkg/ocicarp.xml b/net/pfSense-pkg-ocicarp/files/usr/local/pkg/ocicarp.xml new file mode 100644 index 000000000000..5bff0c8c4dd4 --- /dev/null +++ b/net/pfSense-pkg-ocicarp/files/usr/local/pkg/ocicarp.xml @@ -0,0 +1,50 @@ + + + + + + + + ocicarp + Services: Oracle Cloud Infrastructure CARP + /usr/local/pkg/ocicarp/ocicarp.inc + + Oracle Cloud Infrastructure CARP +
Services
+ /ocicarp_settings.php +
+ + + plugin_carp + + + + + + + + +
diff --git a/net/pfSense-pkg-ocicarp/files/usr/local/pkg/ocicarp/ocicarp.inc b/net/pfSense-pkg-ocicarp/files/usr/local/pkg/ocicarp/ocicarp.inc new file mode 100644 index 000000000000..f10c12964dc1 --- /dev/null +++ b/net/pfSense-pkg-ocicarp/files/usr/local/pkg/ocicarp/ocicarp.inc @@ -0,0 +1,483 @@ + $vnicdetails) { + if ((!$vnicdetails['macAddr']) || (!$vnicdetails['vnicId'])) { + log_error(gettext("Missing OCI metadata for vNIC") . " {$ocivnic}"); + continue; + } + $ociifs[strtolower($vnicdetails['macAddr'])] = $vnicdetails['vnicId']; + } + + return $ociifs; +} + + +/** + * A function to return a list of VIP IDs where they can be matched to OCI + * vNICs (by MAC address). If no matches, returns an empty array. If no + * metadata available, returns null. + * + * @return array Associative array (empty if no matches) where key is CARP VIP ID (vid) and value is the IP address + * @return null If no OCI metadata could be accessed + */ +function ocicarp_get_oci_suitable_vips(): array|null { + $ocivnics = ocicarp_get_oci_vnic_list_details(); + $carplist = get_configured_vip_list('all', VIP_CARP); + $suitablevips = array(); + + if (is_null($ocivnics)) { + return null; + } + + // If either of these are empty, nothing to do here; return an empty array. + if (!empty($ocivnics) && (!empty($carplist))) { + + // Get a list of all the VIPs we can match to interfaces. + $matchedvips = array(); + foreach ($carplist as $vid => $vaddr) { + $vip = get_configured_vip($vid); + if (!array_key_exists('carp_mode', $vip) || ($vip['carp_mode'] !== 'ucast')) { + // Only unicast CARP VIPs are supported. + continue; + } + $realif = get_real_interface($vip['interface']); + $ifaddrs = get_interface_addresses($realif); + if (!$ocivnics[strtolower($ifaddrs['macaddr'])]) { + continue; + } + $matchedvips[$vid] = $vaddr; + } + + if (!empty($matchedvips)) { + /* + * Get VIPs we can match to vNICs and OCIDs. This ensures that IPs + * actually exist in OCI in addition to on the pfSense instance. + */ + $vipdetails = ocicarp_get_all_vip_matching_ocids(array_keys($matchedvips), $ocivnics); + foreach ($vipdetails as $vid => $vipdetail) { + $aliases = array(); + $match = ''; + foreach ($vipdetail['ips'] as $ipdetail) { + // As we will have all IPs (including aliases), look for our VIP IP. + if ($ipdetail['ip'] !== $matchedvips[$vid]) { + $aliases[] = $ipdetail['ip']; + continue; + } + $match = $matchedvips[$vid]; + } + if ($match != '') { + if (!empty($aliases)) { + $suitablevips[$vid] = $matchedvips[$vid] . " (" . implode(', ', $aliases) . ")"; + } else { + $suitablevips[$vid] = $matchedvips[$vid]; + } + } + } + } + } + + return $suitablevips; +} + + +/** + * A function to find the OCI OCID for an vNIC's subnet. + * + * @param string $vnicocid OCID of the vNIC that should have the VIP + * + * @return string OCID of the subnet + * @return null Address was not found + */ +function ocicarp_get_subnet_ocid(string $vnicocid): string|null { + $output = array(); + $ret_val = 0; + // Gets only one row. + exec(OCI_CLI . " vnic get --vnic-id " . escapeshellarg($vnicocid) . " --query 'data.\"subnet-id\"' 2>&1", $output, $ret_val); + if (($ret_val == 0) && (!empty($output))) { + $subnetocid = trim($output[0], '"'); // Strip double quotes. + return $subnetocid; + } else { + log_error(implode(', ', $output)); + return null; + } +} + + +/** + * A function to map an array of IPs from a subnet to their corresponding + * OCIDs. Works for both v4 and v6 addresses. + * + * @param array $ips Array of IP addresses of the same class - v4 or v6 + * @param string $subnetocid OCID of the subnet in which to look for IPs. + * + * @return array Associative multi-dimensional array of CARP VIPs with vNIC + * OCIDs, IP addresses and IP address OCIDs. + */ +function ocicarp_get_subnet_ip_ocids(array $ips, string $subnetocid): array|null { + $mapping = array(); + if (!empty($ips)) { + $output = array(); + $ret_val = 0; + // All IPs will be of the same class, so just check the first one. + $ipcmd = 'private-ip'; + if (is_v6($ips[0])) { + $ipcmd = 'ipv6'; + } + // Gets 0 or more as JSON. + exec(OCI_CLI . " {$ipcmd} list --subnet-id " . escapeshellarg($subnetocid) . " --query 'data[?\"ip-address\"==`" . implode('` || "ip-address"==`', $ips) . "`].[\"ip-address\", id]' 2>&1", $output, $ret_val); + + /* + * If the JMESPath query returns nothing (i.e. no matches) we get an error + * message and not JSON. In that case, return null and log the message. + */ + $json_result = json_decode(implode(PHP_EOL, $output), true); + if (($ret_val == 0) && (!empty($output)) && (!empty($json_result))) { + foreach($json_result as $map) { + $mapping[] = array('ip' => $map[0], 'ipocid' => $map[1]); + } + } else { + log_error(implode(', ', $output)); + return null; + } + } + return $mapping; +} + + +/** + * A function to construct an associative multi-dimensional array of CARP + * VIPs with vNIC OCIDs, IP addresses and IP address OCIDs. + * + * @param array $carpvips OCI CARP VIP IDs + * @param array $ocivnics Associative array of MAC and vNIC OCID + * + * @return array Associative multi-dimensional array of CARP VIPs with vNIC + * OCIDs, IP addresses and IP address OCIDs. + */ +function ocicarp_get_all_vip_matching_ocids($carpvips, $ocivnics):array { + $vnicsubnet = array(); + $mapping = array(); + foreach ($carpvips as $vid) { + $vip = get_configured_vip($vid); + $realif = get_real_interface($vip['interface']); + $ifaddrs = get_interface_addresses($realif); + + if (!$ocivnics[strtolower($ifaddrs['macaddr'])]) { + continue; + } + $vnicocid = $ocivnics[strtolower($ifaddrs['macaddr'])]; + /* + * As looking up the subnet is expensive and multiple VIPs could be on the + * vNIC, cache it to avoid extra look ups. + */ + if (!array_key_exists($vnicocid, $vnicsubnet)) { + $vnicsubnet[$vnicocid] = ocicarp_get_subnet_ocid($vnicocid); + } + + $carpvip = "_vip{$vip['uniqid']}"; + $allips = array($vip['subnet']); + + $ipaliases = trim(get_interface_vip_ips($carpvip), ' '); + if ($ipaliases !== "") { + $allips = array_merge($allips, explode(' ', $ipaliases)); + } + + $ipocids = ocicarp_get_subnet_ip_ocids($allips, $vnicsubnet[$vnicocid]); + if (!empty($ipocids)) { + $mapping[$carpvip]['vnicocid'] = $vnicocid; + $mapping[$carpvip]['ips'] = $ipocids; + } else { + log_error(gettext("Oracle Cloud Infrastructure CARP unable to map any IP OCIDs for") . " {$carpvip} - " . explode(', ', $allips)); + } + } + return $mapping; +} + + +/** + * A function to write the JSON configuration files for each CARP VIP that + * will be used by invocation from devd. + * + * @param array $ocicarp_config OCI CARP package configuration + * @param array $ocivnics array of MAC and vNIC OCID + */ +function ocicarp_write_vip_files($ocicarp_config, $ocivnics):void { + if (!empty($ocicarp_config['carpvips'])) { + + $carpvips = explode(',', $ocicarp_config['carpvips']); + $vipdetails = ocicarp_get_all_vip_matching_ocids($carpvips, $ocivnics); + + if (empty($vipdetails)) { + log_error(gettext("Oracle Cloud Infrastructure CARP could not map VIPs to OCIDs") . " - {$ocicarp_config['carpvips']}"); + return; + } + foreach ($vipdetails as $vip => $vipdetail) { + // Build file body. + $txt = "{" . PHP_EOL; + $txt .= "\t\"{$vipdetail['vnicocid']}\": {". PHP_EOL; + + $ipv4s = "\t\t\"ipv4\": ["; + $ipv6s = "\t\t\"ipv6\": ["; + foreach ($vipdetail['ips'] as $ip) { + if (is_v6($ip['ip'])) { + $ipv6s .= PHP_EOL . "\t\t\t\"{$ip['ipocid']}\","; + } else { + $ipv4s .= PHP_EOL . "\t\t\t\"{$ip['ipocid']}\","; + } + } + $ipv4s = rtrim($ipv4s, ',') . PHP_EOL; + $ipv6s = rtrim($ipv6s, ',') . PHP_EOL; + + $ipv4s .= "\t\t]," . PHP_EOL; + $ipv6s .= "\t\t]" . PHP_EOL; + $txt .= $ipv4s . $ipv6s; + $txt .= "\t}" . PHP_EOL; + $txt .= "}" . PHP_EOL; + + $filename = OCICARP_CONFIG_LOC . "/{$vip}.json"; + if (!file_put_contents($filename, $txt)) { + log_error(gettext("Oracle Cloud Infrastructure CARP ERROR: Could not write to file") . " {$filename}"); + exit; + } + } + } +} + + +/** + * A function to write the devd configuration file with the matching rules to + * call the ocicarp.py script to invoke the OCI APIs. + * + * @param array $ocicarp_config OCI CARP package configuration + * @param array $ocivnics array of MAC and vNIC OCID + */ +function ocicarp_write_devd_config($ocicarp_config, $ocivnics): void { + $txt = "# This file is generated by the pfSense Oracle Cloud Infrastructure CARP package." . PHP_EOL; + $txt .= "# Do not edit this file, it will be overwritten automatically." . PHP_EOL; + + if (!empty($ocicarp_config['carpvips'])) { + foreach (explode(',', $ocicarp_config['carpvips']) as $vid) { + $vip = get_configured_vip($vid); + $realif = get_real_interface($vip['interface']); + $ifaddrs = get_interface_addresses($realif); + + if (!$ocivnics[strtolower($ifaddrs['macaddr'])]) { + continue; + } + $vnicocid = $ocivnics[strtolower($ifaddrs['macaddr'])]; + $txt .= "notify 100 {" . PHP_EOL; + $txt .= "\tmatch \"system\" \"CARP\";" . PHP_EOL; + $txt .= "\tmatch \"type\" \"MASTER\";" . PHP_EOL; + $txt .= "\tmatch \"subsystem\" \"{$vip['vhid']}@{$realif}\";" . PHP_EOL; + $txt .= "\taction \"" . OCICARP_SCRIPT_FILE . " --json " . escapeshellarg(OCICARP_CONFIG_LOC . "/_vip{$vip['uniqid']}.json") . " &\";" . PHP_EOL; + $txt .= "};" . PHP_EOL; + } + } + + if (!file_put_contents(OCICARP_DEVD_CONFIG_FILE, $txt)) { + log_error(gettext("Oracle Cloud Infrastructure CARP ERROR: Could not write to file") . " '" . OCICARP_DEVD_CONFIG_FILE . "'"); + exit; + } +} + + +/** + * A function to resync the configuration and config files and restart devd. + */ +function ocicarp_sync_config():void { + $ocicarp_config = config_get_path('installedpackages/ocicarp/config/0', []); + if (isset($ocicarp_config['enable'])) { + $enable = $ocicarp_config['enable']; + } else { + $enable = 'no'; + } + + $ocivnics = ocicarp_get_oci_vnic_list_details(); + + // Always write VIP config files. + ocicarp_write_vip_files($ocicarp_config, $ocivnics); + if ($enable !== 'yes') { + // Remove the devd config file. + unlink_if_exists(OCICARP_DEVD_CONFIG_FILE); + } else { + // Create the devd config file. + ocicarp_write_devd_config($ocicarp_config, $ocivnics); + } + + // service devd restart exit code is 0 if running, convert to PHP "TRUE". + if (0==mwexec('service devd restart')) { + log_error(gettext("Oracle Cloud Infrastructure CARP sync restarting devd")); + } else { + log_error(gettext("Oracle Cloud Infrastructure CARP failed to restart! Check configuration in") . " '" . OCICARP_DEVD_CONFIG_FILE . "'"); + } +} + + +/** + * A function to handle deinstallation tasks. On deinstall, the devd config + * file is removed and devd is restarted to reload the changed configuration. + * If "keep_config" is cleared in the configuration, all package configuration + * is removed, otherwise it is preserved. + */ +function ocicarp_deinstall_command():void { + $ocicarp_config = config_get_path('installedpackages/ocicarp/config/0', []); + if (isset($ocicarp_config['keep_conf'])) { + $keep_conf = $ocicarp_config['keep_conf']; + } else { + $keep_conf = 'no'; + } + + // Remove the devd config file. + log_error(gettext("Oracle Cloud Infrastructure CARP deinstall removed config files")); + unlink_if_exists(OCICARP_DEVD_CONFIG_FILE); + + if (!empty($ocicarp_config['carpvips'])) { + // Remove the vip config file(s) + foreach (explode(',', $ocicarp_config['carpvips']) as $vip) { + unlink_if_exists(OCICARP_CONFIG_LOC . "/{$vip}.json"); + } + } + + /* + * Restart devd to reread config. + * Using mwexec as restart_service() does not work during deinstall. + */ + log_error(gettext("Oracle Cloud Infrastructure CARP deinstall restarting service devd")); + mwexec('service devd restart'); + + if ($keep_conf == 'no') { + // Remove configuration. + config_del_path('installedpackages/ocicarp/config/0'); + write_config('Oracle Cloud Infrastructure CARP removed settings'); + } +} + + +/** + * A function to build a list of VHID@IF and VID for matching in the + * ocicarp_plugin_carp() function. + * + * @param string $ocicarp_vids The comma separated list of CARP VIP IDs (vids) from the configuration + * + * @return array Associative array (empty if no matches) where key is CARP subsystem ids (i.e. VHID@interface) and value is the matched VIP ID. + */ +function ocicarp_get_carp_vhidif_list($ocicarp_vids): array { + $list = array(); + if (!empty($ocicarp_vids)) { + foreach (explode(',', $ocicarp_vids) as $vid) { + $vip = get_configured_vip($vid); + $realif = get_real_interface($vip['interface']); + $list["{$vip['vhid']}@{$realif}"] = $vid; + } + } + return $list; +} + + +/** + * A function called by rc.carpmaster/rc.carpbackup via plugin_carp. + * + * @param array $pluginparams List of parameters passed from CARP scripts. + */ +function ocicarp_plugin_carp($pluginparams):void { + // Start or stop the service as needed based on the CARP transition. + if ($pluginparams['event'] == "rc.carpbackup") { + log_error(gettext("Oracle Cloud Infrastructure CARP nothing to do") . " (rc.carpbackup)"); + } elseif ($pluginparams['event'] == "rc.carpmaster") { + // If there is no ocicarp configuration, then nothing to do. + $ocicarp_config = config_get_path('installedpackages/ocicarp/config/0', []); + if ((empty($ocicarp_config)) || (!isset($ocicarp_config['enable'])) + || empty($ocicarp_config['carpvips'])) { + log_error(gettext("Oracle Cloud Infrastructure CARP nothing to do (no config)") . " (rc.carpmaster)"); + } else { + // Locate the VIP and match it against the configured check address. + $vhidifs = ocicarp_get_carp_vhidif_list($ocicarp_config['carpvips']); + if (array_key_exists(trim($pluginparams['interface']), $vhidifs)) { + $vid = $vhidifs[trim($pluginparams['interface'])]; + log_error(gettext("Oracle Cloud Infrastructure CARP running for") . " {$vid} (rc.carpmaster)"); + mwexec(OCICARP_SCRIPT_FILE . " --json " . escapeshellarg(OCICARP_CONFIG_LOC . "/{$vid}.json") . " &"); + } else { + log_error(gettext("Oracle Cloud Infrastructure CARP no action (no VIP config) for") . " {$pluginparams['interface']} (rc.carpmaster)"); + } + } + } else { + log_error(gettext("Oracle Cloud Infrastructure CARP no action for event") . " '{$pluginparams['event']}' (rc.carpmaster)"); + } +} diff --git a/net/pfSense-pkg-ocicarp/files/usr/local/share/pfSense-pkg-ocicarp/info.xml b/net/pfSense-pkg-ocicarp/files/usr/local/share/pfSense-pkg-ocicarp/info.xml new file mode 100644 index 000000000000..f395dabb639f --- /dev/null +++ b/net/pfSense-pkg-ocicarp/files/usr/local/share/pfSense-pkg-ocicarp/info.xml @@ -0,0 +1,11 @@ + + + + Oracle Cloud Infrastructure CARP + ocicarp + https://www.oracle.com/cloud/ + + %%PKGVERSION%% + ocicarp.xml + + diff --git a/net/pfSense-pkg-ocicarp/files/usr/local/www/ocicarp_settings.php b/net/pfSense-pkg-ocicarp/files/usr/local/www/ocicarp_settings.php new file mode 100644 index 000000000000..0c1dc9eb9d4b --- /dev/null +++ b/net/pfSense-pkg-ocicarp/files/usr/local/www/ocicarp_settings.php @@ -0,0 +1,178 @@ +addInput(new Form_Checkbox( + 'enable', + gettext('Enable'), + gettext('Oracle Cloud Infrastructure (OCI) CARP integration'), + $pconfig['enable'] +))->setHelp( + ''. gettext('Note') .': ' . gettext('Integration will be automatically disabled if the package is reinstalled.') +); + +$section->addInput(new Form_Checkbox( + 'keep_conf', + gettext('Keep Configuration'), + gettext('Enable'), + $pconfig['keep_conf'] == 'yes' +))->setHelp( + '' . gettext('Note') . ': ' . gettext('With \'Keep Configuration\' enabled (default), configuration will persist on install/deinstall.') +); + +$section->addInput(new Form_Select( + 'carpvips_a', + gettext('Suitable CARP VIPs'), + $pconfig['carpvips_a'], + is_null($available_carp_vips) ? array() : $available_carp_vips, + true +))->setHelp( + gettext('Select the CARP VIPs that OCI will be notified of when they move between nodes. Matched VIP aliases are shown in braces.') . '

' . + '' . gettext('Note') . ' 1: ' . gettext('Ensure the same VIP are selected on the other node') . '.
'. + '' . gettext('Note') . ' 2: ' . gettext('Suitable CARP VIPs are unicast and matchable to OCI vNICs.') +); + +$form->add($section); + +print($form); +?> + +
+ ' . gettext('This package will only work when pfSense is running on Oracle Cloud Infrastructure (OCI). A number of prerequisites must also be met before this package will function correctly:') . '

+
    +
  1. ' . gettext('OCI must be configured with a dynamic group and suitable policy to allow pfSense instances to use Instance Principals to manage IP addresses for their vNICs.') . '
  2. +
  3. ' . gettext('Ensure that pfSense can reach the OCI instance metadata service at http://' . OCI_MDS_IP . ' as this is used to gather details about the vNICs that pfSense is using.') . '
  4. +
  5. ' . gettext('pfSense must be able to reach OCI API endpoints either via an OCI service gateway, NAT gateway or internet gateway. Thus OCI subnet routing must be setup appropriately.') . '
  6. +
  7. ' . gettext('Ensure that for each unicast IPv4 and IPv6 CARP VIP, there exists an equivalent OCI secondary IP address that is assigned to a vNIC (ideally on the primary instance of the cluster).') . '
  8. +
+

' . gettext('For more information on primary and secondary IP addresses see the Private IP Addresses documentation. For more information on instance primary and secondary vNICs see the Virtual Network Interface Cards (VNICs) documentation.') . '

+

' . gettext('As this package uses the OCI CLI for interacting with OCI, the OCI configuration for Instance Principals and API access can be checked from a shell on the pfSense instance. First, copy the OCID of a pfSense vNIC from the OCI console and run a command like the following substituting your OCID:

oci --auth instance_principal network private-ip list --vnic-id PLACE-OCID-HERE
This should return JSON data for IP addresses of the associated vNIC and not an error. Something like:') . '

+
+{
+  "data": [
+    {
+      "availability-domain": "xyzzy",
+      "compartment-id": "compartment-ocid-appears-here",
+      "defined-tags": {
+        "Oracle-Tags": {
+          "CreatedBy": "someone@example.com",
+          "CreatedOn": "1970-01-1T00:00:00.000Z",
+        }
+      },
+      "display-name": "private",
+      "freeform-tags": {},
+      "hostname-label": null,
+      "id": "private-ip-ocid-appears-here",
+      "ip-address": "192.168.10.11",
+      "is-primary": true,
+      "subnet-id": "subnet-ocid-appears-here",
+      "time-created": "1970-01-01T00:00:00.000000+00:00",
+      "vlan-id": null,
+      "vnic-id": "vnic-ocid-appears-here"
+    }
+  ]
+}
+
', 'info')?> +
+ + diff --git a/net/pfSense-pkg-ocicarp/files/usr/local/www/shortcuts/pkg_ocicarp.inc b/net/pfSense-pkg-ocicarp/files/usr/local/www/shortcuts/pkg_ocicarp.inc new file mode 100644 index 000000000000..8d3726698769 --- /dev/null +++ b/net/pfSense-pkg-ocicarp/files/usr/local/www/shortcuts/pkg_ocicarp.inc @@ -0,0 +1,25 @@ +