Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

119 support ha panorama upgrades #120

Merged
merged 2 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading