From 678755f1c3da2571a32770dd6e2fc79e7b943e21 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Tue, 19 Mar 2024 19:55:11 -0500 Subject: [PATCH 1/2] Update pan-os-upgrade version to 1.3.10 --- docker/Dockerfile | 4 ++-- docs/about/release-notes.md | 8 ++++++++ pyproject.toml | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1dbeea4..375e0ff 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,8 +15,8 @@ WORKDIR /app COPY settings.yaml /app # Install any needed packages specified in requirements.txt -# Note: The requirements.txt should contain pan-os-upgrade==1.3.9 -RUN pip install --no-cache-dir pan-os-upgrade==1.3.9 +# Note: The requirements.txt should contain pan-os-upgrade==1.3.10 +RUN pip install --no-cache-dir pan-os-upgrade==1.3.10 # Set the locale to avoid issues with emoji rendering ENV LANG C.UTF-8 diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index e6a04ba..9f82954 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -2,6 +2,14 @@ Welcome to the release notes for the `pan-os-upgrade` tool. This document provides a detailed record of changes, enhancements, and fixes in each version of the tool. +## Version 1.3.10 + +**Release Date:** *<20240319>* + +### What's New in version 1.3.10 + +- Added support for the `panorama` command to perform HA upgrades + ## Version 1.3.9 **Release Date:** *<20240319>* diff --git a/pyproject.toml b/pyproject.toml index 7fee20c..cd5f656 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pan-os-upgrade" -version = "1.3.9" +version = "1.3.10" description = "Python script to automate the upgrade process of PAN-OS firewalls." authors = ["Calvin Remsburg "] documentation = "https://cdot65.github.io/pan-os-upgrade/" From 0d80256d4aec2c6e6df5179d2afd158e46be7a68 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Tue, 19 Mar 2024 20:36:42 -0500 Subject: [PATCH 2/2] add support for HA upgrades for the `panorama` subcommand --- pan_os_upgrade/components/ha.py | 49 +++++++++++++-- pan_os_upgrade/main.py | 108 +++++++++++++++++++++++++++++--- 2 files changed, 143 insertions(+), 14 deletions(-) diff --git a/pan_os_upgrade/components/ha.py b/pan_os_upgrade/components/ha.py index 331bc6e..aa4abc6 100644 --- a/pan_os_upgrade/components/ha.py +++ b/pan_os_upgrade/components/ha.py @@ -501,60 +501,101 @@ def handle_panorama_ha( # If the active and passive target devices are running the same version if version_comparison == "equal": + + # If the current device is primary-active if local_state == "primary-active": + # Add the active target device to the list and exit the upgrade process with target_devices_to_revisit_lock: target_devices_to_revisit.append(target_device) + + # Log message to console logging.info( f"{get_emoji(action='search')} {hostname}: Detected primary-active target device in HA pair running the same version as its peer. Added target device to revisit list." ) + + # Exit the upgrade process for the target device at this time, to be revisited later return False, None + # if the current device is secondary-passive elif local_state == "secondary-passive": - # Continue with upgrade process on the secondary-passive target device - logging.info( - f"{get_emoji(action='report')} {hostname}: Target device is secondary-passive", - ) + + # suspend HA state of the target device + if not dry_run: + logging.info( + f"{get_emoji(action='report')} {hostname}: Suspending HA state of secondary-passive" + ) + suspend_ha_passive( + target_device, + hostname, + ) + + # log message to console + else: + logging.info( + f"{get_emoji(action='report')} {hostname}: Target device is secondary-passive, but we are in dry-run mode. Skipping HA state suspension.", + ) + + # Continue with upgrade process on the passive target device return True, None elif ( local_state == "secondary-suspended" or local_state == "secondary-non-functional" ): + # Continue with upgrade process on the secondary-suspended or secondary-non-functional target device logging.info( f"{get_emoji(action='warning')} {hostname}: Target device is {local_state}", ) + + # Continue with upgrade process on the passive target device return True, None elif version_comparison == "older": + + # log message to console logging.info( f"{get_emoji(action='report')} {hostname}: Target device is on an older version" ) + # Suspend HA state of active if the primary-active is on a later release if local_state == "primary-active" and not dry_run: + + # log message to console logging.info( f"{get_emoji(action='report')} {hostname}: Suspending HA state of primary-active" ) + + # Suspend HA state of primary-active suspend_ha_active( target_device, hostname, ) + return True, None elif version_comparison == "newer": + + # log message to console logging.info( f"{get_emoji(action='report')} {hostname}: Target device is on a newer version" ) + # Suspend HA state of secondary-passive if the primary-active is on a later release if local_state == "primary-active" and not dry_run: + + # log message to console logging.info( f"{get_emoji(action='report')} {hostname}: Suspending HA state of primary-active" ) + + # Suspend HA state of primary-active suspend_ha_passive( target_device, hostname, ) + return True, None return False, None diff --git a/pan_os_upgrade/main.py b/pan_os_upgrade/main.py index 84a24c1..a5e58d9 100644 --- a/pan_os_upgrade/main.py +++ b/pan_os_upgrade/main.py @@ -82,6 +82,7 @@ # Palo Alto Networks imports from panos.firewall import Firewall +from panos.panorama import Panorama # third party imports import typer @@ -448,16 +449,103 @@ def panorama( settings_file_path=SETTINGS_FILE_PATH, ) - # Perform upgrade - upgrade_panorama( - dry_run=dry_run, - panorama=device, - settings_file=SETTINGS_FILE, - settings_file_path=SETTINGS_FILE_PATH, - target_devices_to_revisit=target_devices_to_revisit, - target_devices_to_revisit_lock=target_devices_to_revisit_lock, - target_version=target_version, - ) + panorama_objects_for_upgrade = [device] + + # Determine if the targeted device is operating in an HA pair + ha_status = device.op("show high-availability state") + ha_dict = flatten_xml_to_dict(ha_status) + + # If the device is in an HA pair, store the peer's information + if ha_dict["result"]["enabled"] == "yes": + # Store all peer-info details in a dictionary + peer = ha_dict["result"]["peer-info"] + + # Determine the peer's IP address if the mgmt-ip is not empty + if peer["mgmt-ip"] and len(peer["mgmt-ip"]) > 0: + peer["ip"] = peer["mgmt-ip"].split("/")[0] + + # If the mgmt-ip is empty, use the mgmt-ipv6 field + elif peer["mgmt-ipv6"] and len(peer["mgmt-ipv6"]) > 0: + peer["ip"] = peer["mgmt-ipv6"].split("/")[0] + + else: + # no mgmt-ip or mgmt-ipv6 or ha1-ipaddr found, log message and sys.exit + logging.error( + f"{get_emoji(action='error')} {hostname}: No IP address found for the peer Panorama appliance. Exiting." + ) + sys.exit(1) + + panorama_objects_for_upgrade.append(Panorama(peer["ip"], username, password)) + + # First round of upgrades, targeting all panoramas and placing active panoramas in an HA pair on a revisit list + with ThreadPoolExecutor(max_workers=2) as executor: + # Store future objects along with panoramas for reference + future_to_panorama = { + executor.submit( + upgrade_panorama, + dry_run=dry_run, + panorama=target_device, + settings_file=SETTINGS_FILE, + settings_file_path=SETTINGS_FILE_PATH, + target_devices_to_revisit=target_devices_to_revisit, + target_devices_to_revisit_lock=target_devices_to_revisit_lock, + target_version=target_version, + ): target_device + for target_device in panorama_objects_for_upgrade + } + + # Process completed tasks + for future in as_completed(future_to_panorama): + panorama = future_to_panorama[future] + try: + future.result() + except Exception as exc: + logging.error( + f"{get_emoji(action='error')} {hostname}: Panorama {panorama.hostname} generated an exception: {exc}" + ) + + # Second round of upgrades, revisiting panoramas that were active in an HA pair and had the same version as their peers + if target_devices_to_revisit: + logging.info( + f"{get_emoji(action='start')} {hostname}: Revisiting panoramas that were active in an HA pair and had the same version as their peers." + ) + + # Using ThreadPoolExecutor to manage threads for revisiting panoramas + threads = SETTINGS_FILE.get("concurrency.threads", 10) + logging.debug( + f"{get_emoji(action='working')} {hostname}: Using {threads} threads." + ) + with ThreadPoolExecutor(max_workers=threads) as executor: + future_to_panorama = { + executor.submit( + upgrade_panorama, + dry_run=dry_run, + panorama=target_device, + settings_file=SETTINGS_FILE, + settings_file_path=SETTINGS_FILE_PATH, + target_devices_to_revisit=target_devices_to_revisit, + target_devices_to_revisit_lock=target_devices_to_revisit_lock, + target_version=target_version, + ): target_device + for target_device in target_devices_to_revisit + } + + # Process completed tasks + for future in as_completed(future_to_panorama): + panorama = future_to_panorama[future] + try: + future.result() + logging.info( + f"{get_emoji(action='success')} {hostname}: Completed revisiting panoramas" + ) + except Exception as exc: + logging.error( + f"{get_emoji(action='error')} {hostname}: Exception while revisiting panoramas: {exc}" + ) + + # Clear the list after revisiting + with target_devices_to_revisit_lock: + target_devices_to_revisit.clear() # Subcommand for batch upgrades using Panorama as a communication proxy