Skip to content

Commit

Permalink
Merge pull request #120 from cdot65/119-support-ha-panorama-upgrades
Browse files Browse the repository at this point in the history
119 support ha panorama upgrades
  • Loading branch information
cdot65 authored Mar 20, 2024
2 parents a032156 + 0d80256 commit db0ad37
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 17 deletions.
4 changes: 2 additions & 2 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions docs/about/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>*
Expand Down
49 changes: 45 additions & 4 deletions pan_os_upgrade/components/ha.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
108 changes: 98 additions & 10 deletions pan_os_upgrade/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@

# Palo Alto Networks imports
from panos.firewall import Firewall
from panos.panorama import Panorama

# third party imports
import typer
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]
documentation = "https://cdot65.github.io/pan-os-upgrade/"
Expand Down

0 comments on commit db0ad37

Please sign in to comment.