From c006fffec4b9950bd755db9724bceda61e47322f Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Wed, 24 Jan 2024 06:54:32 -0600 Subject: [PATCH 01/13] Refactor code to improve performance and readability --- pan_os_upgrade/upgrade.py | 2431 ++++++++++++++++++++----------------- 1 file changed, 1327 insertions(+), 1104 deletions(-) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index be839ef..7cde21e 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -247,701 +247,716 @@ class AssuranceOptions: # ---------------------------------------------------------------------------- -# Setting up logging +# Core Upgrade Functions # ---------------------------------------------------------------------------- -def configure_logging(level: str, encoding: str = "utf-8") -> None: +def backup_configuration( + firewall: Firewall, + file_path: str, +) -> bool: """ - Sets up the logging configuration for the script with the specified logging level and encoding. + Backs up the current running configuration of a specified firewall to a local file. - This function initializes the global logger, sets the specified logging level, and configures two handlers: - one for console output and another for file output. It uses RotatingFileHandler for file logging to manage - file size and maintain backups. + This function retrieves the running configuration from the firewall and saves it as an XML file + at the specified file path. It checks the validity of the retrieved XML data and logs the success + or failure of the backup process. Parameters ---------- - level : str - The desired logging level (e.g., 'debug', 'info', 'warning', 'error', 'critical'). - The input is case-insensitive. If an invalid level is provided, it defaults to 'info'. + firewall : Firewall + The firewall instance from which the configuration is to be backed up. + file_path : str + The path where the configuration backup file will be saved. - encoding : str, optional - The encoding format for the file-based log handler, by default 'utf-8'. + Returns + ------- + bool + Returns True if the backup is successfully created, False otherwise. + + Raises + ------ + Exception + Raises an exception if any error occurs during the backup process. Notes ----- - - The Console Handler outputs log messages to the standard output. - - The File Handler logs messages to 'logs/upgrade.log'. This file is rotated when it reaches 1MB in size, - maintaining up to three backup files. - - The logging level influences the verbosity of the log messages. An invalid level defaults to 'info', - ensuring a baseline of logging. - """ - logging_level = getattr(logging, level.upper(), None) - - # Get the root logger - logger = logging.getLogger() - logger.setLevel(logging_level) - - # Remove any existing handlers - for handler in logger.handlers[:]: - logger.removeHandler(handler) - - # Create handlers (console and file handler) - console_handler = logging.StreamHandler() - file_handler = RotatingFileHandler( - "logs/upgrade.log", - maxBytes=1024 * 1024, - backupCount=3, - encoding=encoding, - ) + - The function verifies the XML structure of the retrieved configuration. + - Ensures the directory for the backup file exists. + - The backup file is saved in XML format. - # Create formatters and add them to the handlers - if level == "debug": - console_format = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - ) - file_format = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - ) - else: - console_format = logging.Formatter("%(message)s") - file_format = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - ) + Example + -------- + Backing up the firewall configuration: + >>> firewall = Firewall(hostname='192.168.1.1', 'admin', 'password') + >>> backup_configuration(firewall, '/path/to/config_backup.xml') + # Configuration is backed up to the specified file. + """ - console_handler.setFormatter(console_format) - file_handler.setFormatter(file_format) + try: + # Run operational command to retrieve configuration + config_xml = firewall.op("show config running") + if config_xml is None: + logging.error( + f"{get_emoji('error')} Failed to retrieve running configuration." + ) + return False - # Add handlers to the logger - logger.addHandler(console_handler) - logger.addHandler(file_handler) + # Check XML structure + if ( + config_xml.tag != "response" + or len(config_xml) == 0 + or config_xml[0].tag != "result" + ): + logging.error( + f"{get_emoji('error')} Unexpected XML structure in configuration data." + ) + return False + # Extract the configuration data from the tag + config_data = config_xml.find(".//result/config") -def get_emoji(action: str) -> str: - """ - Retrieves an emoji character corresponding to a specific action keyword. + # Manually construct the string representation of the XML data + config_str = ET.tostring(config_data, encoding="unicode") - This function is used to enhance the visual appeal and readability of log messages or console outputs. - It maps predefined action keywords to their corresponding emoji characters. + # Ensure the directory exists + ensure_directory_exists(file_path) - Parameters - ---------- - action : str - An action keyword for which an emoji is required. Supported keywords include 'success', - 'warning', 'error', 'working', 'report', 'search', 'save', 'stop', and 'start'. + # Write the file to the local filesystem + with open(file_path, "w") as file: + file.write(config_str) - Returns - ------- - str - The emoji character associated with the action keyword. If the keyword is not recognized, - returns an empty string. + logging.debug( + f"{get_emoji('save')} Configuration backed up successfully to {file_path}" + ) + return True - Examples - -------- - >>> get_emoji('success') - '✅' # Indicates a successful operation + except Exception as e: + logging.error(f"{get_emoji('error')} Error backing up configuration: {e}") + return False - >>> get_emoji('error') - '❌' # Indicates an error - >>> get_emoji('start') - '🚀' # Indicates the start of a process - """ - emoji_map = { - "success": "✅", - "warning": "⚠️", - "error": "❌", - "working": "⚙️", - "report": "📝", - "search": "🔍", - "save": "💾", - "stop": "🛑", - "start": "🚀", - } - return emoji_map.get(action, "") +def create_firewall_object( + serial_number: str, + panorama: Panorama, +) -> Firewall: + pass -# ---------------------------------------------------------------------------- -# Helper function to validate either the DNS hostname or IP address -# ---------------------------------------------------------------------------- -def resolve_hostname(hostname: str) -> bool: +def create_peer_firewall(firewall: Firewall) -> Optional[Firewall]: """ - Checks if a given hostname can be resolved via DNS query. - - This function attempts to resolve the specified hostname using DNS. It queries the DNS servers - that the operating system is configured to use. The function is designed to return a boolean - value indicating whether the hostname could be successfully resolved or not. + Creates a Firewall object representing the HA peer firewall. Parameters ---------- - hostname : str - The hostname (e.g., 'example.com') to be resolved. + firewall : Firewall + The current firewall instance to find the HA peer for. Returns ------- - bool - Returns True if the hostname can be resolved, False otherwise. + Optional[Firewall] + A Firewall object representing the HA peer, or None if the serial number of the peer cannot be found. - Raises - ------ - None - This function does not raise any exceptions. It handles all exceptions internally and - returns False in case of any issues during the resolution process. + Notes + ----- + - Retrieves the HA peer's serial number using an operational command. + - If the serial number is found, creates and returns a Firewall object for the HA peer. + - If the serial number is not found, logs an error and returns None. """ try: - dns.resolver.resolve(hostname) - return True - except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.exception.Timeout) as err: - # Optionally log or handle err here if needed - logging.debug(f"Hostname resolution failed: {err}") - return False + ha_peer_serial_response = firewall.op( + "peer.cfg.platform.serial", + cmd_xml=False, + ) + serial_string = ha_peer_serial_response.find(".//result").text + peer_serial_parsed = re.search(r"\b\d+\b", serial_string) + + if peer_serial_parsed: + peer_serial = peer_serial_parsed.group() + peer_firewall = Firewall(serial=peer_serial) + if firewall.parent: + firewall.parent.add(peer_firewall) + return peer_firewall + else: + logging.error(f"{get_emoji('error')} Serial number not found for HA peer") + return None + + except Exception as e: + logging.error( + f"{get_emoji('error')} Error creating HA peer firewall object: {e}" + ) + return None -def ip_callback(value: str) -> str: +def determine_upgrade( + firewall: Firewall, + target_major: int, + target_minor: int, + target_maintenance: Union[int, str], +) -> None: """ - Validates the input as a valid IP address or a resolvable hostname. + Determines the necessity of an upgrade for a firewall to a specific PAN-OS version. - This function first attempts to resolve the hostname via DNS query. If it fails, - it utilizes the ip_address function from the ipaddress standard library module to - validate the provided input as an IP address. It is designed to be used as a callback - function for Typer command-line argument parsing, ensuring that only valid IP addresses - or resolvable hostnames are accepted as input. + This function assesses if upgrading the firewall's PAN-OS version is required by comparing its current + version with the specified target version. The target version is defined by major, minor, and maintenance + version numbers, where the maintenance version can also include hotfix information. The function logs + the current and target versions, and establishes the need for an upgrade if the current version is lower + than the target. If the current version is equal to or higher than the target, it suggests that an upgrade + is unnecessary or a downgrade is being attempted, leading to termination of the script. Parameters ---------- - value : str - A string representing the IP address or hostname to be validated. - - Returns - ------- - str - The validated IP address string or hostname. + firewall : Firewall + The instance of the Firewall whose PAN-OS version is being evaluated. + target_major : int + Major version number of the target PAN-OS. + target_minor : int + Minor version number of the target PAN-OS. + target_maintenance : Union[int, str] + Maintenance or hotfix version number of the target PAN-OS, can be an integer or string. Raises ------ - typer.BadParameter - If the input string is not a valid IP address or a resolvable hostname, a typer.BadParameter - exception is raised with an appropriate error message. + SystemExit + Exits the script if the target version is not an upgrade, indicating either a downgrade attempt + or that the current version already meets or exceeds the target version. + + Notes + ----- + - Parses the PAN-OS version strings into tuples of integers for accurate comparison. + - Utilizes emojis in logging for clear and user-friendly status indication. """ - # First, try to resolve as a hostname - if resolve_hostname(value): - return value + current_version = parse_version(firewall.version) - # If hostname resolution fails, try as an IP address - try: - ipaddress.ip_address(value) - return value + if isinstance(target_maintenance, int): + # Handling integer maintenance version separately + target_version = (target_major, target_minor, target_maintenance, 0) + else: + # Handling string maintenance version with hotfix + target_version = parse_version( + f"{target_major}.{target_minor}.{target_maintenance}" + ) - except ValueError as err: - raise typer.BadParameter( - "The value you passed for --hostname is neither a valid DNS hostname nor IP address, please check your inputs again." - ) from err + logging.info(f"{get_emoji('report')} Current PAN-OS version: {firewall.version}") + logging.info( + f"{get_emoji('report')} Target PAN-OS version: {target_major}.{target_minor}.{target_maintenance}" + ) + upgrade_needed = current_version < target_version + if upgrade_needed: + logging.info( + f"{get_emoji('success')} Confirmed that moving from {firewall.version} to {target_major}.{target_minor}.{target_maintenance} is an upgrade" + ) + return -# ---------------------------------------------------------------------------- -# Helper function to ensure the directories exist for our snapshots -# ---------------------------------------------------------------------------- -def ensure_directory_exists(file_path: str) -> None: - """ - Ensures the existence of the directory for a specified file path, creating it if necessary. - - This function checks if the directory for a given file path exists. If it does not exist, the function - creates the directory along with any necessary parent directories. This is particularly useful for - ensuring that the file system is prepared for file operations that require specific directory structures. - - Parameters - ---------- - file_path : str - The file path whose directory needs to be verified and potentially created. The function extracts - the directory part of the file path to check its existence. + else: + logging.error( + f"{get_emoji('error')} Upgrade is not required or a downgrade was attempted." + ) + logging.error(f"{get_emoji('stop')} Halting script.") - Example - ------- - Ensuring a directory exists for a file path: - >>> file_path = '/path/to/directory/file.txt' - >>> ensure_directory_exists(file_path) - # If '/path/to/directory/' does not exist, it is created. - """ - directory = os.path.dirname(file_path) - if not os.path.exists(directory): - os.makedirs(directory) + sys.exit(1) -# ---------------------------------------------------------------------------- -# Helper function to check readiness and log the result -# ---------------------------------------------------------------------------- -def check_readiness_and_log( - result: dict, - test_name: str, - test_info: dict, -) -> None: +def get_ha_status(firewall: Firewall) -> Tuple[str, Optional[dict]]: """ - Evaluates and logs the results of a specified readiness test. + Determines the High-Availability (HA) deployment status and configuration of a specified Firewall appliance. - This function assesses the outcome of a particular readiness test by examining its result. - It logs the outcome using varying log levels (info, warning, error), determined by the - test's importance and its result. If a test is marked as critical and fails, the script - may terminate execution. + This function queries a firewall to determine its HA deployment status. It can identify if the firewall + operates in a standalone mode, as part of an HA pair (either active/passive or active/active), or within + a cluster configuration. It fetches and logs both the deployment status and, if applicable, detailed + configuration information about the HA setup. Parameters ---------- - result : dict - A dictionary where each key corresponds to a readiness test name. The value is another dictionary - containing two keys: 'state' (a boolean indicating the test's success or failure) and 'reason' - (a string explaining the outcome). + firewall : Firewall + An instance of the Firewall class representing the firewall whose HA status is to be assessed. - test_name : str - The name of the test to evaluate. This name should correspond to a key in the 'result' dictionary. + Returns + ------- + Tuple[str, Optional[dict]] + A tuple containing two elements: + - A string indicating the HA deployment type (e.g., 'standalone', 'active/passive', 'active/active'). + - An optional dictionary with detailed HA configuration information. The dictionary is provided if + the firewall is part of an HA setup; otherwise, None is returned. - test_info : dict - Information about the test, including its description, log level (info, warning, error), and a flag - indicating whether to exit the script upon test failure (exit_on_failure). + Example + ------- + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> ha_status, ha_details = get_ha_status(firewall) + >>> print(ha_status) # Example output: 'active/passive' + >>> print(ha_details) # Example output: {'ha_details': {...}} Notes ----- - - The function utilizes the `get_emoji` helper function to add appropriate emojis to log messages, - enhancing readability and user experience. - - If 'state' in the test result is True, the test is logged as passed. Otherwise, it is either - logged as failed or skipped, based on the specified log level in 'test_info'. - - Raises - ------ - SystemExit - If a critical test (marked with "exit_on_failure": True) fails, the script will raise SystemExit. + - This function uses the 'show_highavailability_state' method from the Firewall class to retrieve HA status. + - For processing the XML response, it employs the 'flatten_xml_to_dict' helper function to translate the + data into a Python dictionary, providing a more accessible format for further operations or analysis. """ - test_result = result.get( - test_name, {"state": False, "reason": "Test not performed"} + logging.debug( + f"{get_emoji('start')} Getting {firewall.serial} deployment information..." ) - log_message = f'{test_info["description"]} - {test_result["reason"]}' + deployment_type = firewall.show_highavailability_state() + logging.debug(f"{get_emoji('report')} Firewall deployment: {deployment_type[0]}") - if test_result["state"]: - logging.info( - f"{get_emoji('success')} Passed Readiness Check: {test_info['description']}" + if deployment_type[1]: + ha_details = flatten_xml_to_dict(deployment_type[1]) + logging.debug( + f"{get_emoji('report')} Firewall deployment details: {ha_details}" ) + return deployment_type[0], ha_details else: - if test_info["log_level"] == "error": - logging.error(f"{get_emoji('error')} {log_message}") - if test_info["exit_on_failure"]: - logging.error(f"{get_emoji('stop')} Halting script.") - - sys.exit(1) - elif test_info["log_level"] == "warning": - logging.debug( - f"{get_emoji('report')} Skipped Readiness Check: {test_info['description']}" - ) - else: - logging.debug(log_message) + return deployment_type[0], None -# ---------------------------------------------------------------------------- -# Setting up connection to either Panorama or PAN-OS firewall appliance -# ---------------------------------------------------------------------------- -def connect_to_host( - hostname: str, - api_username: str, - api_password: str, -) -> PanDevice: +def handle_ha_logic( + firewall: Firewall, + target_version: str, + dry_run: bool, +) -> Tuple[bool, Optional[Firewall]]: """ - Establishes a connection to a Panorama or PAN-OS firewall appliance using provided credentials. - - This function uses the hostname, username, and password to attempt a connection to a target appliance, - which can be either a Panorama management server or a PAN-OS firewall. It identifies the type of - appliance based on the provided credentials and hostname. Upon successful connection, it returns an - appropriate PanDevice object (either Panorama or Firewall). + Handles the logic specific to High Availability (HA) configurations. Parameters ---------- - hostname : str - The DNS Hostname or IP address of the target appliance. - api_username : str - Username for authentication. - api_password : str - Password for authentication. + firewall : Firewall + The firewall instance to evaluate for HA logic. + target_version : str + The target PAN-OS version for the upgrade. + dry_run : bool + If True, simulates the logic without making changes. Returns ------- - PanDevice - An instance of PanDevice (either Panorama or Firewall), representing the established connection. - - Raises - ------ - SystemExit - If the connection attempt fails, such as due to a timeout, incorrect credentials, or other errors. + Tuple[bool, Optional[Firewall]] + A tuple where the first element is a boolean indicating whether to proceed with the upgrade, + and the second element is an optional Firewall object representing the peer firewall if the + current firewall is not the target for upgrade. - Example - -------- - Connecting to a Panorama management server: - >>> connect_to_host('panorama.example.com', 'admin', 'password') - - - Connecting to a PAN-OS firewall: - >>> connect_to_host('192.168.0.1', 'admin', 'password') - + Notes + ----- + - This function determines if the firewall is part of an HA pair and its role (active/passive). + - It evaluates if the HA peer firewall needs to be upgraded first. + - In dry run mode, it simulates the HA logic without performing actual operations. """ - try: - target_device = PanDevice.create_from_device( - hostname, - api_username, - api_password, - ) - - return target_device - - except PanConnectionTimeout: - logging.error( - f"{get_emoji('error')} Connection to the {hostname} appliance timed out. Please check the DNS hostname or IP address and network connectivity." - ) - - sys.exit(1) - - except Exception as e: - logging.error( - f"{get_emoji('error')} An error occurred while connecting to the {hostname} appliance: {e}" - ) + deploy_info, ha_details = get_ha_status(firewall) - sys.exit(1) + # If the firewall is not part of an HA configuration, proceed with the upgrade + if not ha_details: + return True, None + + local_state = ha_details["result"]["group"]["local-info"]["state"] + local_version = ha_details["result"]["group"]["local-info"]["build-rel"] + peer_version = ha_details["result"]["group"]["peer-info"]["build-rel"] + version_comparison = compare_versions(local_version, peer_version) + + # If the active and passive firewalls are running the same version + if version_comparison == "equal": + if local_state == "active": + # Target the passive firewall first + logging.debug(f"{get_emoji('report')} Firewall is active") + peer_firewall = create_peer_firewall(firewall) + if peer_firewall: + logging.debug( + f"{get_emoji('report')} Peer firewall: {peer_firewall.about()}" + ) + return False, peer_firewall + elif local_state == "passive": + # Continue with upgrade process on the passive firewall + logging.debug(f"{get_emoji('report')} Firewall is passive") + return True, None + + elif version_comparison == "older": + logging.debug(f"{get_emoji('report')} Firewall is on an older version") + # Suspend HA state of active if the passive is on a later release + if local_state == "active" and not dry_run: + logging.debug(f"{get_emoji('report')} Suspending HA state of active") + suspend_ha_active(firewall) + return True, None + + elif version_comparison == "newer": + logging.debug(f"{get_emoji('report')} Firewall is on a newer version") + # Suspend HA state of passive if the active is on a later release + if local_state == "passive" and not dry_run: + logging.debug(f"{get_emoji('report')} Suspending HA state of passive") + suspend_ha_passive(firewall) + return True, None + + return False, None -# ---------------------------------------------------------------------------- -# Determine if an upgrade is suitable -# ---------------------------------------------------------------------------- -def determine_upgrade( +def perform_readiness_checks( firewall: Firewall, - target_major: int, - target_minor: int, - target_maintenance: Union[int, str], + hostname: str, + file_path: str, ) -> None: """ - Determines the necessity of an upgrade for a firewall to a specific PAN-OS version. + Executes readiness checks on a specified firewall and saves the results as a JSON file. - This function assesses if upgrading the firewall's PAN-OS version is required by comparing its current - version with the specified target version. The target version is defined by major, minor, and maintenance - version numbers, where the maintenance version can also include hotfix information. The function logs - the current and target versions, and establishes the need for an upgrade if the current version is lower - than the target. If the current version is equal to or higher than the target, it suggests that an upgrade - is unnecessary or a downgrade is being attempted, leading to termination of the script. + This function initiates a series of readiness checks on the firewall to assess its state before + proceeding with operations like upgrades. The checks cover aspects like configuration status, + content version, license validity, HA status, and more. The results of these checks are logged, + and a detailed report is generated and saved to the provided file path. Parameters ---------- firewall : Firewall - The instance of the Firewall whose PAN-OS version is being evaluated. - target_major : int - Major version number of the target PAN-OS. - target_minor : int - Minor version number of the target PAN-OS. - target_maintenance : Union[int, str] - Maintenance or hotfix version number of the target PAN-OS, can be an integer or string. - - Raises - ------ - SystemExit - Exits the script if the target version is not an upgrade, indicating either a downgrade attempt - or that the current version already meets or exceeds the target version. + The firewall instance on which to perform the readiness checks. + hostname : str + Hostname of the firewall, used primarily for logging purposes. + file_path : str + Path to the file where the readiness check report JSON will be saved. Notes ----- - - Parses the PAN-OS version strings into tuples of integers for accurate comparison. - - Utilizes emojis in logging for clear and user-friendly status indication. - """ - - def parse_version(version: str) -> Tuple[int, int, int, int]: - parts = version.split(".") - if len(parts) == 2: # When maintenance version is an integer - major, minor = parts - maintenance, hotfix = 0, 0 - else: # When maintenance version includes hotfix - major, minor, maintenance = parts - if "-h" in maintenance: - maintenance, hotfix = maintenance.split("-h") - else: - hotfix = 0 - - return int(major), int(minor), int(maintenance), int(hotfix) + - Utilizes the `run_assurance` function to perform the readiness checks. + - Ensures the existence of the directory where the report file will be saved. + - Logs the outcome of the readiness checks and saves the report in JSON format. + - Logs an error message if the readiness check creation fails. - current_version = parse_version(firewall.version) + Example + -------- + Conducting readiness checks: + >>> firewall = Firewall(hostname='192.168.1.1', 'username', 'password') + >>> perform_readiness_checks(firewall, 'firewall1', '/path/to/readiness_report.json') + # Readiness report is saved to the specified path. + """ - if isinstance(target_maintenance, int): - # Handling integer maintenance version separately - target_version = (target_major, target_minor, target_maintenance, 0) - else: - # Handling string maintenance version with hotfix - target_version = parse_version( - f"{target_major}.{target_minor}.{target_maintenance}" - ) + logging.debug( + f"{get_emoji('start')} Performing readiness checks of target firewall..." + ) - logging.info(f"{get_emoji('report')} Current PAN-OS version: {firewall.version}") - logging.info( - f"{get_emoji('report')} Target PAN-OS version: {target_major}.{target_minor}.{target_maintenance}" + readiness_check = run_assurance( + firewall, + hostname, + operation_type="readiness_check", + actions=[ + "candidate_config", + "content_version", + "expired_licenses", + "ha", + # "jobs", + "free_disk_space", + "ntp_sync", + "panorama", + "planes_clock_sync", + ], + config={}, ) - upgrade_needed = current_version < target_version - if upgrade_needed: - logging.info( - f"{get_emoji('success')} Confirmed that moving from {firewall.version} to {target_major}.{target_minor}.{target_maintenance} is an upgrade" - ) - return + # Check if a readiness check was successfully created + if isinstance(readiness_check, ReadinessCheckReport): + # Do something with the readiness check report, e.g., log it, save it, etc. + logging.info(f"{get_emoji('success')} Readiness Checks completed") + readiness_check_report_json = readiness_check.model_dump_json(indent=4) + logging.debug(readiness_check_report_json) - else: - logging.error( - f"{get_emoji('error')} Upgrade is not required or a downgrade was attempted." - ) - logging.error(f"{get_emoji('stop')} Halting script.") + ensure_directory_exists(file_path) - sys.exit(1) + with open(file_path, "w") as file: + file.write(readiness_check_report_json) + + logging.debug( + f"{get_emoji('save')} Readiness checks completed for {hostname}, saved to {file_path}" + ) + else: + logging.error(f"{get_emoji('error')} Failed to create readiness check") -# ---------------------------------------------------------------------------- -# Determine the firewall's PAN-OS version and any available updates -# ---------------------------------------------------------------------------- -def software_update_check( +def perform_reboot( firewall: Firewall, - version: str, - ha_details: dict, -) -> bool: + target_version: str, + ha_details: Optional[dict] = None, +) -> None: """ - Verifies the availability and readiness of a specified PAN-OS version for upgrade on a firewall. + Initiates and oversees the reboot process of a firewall, ensuring it reaches the specified target version. - This function checks if the target PAN-OS version is available for upgrade on the specified firewall. - It first refreshes the firewall's system information to ensure current data, then uses the - `determine_upgrade` function to validate if the target version is an upgrade compared to the current - version. It checks the list of available PAN-OS versions and verifies if the base image for the - target version is downloaded. The function returns True if the target version is available and the - base image is downloaded, and False if the version is not available, the base image is not downloaded, - or a downgrade attempt is identified. + This function triggers a reboot of the specified firewall and monitors its status throughout the process. + In HA (High Availability) setups, it confirms synchronization with the HA peer post-reboot. The function + includes robust handling of various states and errors, with detailed logging. It verifies the firewall + reaches the target PAN-OS version upon reboot completion. Parameters ---------- firewall : Firewall - The firewall instance to be checked for software update availability. - version : str - The target PAN-OS version intended for the upgrade. - ha_details : dict - High-availability (HA) details of the firewall. Used to assess if HA synchronization is required for the update. - - Returns - ------- - bool - True if the target PAN-OS version is available and ready for upgrade, False otherwise. + The firewall instance to be rebooted. + target_version : str + The target PAN-OS version to confirm after reboot. + ha_details : Optional[dict], optional + High Availability details of the firewall, if applicable. Default is None. Raises ------ SystemExit - Exits the script if a downgrade attempt is identified or if the target version is not suitable for an upgrade. + Exits the script if the firewall fails to reboot to the target version, if HA synchronization issues + occur, or if critical errors are encountered during the reboot process. + + Notes + ----- + - The function checks the firewall's version and HA synchronization status (if applicable) post-reboot. + - Confirms that the firewall has successfully rebooted to the target PAN-OS version. + - Script terminates if the firewall doesn't reach the target version or synchronize (in HA setups) within + 20 minutes. Example - -------- - >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> software_update_check(firewall, '10.1.0', ha_details={}) - True # If the version 10.1.0 is available and ready for upgrade + ------- + Rebooting a firewall to a specific PAN-OS version: + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> perform_reboot(firewall, '10.2.0') + # The firewall undergoes a reboot and the script monitors until it reaches the target version 10.2.0. """ - # parse version - major, minor, maintenance = version.split(".") - # Make sure we know about the system details - if we have connected via Panorama, this can be null without this. - logging.debug("Refreshing running system information") - firewall.refresh_system_info() + reboot_start_time = time.time() + rebooted = False - # check to see if the specified version is older than the current version - determine_upgrade(firewall, major, minor, maintenance) + # Check if HA details are available + if ha_details: + logging.info(f"{get_emoji('start')} Rebooting the passive HA firewall...") - # retrieve available versions of PAN-OS - firewall.software.check() - available_versions = firewall.software.versions - logging.debug(f"Available PAN-OS versions: {available_versions}") + # Reboot standalone firewall + else: + logging.info(f"{get_emoji('start')} Rebooting the standalone firewall...") - # check to see if specified version is available for upgrade - if version in available_versions: - logging.info( - f"{get_emoji('success')} PAN-OS version {version} is available for download" - ) + reboot_job = firewall.op( + "", cmd_xml=False + ) + reboot_job_result = flatten_xml_to_dict(reboot_job) + logging.info(f"{get_emoji('report')} {reboot_job_result['result']}") - # validate the specified version's base image is already downloaded - if available_versions[f"{major}.{minor}.0"]["downloaded"]: - logging.info( - f"{get_emoji('success')} Base image for {version} is already downloaded" - ) - return True + # Wait for the firewall reboot process to initiate before checking status + time.sleep(60) + + while not rebooted: + # Check if HA details are available + if ha_details: + try: + deploy_info, current_ha_details = get_ha_status(firewall) + logging.debug( + f"{get_emoji('report')} deploy_info: {deploy_info}", + ) + logging.debug( + f"{get_emoji('report')} current_ha_details: {current_ha_details}", + ) + + if current_ha_details and deploy_info in ["active", "passive"]: + if ( + current_ha_details["result"]["group"]["running-sync"] + == "synchronized" + ): + logging.info( + f"{get_emoji('success')} HA passive firewall rebooted and synchronized with its peer in {int(time.time() - reboot_start_time)} seconds" + ) + rebooted = True + else: + logging.info( + f"{get_emoji('working')} HA passive firewall rebooted but not yet synchronized with its peer. Will try again in 30 seconds." + ) + time.sleep(60) + except (PanXapiError, PanConnectionTimeout, PanURLError): + logging.info(f"{get_emoji('working')} Firewall is rebooting...") + time.sleep(60) + # Reboot standalone firewall else: + try: + firewall.refresh_system_info() + logging.info( + f"{get_emoji('report')} Firewall version: {firewall.version}" + ) + + if firewall.version == target_version: + logging.info( + f"{get_emoji('success')} Firewall rebooted in {int(time.time() - reboot_start_time)} seconds" + ) + rebooted = True + else: + logging.error( + f"{get_emoji('stop')} Firewall rebooted but running the target version. Please try again." + ) + sys.exit(1) + except (PanXapiError, PanConnectionTimeout, PanURLError): + logging.info(f"{get_emoji('working')} Firewall is rebooting...") + time.sleep(60) + + # Check if 20 minutes have passed + if time.time() - reboot_start_time > 1200: # 20 minutes in seconds logging.error( - f"{get_emoji('error')} Base image for {version} is not downloaded" + f"{get_emoji('error')} Firewall did not become available and/or establish a Connected sync state with its HA peer after 20 minutes. Please check the firewall status manually." ) - return False - else: - logging.error( - f"{get_emoji('error')} PAN-OS version {version} is not available for download" - ) - return False + break -# ---------------------------------------------------------------------------- -# Determine if the firewall is standalone, HA, or in a cluster -# ---------------------------------------------------------------------------- -def get_ha_status(firewall: Firewall) -> Tuple[str, Optional[dict]]: +def perform_snapshot( + firewall: Firewall, + hostname: str, + file_path: str, +) -> None: """ - Determines the High-Availability (HA) deployment status and configuration of a specified Firewall appliance. + Executes a network state snapshot on the specified firewall and saves it as a JSON file. - This function queries a firewall to determine its HA deployment status. It can identify if the firewall - operates in a standalone mode, as part of an HA pair (either active/passive or active/active), or within - a cluster configuration. It fetches and logs both the deployment status and, if applicable, detailed - configuration information about the HA setup. + This function collects various network state information from the firewall, such as ARP table, content version, + IPsec tunnels, etc. The collected data is then serialized into JSON format and saved to the provided file path. + It logs the beginning of the operation, its success, or any failures encountered during the snapshot creation. Parameters ---------- firewall : Firewall - An instance of the Firewall class representing the firewall whose HA status is to be assessed. - - Returns - ------- - Tuple[str, Optional[dict]] - A tuple containing two elements: - - A string indicating the HA deployment type (e.g., 'standalone', 'active/passive', 'active/active'). - - An optional dictionary with detailed HA configuration information. The dictionary is provided if - the firewall is part of an HA setup; otherwise, None is returned. - - Example - ------- - >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> ha_status, ha_details = get_ha_status(firewall) - >>> print(ha_status) # Example output: 'active/passive' - >>> print(ha_details) # Example output: {'ha_details': {...}} + The firewall instance from which to collect the network state information. + hostname : str + Hostname of the firewall, used primarily for logging purposes. + file_path : str + Path to the file where the snapshot JSON will be saved. Notes ----- - - This function uses the 'show_highavailability_state' method from the Firewall class to retrieve HA status. - - For processing the XML response, it employs the 'flatten_xml_to_dict' helper function to translate the - data into a Python dictionary, providing a more accessible format for further operations or analysis. - """ - logging.debug( - f"{get_emoji('start')} Getting {firewall.serial} deployment information..." - ) - deployment_type = firewall.show_highavailability_state() - logging.debug(f"{get_emoji('report')} Firewall deployment: {deployment_type[0]}") + - Utilizes the `run_assurance` function to collect the required network state information. + - Ensures the existence of the directory where the snapshot file will be saved. + - Logs a success message and the JSON representation of the snapshot if the operation is successful. + - Logs an error message if the snapshot creation fails. - if deployment_type[1]: - ha_details = flatten_xml_to_dict(deployment_type[1]) - logging.debug( - f"{get_emoji('report')} Firewall deployment details: {ha_details}" + Example + -------- + Creating a network state snapshot: + >>> firewall = Firewall(hostname='192.168.1.1', 'admin', 'password') + >>> perform_snapshot(firewall, 'firewall1', '/path/to/snapshot.json') + # Snapshot file is saved to the specified path. + """ + + logging.info( + f"{get_emoji('start')} Performing snapshot of network state information..." + ) + + # take snapshots + network_snapshot = run_assurance( + firewall, + hostname, + operation_type="state_snapshot", + actions=[ + "arp_table", + "content_version", + "ip_sec_tunnels", + "license", + "nics", + "routes", + "session_stats", + ], + config={}, + ) + + # Check if a readiness check was successfully created + if isinstance(network_snapshot, SnapshotReport): + logging.info(f"{get_emoji('success')} Network snapshot created successfully") + network_snapshot_json = network_snapshot.model_dump_json(indent=4) + logging.debug(network_snapshot_json) + + ensure_directory_exists(file_path) + + with open(file_path, "w") as file: + file.write(network_snapshot_json) + + logging.debug( + f"{get_emoji('save')} Network state snapshot collected from {hostname}, saved to {file_path}" ) - return deployment_type[0], ha_details else: - return deployment_type[0], None + logging.error(f"{get_emoji('error')} Failed to create snapshot") -# ---------------------------------------------------------------------------- -# Download the target PAN-OS version -# ---------------------------------------------------------------------------- -def software_download( +def perform_upgrade( firewall: Firewall, + hostname: str, target_version: str, - ha_details: dict, -) -> bool: + ha_details: Optional[dict] = None, + max_retries: int = 3, + retry_interval: int = 60, +) -> None: """ - Initiates and monitors the download of a specified PAN-OS software version on the firewall. + Initiates and manages the upgrade process of a firewall to a specified PAN-OS version. - This function starts the download process for the given target PAN-OS version on the specified - firewall. It continually checks and logs the download's progress. If the download is successful, - it returns True. If the download process encounters errors or fails, these are logged, and the - function returns False. Exceptions during the download process lead to script termination. + This function attempts to upgrade the firewall to the given PAN-OS version, handling potential issues + and retrying if necessary. It deals with High Availability (HA) considerations and ensures that the + upgrade process is robust against temporary failures or busy states. The function logs each step of the + process and exits the script if critical errors occur. Parameters ---------- firewall : Firewall - The Firewall instance on which the software is to be downloaded. + The firewall instance to be upgraded. + hostname : str + The hostname of the firewall, used for logging purposes. target_version : str - The PAN-OS version targeted for download. - ha_details : dict - High-availability details of the firewall, determining if HA synchronization is needed. - - Returns - ------- - bool - True if the download is successful, False if the download fails or encounters an error. + The target PAN-OS version for the upgrade. + ha_details : Optional[dict], optional + High Availability details of the firewall, by default None. + max_retries : int, optional + The maximum number of retry attempts for the upgrade, by default 3. + retry_interval : int, optional + The interval (in seconds) to wait between retry attempts, by default 60. Raises ------ SystemExit - Raised if an exception occurs during the download process or if a critical error is encountered. - - Example - -------- - Initiating a PAN-OS version download: - >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> software_download(firewall, '10.1.0', ha_details={}) - True or False depending on the success of the download + Exits the script if the upgrade job fails, if HA synchronization issues occur, + or if critical errors are encountered during the upgrade process. Notes ----- - - Before initiating the download, the function checks if the target version is already available on the firewall. - - It uses the 'download' method of the Firewall's software attribute to perform the download. - - The function sleeps for 30 seconds between each status check to allow time for the download to progress. - """ - - if firewall.software.versions[target_version]["downloaded"]: - logging.info( - f"{get_emoji('success')} PAN-OS version {target_version} already on firewall." - ) - return True + - The function handles retries based on the 'max_retries' and 'retry_interval' parameters. + - In case of 'software manager is currently in use' errors, retries are attempted. + - Critical errors during the upgrade process lead to script termination. - if ( - not firewall.software.versions[target_version]["downloaded"] - or firewall.software.versions[target_version]["downloaded"] != "downloading" - ): - logging.info( - f"{get_emoji('search')} PAN-OS version {target_version} is not on the firewall" - ) + Example + ------- + Upgrading a firewall to a specific PAN-OS version: + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> perform_upgrade(firewall, '192.168.1.1', '10.2.0', max_retries=2, retry_interval=30) + # The firewall is upgraded to PAN-OS version 10.2.0, with retries if necessary. + """ - start_time = time.time() + logging.info( + f"{get_emoji('start')} Performing upgrade on {hostname} to version {target_version}..." + ) + attempt = 0 + while attempt < max_retries: try: logging.info( - f"{get_emoji('start')} PAN-OS version {target_version} is beginning download" + f"{get_emoji('start')} Attempting upgrade {hostname} to version {target_version} (Attempt {attempt + 1} of {max_retries})..." ) - firewall.software.download(target_version) - except PanDeviceXapiError as download_error: - logging.error(f"{get_emoji('error')} {download_error}") - - sys.exit(1) - - while True: - firewall.software.info() - dl_status = firewall.software.versions[target_version]["downloaded"] - elapsed_time = int(time.time() - start_time) + install_job = firewall.software.install(target_version, sync=True) - if dl_status is True: + if install_job["success"]: logging.info( - f"{get_emoji('success')} {target_version} downloaded in {elapsed_time} seconds", - ) - return True - elif dl_status in (False, "downloading"): - # Consolidate logging for both 'False' and 'downloading' states - status_msg = ( - "Download is starting" - if dl_status is False - else f"Downloading PAN-OS version {target_version}" + f"{get_emoji('success')} {hostname} upgrade completed successfully" ) - if ha_details: + logging.debug(f"{get_emoji('report')} {install_job}") + break # Exit loop on successful upgrade + else: + logging.error(f"{get_emoji('error')} {hostname} upgrade job failed.") + attempt += 1 + if attempt < max_retries: logging.info( - f"{get_emoji('working')} {status_msg} - HA will sync image - Elapsed time: {elapsed_time} seconds" + f"{get_emoji('warning')} Retrying in {retry_interval} seconds..." ) - else: - logging.info(f"{status_msg} - Elapsed time: {elapsed_time} seconds") + time.sleep(retry_interval) + + except PanDeviceError as upgrade_error: + logging.error( + f"{get_emoji('error')} {hostname} upgrade error: {upgrade_error}" + ) + error_message = str(upgrade_error) + if "software manager is currently in use" in error_message: + attempt += 1 + if attempt < max_retries: + logging.info( + f"{get_emoji('warning')} Software manager is busy. Retrying in {retry_interval} seconds..." + ) + time.sleep(retry_interval) else: logging.error( - f"{get_emoji('error')} Download failed after {elapsed_time} seconds" + f"{get_emoji('stop')} Critical error during upgrade. Halting script." ) - return False - - time.sleep(30) - - else: - logging.error(f"{get_emoji('error')} Error downloading {target_version}.") - - sys.exit(1) + sys.exit(1) -# ---------------------------------------------------------------------------- -# Handle panos-upgrade-assurance operations -# ---------------------------------------------------------------------------- def run_assurance( firewall: Firewall, hostname: str, @@ -1070,482 +1085,755 @@ def run_assurance( return results -# ---------------------------------------------------------------------------- -# Perform the snapshot of the network state -# ---------------------------------------------------------------------------- -def perform_snapshot( +def software_download( firewall: Firewall, - hostname: str, - file_path: str, -) -> None: + target_version: str, + ha_details: dict, +) -> bool: """ - Executes a network state snapshot on the specified firewall and saves it as a JSON file. + Initiates and monitors the download of a specified PAN-OS software version on the firewall. - This function collects various network state information from the firewall, such as ARP table, content version, - IPsec tunnels, etc. The collected data is then serialized into JSON format and saved to the provided file path. - It logs the beginning of the operation, its success, or any failures encountered during the snapshot creation. + This function starts the download process for the given target PAN-OS version on the specified + firewall. It continually checks and logs the download's progress. If the download is successful, + it returns True. If the download process encounters errors or fails, these are logged, and the + function returns False. Exceptions during the download process lead to script termination. Parameters ---------- firewall : Firewall - The firewall instance from which to collect the network state information. - hostname : str - Hostname of the firewall, used primarily for logging purposes. - file_path : str - Path to the file where the snapshot JSON will be saved. + The Firewall instance on which the software is to be downloaded. + target_version : str + The PAN-OS version targeted for download. + ha_details : dict + High-availability details of the firewall, determining if HA synchronization is needed. - Notes - ----- - - Utilizes the `run_assurance` function to collect the required network state information. - - Ensures the existence of the directory where the snapshot file will be saved. - - Logs a success message and the JSON representation of the snapshot if the operation is successful. - - Logs an error message if the snapshot creation fails. + Returns + ------- + bool + True if the download is successful, False if the download fails or encounters an error. + + Raises + ------ + SystemExit + Raised if an exception occurs during the download process or if a critical error is encountered. Example -------- - Creating a network state snapshot: - >>> firewall = Firewall(hostname='192.168.1.1', 'admin', 'password') - >>> perform_snapshot(firewall, 'firewall1', '/path/to/snapshot.json') - # Snapshot file is saved to the specified path. - """ + Initiating a PAN-OS version download: + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> software_download(firewall, '10.1.0', ha_details={}) + True or False depending on the success of the download - logging.info( - f"{get_emoji('start')} Performing snapshot of network state information..." - ) + Notes + ----- + - Before initiating the download, the function checks if the target version is already available on the firewall. + - It uses the 'download' method of the Firewall's software attribute to perform the download. + - The function sleeps for 30 seconds between each status check to allow time for the download to progress. + """ - # take snapshots - network_snapshot = run_assurance( - firewall, - hostname, - operation_type="state_snapshot", - actions=[ - "arp_table", - "content_version", - "ip_sec_tunnels", - "license", - "nics", - "routes", - "session_stats", - ], - config={}, - ) + if firewall.software.versions[target_version]["downloaded"]: + logging.info( + f"{get_emoji('success')} PAN-OS version {target_version} already on firewall." + ) + return True - # Check if a readiness check was successfully created - if isinstance(network_snapshot, SnapshotReport): - logging.info(f"{get_emoji('success')} Network snapshot created successfully") - network_snapshot_json = network_snapshot.model_dump_json(indent=4) - logging.debug(network_snapshot_json) + if ( + not firewall.software.versions[target_version]["downloaded"] + or firewall.software.versions[target_version]["downloaded"] != "downloading" + ): + logging.info( + f"{get_emoji('search')} PAN-OS version {target_version} is not on the firewall" + ) - ensure_directory_exists(file_path) + start_time = time.time() - with open(file_path, "w") as file: - file.write(network_snapshot_json) + try: + logging.info( + f"{get_emoji('start')} PAN-OS version {target_version} is beginning download" + ) + firewall.software.download(target_version) + except PanDeviceXapiError as download_error: + logging.error(f"{get_emoji('error')} {download_error}") + + sys.exit(1) + + while True: + firewall.software.info() + dl_status = firewall.software.versions[target_version]["downloaded"] + elapsed_time = int(time.time() - start_time) + + if dl_status is True: + logging.info( + f"{get_emoji('success')} {target_version} downloaded in {elapsed_time} seconds", + ) + return True + elif dl_status in (False, "downloading"): + # Consolidate logging for both 'False' and 'downloading' states + status_msg = ( + "Download is starting" + if dl_status is False + else f"Downloading PAN-OS version {target_version}" + ) + if ha_details: + logging.info( + f"{get_emoji('working')} {status_msg} - HA will sync image - Elapsed time: {elapsed_time} seconds" + ) + else: + logging.info(f"{status_msg} - Elapsed time: {elapsed_time} seconds") + else: + logging.error( + f"{get_emoji('error')} Download failed after {elapsed_time} seconds" + ) + return False + + time.sleep(30) - logging.debug( - f"{get_emoji('save')} Network state snapshot collected from {hostname}, saved to {file_path}" - ) else: - logging.error(f"{get_emoji('error')} Failed to create snapshot") + logging.error(f"{get_emoji('error')} Error downloading {target_version}.") + + sys.exit(1) -# ---------------------------------------------------------------------------- -# Perform the readiness checks -# ---------------------------------------------------------------------------- -def perform_readiness_checks( +def software_update_check( firewall: Firewall, - hostname: str, - file_path: str, -) -> None: + version: str, + ha_details: dict, +) -> bool: """ - Executes readiness checks on a specified firewall and saves the results as a JSON file. + Verifies the availability and readiness of a specified PAN-OS version for upgrade on a firewall. - This function initiates a series of readiness checks on the firewall to assess its state before - proceeding with operations like upgrades. The checks cover aspects like configuration status, - content version, license validity, HA status, and more. The results of these checks are logged, - and a detailed report is generated and saved to the provided file path. + This function checks if the target PAN-OS version is available for upgrade on the specified firewall. + It first refreshes the firewall's system information to ensure current data, then uses the + `determine_upgrade` function to validate if the target version is an upgrade compared to the current + version. It checks the list of available PAN-OS versions and verifies if the base image for the + target version is downloaded. The function returns True if the target version is available and the + base image is downloaded, and False if the version is not available, the base image is not downloaded, + or a downgrade attempt is identified. Parameters ---------- firewall : Firewall - The firewall instance on which to perform the readiness checks. - hostname : str - Hostname of the firewall, used primarily for logging purposes. - file_path : str - Path to the file where the readiness check report JSON will be saved. + The firewall instance to be checked for software update availability. + version : str + The target PAN-OS version intended for the upgrade. + ha_details : dict + High-availability (HA) details of the firewall. Used to assess if HA synchronization is required for the update. - Notes - ----- - - Utilizes the `run_assurance` function to perform the readiness checks. - - Ensures the existence of the directory where the report file will be saved. - - Logs the outcome of the readiness checks and saves the report in JSON format. - - Logs an error message if the readiness check creation fails. + Returns + ------- + bool + True if the target PAN-OS version is available and ready for upgrade, False otherwise. + + Raises + ------ + SystemExit + Exits the script if a downgrade attempt is identified or if the target version is not suitable for an upgrade. Example -------- - Conducting readiness checks: - >>> firewall = Firewall(hostname='192.168.1.1', 'username', 'password') - >>> perform_readiness_checks(firewall, 'firewall1', '/path/to/readiness_report.json') - # Readiness report is saved to the specified path. + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> software_update_check(firewall, '10.1.0', ha_details={}) + True # If the version 10.1.0 is available and ready for upgrade """ + # parse version + major, minor, maintenance = version.split(".") - logging.debug( - f"{get_emoji('start')} Performing readiness checks of target firewall..." - ) + # Make sure we know about the system details - if we have connected via Panorama, this can be null without this. + logging.debug("Refreshing running system information") + firewall.refresh_system_info() - readiness_check = run_assurance( - firewall, - hostname, - operation_type="readiness_check", - actions=[ - "candidate_config", - "content_version", - "expired_licenses", - "ha", - # "jobs", - "free_disk_space", - "ntp_sync", - "panorama", - "planes_clock_sync", - ], - config={}, - ) + # check to see if the specified version is older than the current version + determine_upgrade(firewall, major, minor, maintenance) - # Check if a readiness check was successfully created - if isinstance(readiness_check, ReadinessCheckReport): - # Do something with the readiness check report, e.g., log it, save it, etc. - logging.info(f"{get_emoji('success')} Readiness Checks completed") - readiness_check_report_json = readiness_check.model_dump_json(indent=4) - logging.debug(readiness_check_report_json) + # retrieve available versions of PAN-OS + firewall.software.check() + available_versions = firewall.software.versions + logging.debug(f"Available PAN-OS versions: {available_versions}") - ensure_directory_exists(file_path) + # check to see if specified version is available for upgrade + if version in available_versions: + logging.info( + f"{get_emoji('success')} PAN-OS version {version} is available for download" + ) - with open(file_path, "w") as file: - file.write(readiness_check_report_json) + # validate the specified version's base image is already downloaded + if available_versions[f"{major}.{minor}.0"]["downloaded"]: + logging.info( + f"{get_emoji('success')} Base image for {version} is already downloaded" + ) + return True - logging.debug( - f"{get_emoji('save')} Readiness checks completed for {hostname}, saved to {file_path}" - ) + else: + logging.error( + f"{get_emoji('error')} Base image for {version} is not downloaded" + ) + return False else: - logging.error(f"{get_emoji('error')} Failed to create readiness check") + logging.error( + f"{get_emoji('error')} PAN-OS version {version} is not available for download" + ) + return False -# ---------------------------------------------------------------------------- -# Back up the configuration -# ---------------------------------------------------------------------------- -def backup_configuration( - firewall: Firewall, - file_path: str, -) -> bool: +def suspend_ha_active(firewall: Firewall) -> bool: """ - Backs up the current running configuration of a specified firewall to a local file. - - This function retrieves the running configuration from the firewall and saves it as an XML file - at the specified file path. It checks the validity of the retrieved XML data and logs the success - or failure of the backup process. + Suspends the HA state of the active firewall in an HA pair. Parameters ---------- firewall : Firewall - The firewall instance from which the configuration is to be backed up. - file_path : str - The path where the configuration backup file will be saved. + The active firewall in the HA pair. Returns ------- bool - Returns True if the backup is successfully created, False otherwise. - - Raises - ------ - Exception - Raises an exception if any error occurs during the backup process. + Returns True if the HA state suspension is successful, False otherwise. Notes ----- - - The function verifies the XML structure of the retrieved configuration. - - Ensures the directory for the backup file exists. - - The backup file is saved in XML format. - - Example - -------- - Backing up the firewall configuration: - >>> firewall = Firewall(hostname='192.168.1.1', 'admin', 'password') - >>> backup_configuration(firewall, '/path/to/config_backup.xml') - # Configuration is backed up to the specified file. + - This function should be called only when it's confirmed that the firewall is the active member of an HA pair. + - It suspends the HA state to allow the passive firewall to become active. """ - try: - # Run operational command to retrieve configuration - config_xml = firewall.op("show config running") - if config_xml is None: + suspension_response = firewall.op( + "", + cmd_xml=False, + ) + if "success" in suspension_response.text: + logging.info(f"{get_emoji('success')} Active firewall HA state suspended.") + return True + else: logging.error( - f"{get_emoji('error')} Failed to retrieve running configuration." + f"{get_emoji('error')} Failed to suspend active firewall HA state." ) return False + except Exception as e: + logging.error( + f"{get_emoji('error')} Error suspending active firewall HA state: {e}" + ) + return False - # Check XML structure - if ( - config_xml.tag != "response" - or len(config_xml) == 0 - or config_xml[0].tag != "result" - ): - logging.error( - f"{get_emoji('error')} Unexpected XML structure in configuration data." - ) - return False - # Extract the configuration data from the tag - config_data = config_xml.find(".//result/config") +def suspend_ha_passive(firewall: Firewall) -> bool: + """ + Suspends the HA state of the passive firewall in an HA pair. - # Manually construct the string representation of the XML data - config_str = ET.tostring(config_data, encoding="unicode") - - # Ensure the directory exists - ensure_directory_exists(file_path) + Parameters + ---------- + firewall : Firewall + The passive firewall in the HA pair. - # Write the file to the local filesystem - with open(file_path, "w") as file: - file.write(config_str) + Returns + ------- + bool + Returns True if the HA state suspension is successful, False otherwise. - logging.debug( - f"{get_emoji('save')} Configuration backed up successfully to {file_path}" + Notes + ----- + - This function should be called only when it's confirmed that the firewall is the passive member of an HA pair. + - It suspends the HA state to prevent it from becoming active during the upgrade process. + """ + try: + suspension_response = firewall.op( + "", + cmd_xml=False, ) - return True - + if "success" in suspension_response.text: + logging.info(f"{get_emoji('success')} Passive firewall HA state suspended.") + return True + else: + logging.error( + f"{get_emoji('error')} Failed to suspend passive firewall HA state." + ) + return False except Exception as e: - logging.error(f"{get_emoji('error')} Error backing up configuration: {e}") + logging.error( + f"{get_emoji('error')} Error suspending passive firewall HA state: {e}" + ) return False -# ---------------------------------------------------------------------------- -# Perform the upgrade process -# ---------------------------------------------------------------------------- -def perform_upgrade( +def upgrade_firewall( firewall: Firewall, - hostname: str, target_version: str, - ha_details: Optional[dict] = None, - max_retries: int = 3, - retry_interval: int = 60, + dry_run: bool, +) -> None: + pass + + +def upgrade_single_firewall( + firewall: Firewall, + target_version: str, + dry_run: bool, ) -> None: """ - Initiates and manages the upgrade process of a firewall to a specified PAN-OS version. + Manages the upgrade process for a single firewall appliance to a specified PAN-OS version. - This function attempts to upgrade the firewall to the given PAN-OS version, handling potential issues - and retrying if necessary. It deals with High Availability (HA) considerations and ensures that the - upgrade process is robust against temporary failures or busy states. The function logs each step of the - process and exits the script if critical errors occur. + This function orchestrates a series of steps to upgrade a firewall, including readiness checks, + software download, configuration backup, and the actual upgrade and reboot processes. It supports a + 'dry run' mode to simulate the upgrade process without applying changes. The function is designed to handle + both standalone firewalls and firewalls in a High Availability (HA) setup. Parameters ---------- firewall : Firewall - The firewall instance to be upgraded. - hostname : str - The hostname of the firewall, used for logging purposes. + An instance of the Firewall class representing the firewall to be upgraded. target_version : str - The target PAN-OS version for the upgrade. - ha_details : Optional[dict], optional - High Availability details of the firewall, by default None. - max_retries : int, optional - The maximum number of retry attempts for the upgrade, by default 3. - retry_interval : int, optional - The interval (in seconds) to wait between retry attempts, by default 60. + The target PAN-OS version to upgrade the firewall to. + dry_run : bool + If True, the function will simulate the upgrade process without making any changes. + If False, the function will proceed with the actual upgrade. - Raises - ------ - SystemExit - Exits the script if the upgrade job fails, if HA synchronization issues occur, - or if critical errors are encountered during the upgrade process. + Steps + ----- + 1. Refresh system information to ensure latest data is available. + 2. Determine if the firewall is standalone, part of HA, or in a cluster. + 3. Check firewall readiness for the specified target version. + 4. Download the target PAN-OS version if not already present. + 5. Perform pre-upgrade snapshots and readiness checks. + 6. Backup current configuration to the local filesystem. + 7. Proceed with upgrade and reboot if not a dry run. Notes ----- - - The function handles retries based on the 'max_retries' and 'retry_interval' parameters. - - In case of 'software manager is currently in use' errors, retries are attempted. - - Critical errors during the upgrade process lead to script termination. + - The script gracefully exits if the firewall is not ready for the upgrade. + - In HA setups, the script checks for synchronization status of the HA pair. + - In dry run mode, the script simulates the upgrade process without performing the actual upgrade. Example ------- Upgrading a firewall to a specific PAN-OS version: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> perform_upgrade(firewall, '192.168.1.1', '10.2.0', max_retries=2, retry_interval=30) - # The firewall is upgraded to PAN-OS version 10.2.0, with retries if necessary. + >>> upgrade_single_firewall(firewall, '10.1.0', dry_run=False) + # This will upgrade the firewall to PAN-OS version 10.1.0. """ - + # Refresh system information to ensure we have the latest data + logging.debug(f"{get_emoji('start')} Refreshing system information...") + firewall_details = SystemSettings.refreshall(firewall)[0] logging.info( - f"{get_emoji('start')} Performing upgrade on {hostname} to version {target_version}..." + f"{get_emoji('report')} {firewall.serial} {firewall_details.hostname} {firewall_details.ip_address}" ) - attempt = 0 - while attempt < max_retries: - try: - logging.info( - f"{get_emoji('start')} Attempting upgrade {hostname} to version {target_version} (Attempt {attempt + 1} of {max_retries})..." - ) - install_job = firewall.software.install(target_version, sync=True) + # Determine if the firewall is standalone, HA, or in a cluster + logging.debug( + f"{get_emoji('start')} Performing test to see if firewall is standalone, HA, or in a cluster..." + ) + deploy_info, ha_details = get_ha_status(firewall) + logging.info(f"{get_emoji('report')} Firewall HA mode: {deploy_info}") + logging.debug(f"{get_emoji('report')} Firewall HA details: {ha_details}") - if install_job["success"]: + # If firewall is part of HA pair, determine if it's active or passive + if ha_details: + proceed_with_upgrade, peer_firewall = handle_ha_logic( + firewall, target_version, dry_run + ) + + if not proceed_with_upgrade: + if peer_firewall: logging.info( - f"{get_emoji('success')} {hostname} upgrade completed successfully" + f"{get_emoji('start')} Switching control to the peer firewall for upgrade." ) - logging.debug(f"{get_emoji('report')} {install_job}") - break # Exit loop on successful upgrade - else: - logging.error(f"{get_emoji('error')} {hostname} upgrade job failed.") - attempt += 1 - if attempt < max_retries: - logging.info( - f"{get_emoji('warning')} Retrying in {retry_interval} seconds..." - ) - time.sleep(retry_interval) - - except PanDeviceError as upgrade_error: - logging.error( - f"{get_emoji('error')} {hostname} upgrade error: {upgrade_error}" - ) - error_message = str(upgrade_error) - if "software manager is currently in use" in error_message: - attempt += 1 - if attempt < max_retries: - logging.info( - f"{get_emoji('warning')} Software manager is busy. Retrying in {retry_interval} seconds..." - ) - time.sleep(retry_interval) + # Here you would add the logic to switch control to the peer firewall + # This could involve a recursive call to upgrade_single_firewall or a different approach + upgrade_single_firewall(peer_firewall, target_version, dry_run) + return else: logging.error( - f"{get_emoji('stop')} Critical error during upgrade. Halting script." + f"{get_emoji('error')} Unable to determine HA peer for upgrade." ) sys.exit(1) + # Check to see if the firewall is ready for an upgrade + logging.debug( + f"{get_emoji('start')} Performing test to validate firewall's readiness..." + ) + update_available = software_update_check(firewall, target_version, ha_details) + logging.debug(f"{get_emoji('report')} Firewall readiness check complete") + + # gracefully exit if the firewall is not ready for an upgrade to target version + if not update_available: + logging.error( + f"{get_emoji('error')} Firewall is not ready for upgrade to {target_version}.", + ) + + sys.exit(1) + + # Download the target PAN-OS version + logging.info( + f"{get_emoji('start')} Performing test to see if {target_version} is already downloaded..." + ) + image_downloaded = software_download(firewall, target_version, ha_details) + if deploy_info == "active" or deploy_info == "passive": + logging.info( + f"{get_emoji('success')} {target_version} has been downloaded and sync'd to HA peer." + ) + else: + logging.info( + f"{get_emoji('success')} PAN-OS version {target_version} has been downloaded." + ) + + # Begin snapshots of the network state + if not image_downloaded: + logging.error(f"{get_emoji('error')} Image not downloaded, exiting...") + + sys.exit(1) + + # Perform the pre-upgrade snapshot + perform_snapshot( + firewall, + firewall_details.hostname, + f'assurance/snapshots/{firewall_details.hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.json', + ) + + # Perform Readiness Checks + perform_readiness_checks( + firewall, + firewall_details.hostname, + f'assurance/readiness_checks/{firewall_details.hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.json', + ) + + # If the firewall is in an HA pair, check the HA peer to ensure sync has been enabled + if ha_details: + logging.info( + f"{get_emoji('start')} Performing test to see if HA peer is in sync..." + ) + if ha_details["result"]["group"]["running-sync"] == "synchronized": + logging.info(f"{get_emoji('success')} HA peer sync test has been completed") + else: + logging.error( + f"{get_emoji('error')} HA peer state is not in sync, please try again" + ) + logging.error(f"{get_emoji('stop')} Halting script.") + + sys.exit(1) + + # Back up configuration to local filesystem + logging.info( + f"{get_emoji('start')} Performing backup of {firewall_details.hostname}'s configuration to local filesystem..." + ) + backup_config = backup_configuration( + firewall, + f'assurance/configurations/{firewall_details.hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.xml', + ) + logging.debug(f"{get_emoji('report')} {backup_config}") + + # Exit execution is dry_run is True + if dry_run is True: + logging.info(f"{get_emoji('success')} Dry run complete, exiting...") + logging.info(f"{get_emoji('stop')} Halting script.") + sys.exit(0) + else: + logging.info(f"{get_emoji('start')} Not a dry run, continue with upgrade...") + + # Perform the upgrade + perform_upgrade( + firewall=firewall, + hostname=firewall_details.hostname, + target_version=target_version, + ha_details=ha_details, + ) + + # Perform the reboot + perform_reboot( + firewall=firewall, + target_version=target_version, + ha_details=ha_details, + ) + # ---------------------------------------------------------------------------- -# Perform the reboot process +# Utility Functions # ---------------------------------------------------------------------------- -def perform_reboot( - firewall: Firewall, - target_version: str, - ha_details: Optional[dict] = None, +def check_readiness_and_log( + result: dict, + test_name: str, + test_info: dict, ) -> None: """ - Initiates and oversees the reboot process of a firewall, ensuring it reaches the specified target version. + Evaluates and logs the results of a specified readiness test. - This function triggers a reboot of the specified firewall and monitors its status throughout the process. - In HA (High Availability) setups, it confirms synchronization with the HA peer post-reboot. The function - includes robust handling of various states and errors, with detailed logging. It verifies the firewall - reaches the target PAN-OS version upon reboot completion. + This function assesses the outcome of a particular readiness test by examining its result. + It logs the outcome using varying log levels (info, warning, error), determined by the + test's importance and its result. If a test is marked as critical and fails, the script + may terminate execution. Parameters ---------- - firewall : Firewall - The firewall instance to be rebooted. - target_version : str - The target PAN-OS version to confirm after reboot. - ha_details : Optional[dict], optional - High Availability details of the firewall, if applicable. Default is None. + result : dict + A dictionary where each key corresponds to a readiness test name. The value is another dictionary + containing two keys: 'state' (a boolean indicating the test's success or failure) and 'reason' + (a string explaining the outcome). + + test_name : str + The name of the test to evaluate. This name should correspond to a key in the 'result' dictionary. + + test_info : dict + Information about the test, including its description, log level (info, warning, error), and a flag + indicating whether to exit the script upon test failure (exit_on_failure). + + Notes + ----- + - The function utilizes the `get_emoji` helper function to add appropriate emojis to log messages, + enhancing readability and user experience. + - If 'state' in the test result is True, the test is logged as passed. Otherwise, it is either + logged as failed or skipped, based on the specified log level in 'test_info'. Raises ------ SystemExit - Exits the script if the firewall fails to reboot to the target version, if HA synchronization issues - occur, or if critical errors are encountered during the reboot process. + If a critical test (marked with "exit_on_failure": True) fails, the script will raise SystemExit. + """ + test_result = result.get( + test_name, {"state": False, "reason": "Test not performed"} + ) + log_message = f'{test_info["description"]} - {test_result["reason"]}' + + if test_result["state"]: + logging.info( + f"{get_emoji('success')} Passed Readiness Check: {test_info['description']}" + ) + else: + if test_info["log_level"] == "error": + logging.error(f"{get_emoji('error')} {log_message}") + if test_info["exit_on_failure"]: + logging.error(f"{get_emoji('stop')} Halting script.") + + sys.exit(1) + elif test_info["log_level"] == "warning": + logging.debug( + f"{get_emoji('report')} Skipped Readiness Check: {test_info['description']}" + ) + else: + logging.debug(log_message) + + +def compare_versions(version1: str, version2: str) -> str: + """ + Compares two PAN-OS version strings. + + Parameters: + version1 (str): First version string to compare. + version2 (str): Second version string to compare. + + Returns: + str: 'older' if version1 < version2, 'newer' if version1 > version2, 'equal' if they are the same. + """ + parsed_version1 = parse_version(version1) + parsed_version2 = parse_version(version2) + + if parsed_version1 < parsed_version2: + return "older" + elif parsed_version1 > parsed_version2: + return "newer" + else: + return "equal" + + +def configure_logging(level: str, encoding: str = "utf-8") -> None: + """ + Sets up the logging configuration for the script with the specified logging level and encoding. + + This function initializes the global logger, sets the specified logging level, and configures two handlers: + one for console output and another for file output. It uses RotatingFileHandler for file logging to manage + file size and maintain backups. + + Parameters + ---------- + level : str + The desired logging level (e.g., 'debug', 'info', 'warning', 'error', 'critical'). + The input is case-insensitive. If an invalid level is provided, it defaults to 'info'. + + encoding : str, optional + The encoding format for the file-based log handler, by default 'utf-8'. Notes ----- - - The function checks the firewall's version and HA synchronization status (if applicable) post-reboot. - - Confirms that the firewall has successfully rebooted to the target PAN-OS version. - - Script terminates if the firewall doesn't reach the target version or synchronize (in HA setups) within - 20 minutes. + - The Console Handler outputs log messages to the standard output. + - The File Handler logs messages to 'logs/upgrade.log'. This file is rotated when it reaches 1MB in size, + maintaining up to three backup files. + - The logging level influences the verbosity of the log messages. An invalid level defaults to 'info', + ensuring a baseline of logging. + """ + logging_level = getattr(logging, level.upper(), None) + + # Get the root logger + logger = logging.getLogger() + logger.setLevel(logging_level) + + # Remove any existing handlers + for handler in logger.handlers[:]: + logger.removeHandler(handler) + + # Create handlers (console and file handler) + console_handler = logging.StreamHandler() + file_handler = RotatingFileHandler( + "logs/upgrade.log", + maxBytes=1024 * 1024, + backupCount=3, + encoding=encoding, + ) + + # Create formatters and add them to the handlers + if level == "debug": + console_format = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + file_format = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + else: + console_format = logging.Formatter("%(message)s") + file_format = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + + console_handler.setFormatter(console_format) + file_handler.setFormatter(file_format) + + # Add handlers to the logger + logger.addHandler(console_handler) + logger.addHandler(file_handler) + + +def connect_to_host( + hostname: str, + api_username: str, + api_password: str, +) -> PanDevice: + """ + Establishes a connection to a Panorama or PAN-OS firewall appliance using provided credentials. + + This function uses the hostname, username, and password to attempt a connection to a target appliance, + which can be either a Panorama management server or a PAN-OS firewall. It identifies the type of + appliance based on the provided credentials and hostname. Upon successful connection, it returns an + appropriate PanDevice object (either Panorama or Firewall). + + Parameters + ---------- + hostname : str + The DNS Hostname or IP address of the target appliance. + api_username : str + Username for authentication. + api_password : str + Password for authentication. + + Returns + ------- + PanDevice + An instance of PanDevice (either Panorama or Firewall), representing the established connection. + + Raises + ------ + SystemExit + If the connection attempt fails, such as due to a timeout, incorrect credentials, or other errors. + + Example + -------- + Connecting to a Panorama management server: + >>> connect_to_host('panorama.example.com', 'admin', 'password') + + + Connecting to a PAN-OS firewall: + >>> connect_to_host('192.168.0.1', 'admin', 'password') + + """ + try: + target_device = PanDevice.create_from_device( + hostname, + api_username, + api_password, + ) + + return target_device + + except PanConnectionTimeout: + logging.error( + f"{get_emoji('error')} Connection to the {hostname} appliance timed out. Please check the DNS hostname or IP address and network connectivity." + ) + + sys.exit(1) + + except Exception as e: + logging.error( + f"{get_emoji('error')} An error occurred while connecting to the {hostname} appliance: {e}" + ) + + sys.exit(1) + + +def ensure_directory_exists(file_path: str) -> None: + """ + Ensures the existence of the directory for a specified file path, creating it if necessary. + + This function checks if the directory for a given file path exists. If it does not exist, the function + creates the directory along with any necessary parent directories. This is particularly useful for + ensuring that the file system is prepared for file operations that require specific directory structures. + + Parameters + ---------- + file_path : str + The file path whose directory needs to be verified and potentially created. The function extracts + the directory part of the file path to check its existence. Example ------- - Rebooting a firewall to a specific PAN-OS version: - >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> perform_reboot(firewall, '10.2.0') - # The firewall undergoes a reboot and the script monitors until it reaches the target version 10.2.0. + Ensuring a directory exists for a file path: + >>> file_path = '/path/to/directory/file.txt' + >>> ensure_directory_exists(file_path) + # If '/path/to/directory/' does not exist, it is created. """ + directory = os.path.dirname(file_path) + if not os.path.exists(directory): + os.makedirs(directory) - reboot_start_time = time.time() - rebooted = False - - # Check if HA details are available - if ha_details: - logging.info(f"{get_emoji('start')} Rebooting the passive HA firewall...") - # Reboot standalone firewall - else: - logging.info(f"{get_emoji('start')} Rebooting the standalone firewall...") +def filter_string_to_dict(filter_string: str) -> dict: + """ + Converts a string containing comma-separated key-value pairs into a dictionary. - reboot_job = firewall.op( - "", cmd_xml=False - ) - reboot_job_result = flatten_xml_to_dict(reboot_job) - logging.info(f"{get_emoji('report')} {reboot_job_result['result']}") + This utility function parses a string where each key-value pair is separated by a comma, and + each key is separated from its value by an equal sign ('='). It's useful for converting filter + strings into dictionary formats, commonly used in configurations and queries. - # Wait for the firewall reboot process to initiate before checking status - time.sleep(60) + Parameters + ---------- + filter_string : str + The string to be parsed into key-value pairs. It should follow the format 'key1=value1,key2=value2,...'. + If the string is empty or improperly formatted, an empty dictionary is returned. - while not rebooted: - # Check if HA details are available - if ha_details: - try: - deploy_info, current_ha_details = get_ha_status(firewall) - logging.debug( - f"{get_emoji('report')} deploy_info: {deploy_info}", - ) - logging.debug( - f"{get_emoji('report')} current_ha_details: {current_ha_details}", - ) + Returns + ------- + dict + A dictionary with keys and values derived from the `filter_string`. Keys are the substrings before each '=' + character, and values are the corresponding substrings after the '=' character. - if current_ha_details and deploy_info in ["active", "passive"]: - if ( - current_ha_details["result"]["group"]["running-sync"] - == "synchronized" - ): - logging.info( - f"{get_emoji('success')} HA passive firewall rebooted and synchronized with its peer in {int(time.time() - reboot_start_time)} seconds" - ) - rebooted = True - else: - logging.info( - f"{get_emoji('working')} HA passive firewall rebooted but not yet synchronized with its peer. Will try again in 30 seconds." - ) - time.sleep(60) - except (PanXapiError, PanConnectionTimeout, PanURLError): - logging.info(f"{get_emoji('working')} Firewall is rebooting...") - time.sleep(60) + Examples + -------- + Converting a filter string to a dictionary: + >>> filter_string_to_dict("hostname=test,serial=11111") + {'hostname': 'test', 'serial': '11111'} - # Reboot standalone firewall - else: - try: - firewall.refresh_system_info() - logging.info( - f"{get_emoji('report')} Firewall version: {firewall.version}" - ) + Handling an empty or improperly formatted string: + >>> filter_string_to_dict("") + {} + >>> filter_string_to_dict("invalid_format_string") + {} - if firewall.version == target_version: - logging.info( - f"{get_emoji('success')} Firewall rebooted in {int(time.time() - reboot_start_time)} seconds" - ) - rebooted = True - else: - logging.error( - f"{get_emoji('stop')} Firewall rebooted but running the target version. Please try again." - ) - sys.exit(1) - except (PanXapiError, PanConnectionTimeout, PanURLError): - logging.info(f"{get_emoji('working')} Firewall is rebooting...") - time.sleep(60) + Notes + ----- + - The function does not perform validation on the key-value pairs. It's assumed that the input string is + correctly formatted. + - In case of duplicate keys, the last occurrence of the key in the string will determine its value in the + resulting dictionary. + """ + result = {} + for substr in filter_string.split(","): + k, v = substr.split("=") + result[k] = v - # Check if 20 minutes have passed - if time.time() - reboot_start_time > 1200: # 20 minutes in seconds - logging.error( - f"{get_emoji('error')} Firewall did not become available and/or establish a Connected sync state with its HA peer after 20 minutes. Please check the firewall status manually." - ) - break + return result -# ---------------------------------------------------------------------------- -# Helper function to convert XML ET.Element into a Python dictionary -# ---------------------------------------------------------------------------- def flatten_xml_to_dict(element: ET.Element) -> dict: """ Converts a given XML element to a dictionary, flattening the XML structure. @@ -1609,87 +1897,48 @@ def flatten_xml_to_dict(element: ET.Element) -> dict: return result -def model_from_api_response( - element: Union[ET.Element, ET.ElementTree], - model: type[FromAPIResponseMixin], -) -> FromAPIResponseMixin: +def get_emoji(action: str) -> str: """ - Converts an XML Element, typically from an API response, into a specified Pydantic model. + Retrieves an emoji character corresponding to a specific action keyword. - This function facilitates the transformation of XML data into a structured Pydantic model. - It first flattens the XML Element into a dictionary and then maps this dictionary to the - specified Pydantic model. This approach simplifies the handling of complex XML structures - often returned by APIs, enabling easier manipulation and access to the data within Python. + This function is used to enhance the visual appeal and readability of log messages or console outputs. + It maps predefined action keywords to their corresponding emoji characters. Parameters ---------- - element : Union[ET.Element, ET.ElementTree] - The XML Element or ElementTree to be converted. This is typically obtained from parsing - XML data returned by an API call. - - model : type[FromAPIResponseMixin] - The Pydantic model class into which the XML data will be converted. This model must - inherit from the FromAPIResponseMixin, indicating it can handle data derived from - API responses. + action : str + An action keyword for which an emoji is required. Supported keywords include 'success', + 'warning', 'error', 'working', 'report', 'search', 'save', 'stop', and 'start'. Returns ------- - FromAPIResponseMixin - An instance of the specified Pydantic model, populated with data extracted from the - provided XML Element. - - Example - ------- - Converting an XML response to a Pydantic model: - >>> xml_element = ET.fromstring('value') - >>> MyModel = type('MyModel', (FromAPIResponseMixin, BaseModel), {}) - >>> model_instance = model_from_api_response(xml_element, MyModel) - # 'model_instance' is now an instance of 'MyModel' with data from 'xml_element'. - """ - result_dict = flatten_xml_to_dict(element) - return model.from_api_response(result_dict) - - -def get_managed_devices(panorama: Panorama, **filters) -> list[ManagedDevice]: - """ - Retrieves a filtered list of managed devices from a specified Panorama appliance. - - This function queries a Panorama appliance for its managed devices and filters the results - based on the provided keyword arguments. Each keyword argument must correspond to an - attribute of the `ManagedDevice` model. The function applies regex matching for each - filter, returning only those devices that match all specified filters. - - Parameters - ---------- - panorama : Panorama - An instance of the Panorama class, representing the Panorama appliance to query. + str + The emoji character associated with the action keyword. If the keyword is not recognized, + returns an empty string. - filters : **kwargs - Keyword argument filters to apply. Each keyword should correspond to an attribute - of the `ManagedDevice` model class. The value for each keyword is a regex pattern - to match against the corresponding attribute. + Examples + -------- + >>> get_emoji('success') + '✅' # Indicates a successful operation - Returns - ------- - list[ManagedDevice] - A list of `ManagedDevice` instances that match the specified filters. + >>> get_emoji('error') + '❌' # Indicates an error - Example - ------- - Retrieving devices from Panorama with specific hostname and model filters: - >>> panorama = Panorama('192.168.1.1', 'admin', 'password') - >>> managed_devices = get_managed_devices(panorama, hostname='^PA-220$', model='.*220.*') - # Returns a list of `ManagedDevice` instances for devices with hostnames matching 'PA-220' - # and model containing '220'. + >>> get_emoji('start') + '🚀' # Indicates the start of a process """ - managed_devices = model_from_api_response( - panorama.op("show devices all"), ManagedDevices - ) - devices = managed_devices.devices - for filter_key, filter_value in filters.items(): - devices = [d for d in devices if re.match(filter_value, getattr(d, filter_key))] - - return devices + emoji_map = { + "success": "✅", + "warning": "⚠️", + "error": "❌", + "working": "⚙️", + "report": "📝", + "search": "🔍", + "save": "💾", + "stop": "🛑", + "start": "🚀", + } + return emoji_map.get(action, "") def get_firewalls_from_panorama(panorama: Panorama, **filters) -> list[Firewall]: @@ -1731,210 +1980,177 @@ def get_firewalls_from_panorama(panorama: Panorama, **filters) -> list[Firewall] return firewalls -def upgrade_single_firewall( - firewall: Firewall, - target_version: str, - dry_run: bool, -) -> None: +def get_managed_devices(panorama: Panorama, **filters) -> list[ManagedDevice]: """ - Manages the upgrade process for a single firewall appliance to a specified PAN-OS version. + Retrieves a filtered list of managed devices from a specified Panorama appliance. - This function orchestrates a series of steps to upgrade a firewall, including readiness checks, - software download, configuration backup, and the actual upgrade and reboot processes. It supports a - 'dry run' mode to simulate the upgrade process without applying changes. The function is designed to handle - both standalone firewalls and firewalls in a High Availability (HA) setup. + This function queries a Panorama appliance for its managed devices and filters the results + based on the provided keyword arguments. Each keyword argument must correspond to an + attribute of the `ManagedDevice` model. The function applies regex matching for each + filter, returning only those devices that match all specified filters. Parameters ---------- - firewall : Firewall - An instance of the Firewall class representing the firewall to be upgraded. - target_version : str - The target PAN-OS version to upgrade the firewall to. - dry_run : bool - If True, the function will simulate the upgrade process without making any changes. - If False, the function will proceed with the actual upgrade. + panorama : Panorama + An instance of the Panorama class, representing the Panorama appliance to query. - Steps - ----- - 1. Refresh system information to ensure latest data is available. - 2. Determine if the firewall is standalone, part of HA, or in a cluster. - 3. Check firewall readiness for the specified target version. - 4. Download the target PAN-OS version if not already present. - 5. Perform pre-upgrade snapshots and readiness checks. - 6. Backup current configuration to the local filesystem. - 7. Proceed with upgrade and reboot if not a dry run. + filters : **kwargs + Keyword argument filters to apply. Each keyword should correspond to an attribute + of the `ManagedDevice` model class. The value for each keyword is a regex pattern + to match against the corresponding attribute. - Notes - ----- - - The script gracefully exits if the firewall is not ready for the upgrade. - - In HA setups, the script checks for synchronization status of the HA pair. - - In dry run mode, the script simulates the upgrade process without performing the actual upgrade. + Returns + ------- + list[ManagedDevice] + A list of `ManagedDevice` instances that match the specified filters. Example ------- - Upgrading a firewall to a specific PAN-OS version: - >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> upgrade_single_firewall(firewall, '10.1.0', dry_run=False) - # This will upgrade the firewall to PAN-OS version 10.1.0. + Retrieving devices from Panorama with specific hostname and model filters: + >>> panorama = Panorama('192.168.1.1', 'admin', 'password') + >>> managed_devices = get_managed_devices(panorama, hostname='^PA-220$', model='.*220.*') + # Returns a list of `ManagedDevice` instances for devices with hostnames matching 'PA-220' + # and model containing '220'. """ - # Refresh system information to ensure we have the latest data - logging.debug(f"{get_emoji('start')} Refreshing system information...") - firewall_details = SystemSettings.refreshall(firewall)[0] - logging.info( - f"{get_emoji('report')} {firewall.serial} {firewall_details.hostname} {firewall_details.ip_address}" + managed_devices = model_from_api_response( + panorama.op("show devices all"), ManagedDevices ) + devices = managed_devices.devices + for filter_key, filter_value in filters.items(): + devices = [d for d in devices if re.match(filter_value, getattr(d, filter_key))] - # Determine if the firewall is standalone, HA, or in a cluster - logging.debug( - f"{get_emoji('start')} Performing test to see if firewall is standalone, HA, or in a cluster..." - ) - deploy_info, ha_details = get_ha_status(firewall) - logging.info(f"{get_emoji('report')} Firewall HA mode: {deploy_info}") - logging.debug(f"{get_emoji('report')} Firewall HA details: {ha_details}") + return devices - # Check to see if the firewall is ready for an upgrade - logging.debug( - f"{get_emoji('start')} Performing test to validate firewall's readiness..." - ) - update_available = software_update_check(firewall, target_version, ha_details) - logging.debug(f"{get_emoji('report')} Firewall readiness check complete") - # gracefully exit if the firewall is not ready for an upgrade to target version - if not update_available: - logging.error( - f"{get_emoji('error')} Firewall is not ready for upgrade to {target_version}.", - ) +def ip_callback(value: str) -> str: + """ + Validates the input as a valid IP address or a resolvable hostname. - sys.exit(1) + This function first attempts to resolve the hostname via DNS query. If it fails, + it utilizes the ip_address function from the ipaddress standard library module to + validate the provided input as an IP address. It is designed to be used as a callback + function for Typer command-line argument parsing, ensuring that only valid IP addresses + or resolvable hostnames are accepted as input. - # Download the target PAN-OS version - logging.info( - f"{get_emoji('start')} Performing test to see if {target_version} is already downloaded..." - ) - image_downloaded = software_download(firewall, target_version, ha_details) - if deploy_info == "active" or deploy_info == "passive": - logging.info( - f"{get_emoji('success')} {target_version} has been downloaded and sync'd to HA peer." - ) - else: - logging.info( - f"{get_emoji('success')} PAN-OS version {target_version} has been downloaded." - ) + Parameters + ---------- + value : str + A string representing the IP address or hostname to be validated. - # Begin snapshots of the network state - if not image_downloaded: - logging.error(f"{get_emoji('error')} Image not downloaded, exiting...") + Returns + ------- + str + The validated IP address string or hostname. - sys.exit(1) + Raises + ------ + typer.BadParameter + If the input string is not a valid IP address or a resolvable hostname, a typer.BadParameter + exception is raised with an appropriate error message. + """ - # Perform the pre-upgrade snapshot - perform_snapshot( - firewall, - firewall_details.hostname, - f'assurance/snapshots/{firewall_details.hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.json', - ) + # First, try to resolve as a hostname + if resolve_hostname(value): + return value - # Perform Readiness Checks - perform_readiness_checks( - firewall, - firewall_details.hostname, - f'assurance/readiness_checks/{firewall_details.hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.json', - ) + # If hostname resolution fails, try as an IP address + try: + ipaddress.ip_address(value) + return value - # If the firewall is in an HA pair, check the HA peer to ensure sync has been enabled - if ha_details: - logging.info( - f"{get_emoji('start')} Performing test to see if HA peer is in sync..." - ) - if ha_details["result"]["group"]["running-sync"] == "synchronized": - logging.info(f"{get_emoji('success')} HA peer sync test has been completed") - else: - logging.error( - f"{get_emoji('error')} HA peer state is not in sync, please try again" - ) - logging.error(f"{get_emoji('stop')} Halting script.") + except ValueError as err: + raise typer.BadParameter( + "The value you passed for --hostname is neither a valid DNS hostname nor IP address, please check your inputs again." + ) from err - sys.exit(1) - # Back up configuration to local filesystem - logging.info( - f"{get_emoji('start')} Performing backup of {firewall_details.hostname}'s configuration to local filesystem..." - ) - backup_config = backup_configuration( - firewall, - f'assurance/configurations/{firewall_details.hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.xml', - ) - logging.debug(f"{get_emoji('report')} {backup_config}") +def model_from_api_response( + element: Union[ET.Element, ET.ElementTree], + model: type[FromAPIResponseMixin], +) -> FromAPIResponseMixin: + """ + Converts an XML Element, typically from an API response, into a specified Pydantic model. - # Exit execution is dry_run is True - if dry_run is True: - logging.info(f"{get_emoji('success')} Dry run complete, exiting...") - logging.info(f"{get_emoji('stop')} Halting script.") - sys.exit(0) - else: - logging.info(f"{get_emoji('start')} Not a dry run, continue with upgrade...") + This function facilitates the transformation of XML data into a structured Pydantic model. + It first flattens the XML Element into a dictionary and then maps this dictionary to the + specified Pydantic model. This approach simplifies the handling of complex XML structures + often returned by APIs, enabling easier manipulation and access to the data within Python. - # Perform the upgrade - perform_upgrade( - firewall=firewall, - hostname=firewall_details.hostname, - target_version=target_version, - ha_details=ha_details, - ) + Parameters + ---------- + element : Union[ET.Element, ET.ElementTree] + The XML Element or ElementTree to be converted. This is typically obtained from parsing + XML data returned by an API call. - # Perform the reboot - perform_reboot( - firewall=firewall, - target_version=target_version, - ha_details=ha_details, - ) + model : type[FromAPIResponseMixin] + The Pydantic model class into which the XML data will be converted. This model must + inherit from the FromAPIResponseMixin, indicating it can handle data derived from + API responses. + Returns + ------- + FromAPIResponseMixin + An instance of the specified Pydantic model, populated with data extracted from the + provided XML Element. -def filter_string_to_dict(filter_string: str) -> dict: + Example + ------- + Converting an XML response to a Pydantic model: + >>> xml_element = ET.fromstring('value') + >>> MyModel = type('MyModel', (FromAPIResponseMixin, BaseModel), {}) + >>> model_instance = model_from_api_response(xml_element, MyModel) + # 'model_instance' is now an instance of 'MyModel' with data from 'xml_element'. """ - Converts a string containing comma-separated key-value pairs into a dictionary. + result_dict = flatten_xml_to_dict(element) + return model.from_api_response(result_dict) - This utility function parses a string where each key-value pair is separated by a comma, and - each key is separated from its value by an equal sign ('='). It's useful for converting filter - strings into dictionary formats, commonly used in configurations and queries. + +def parse_version(version: str) -> Tuple[int, int, int, int]: + parts = version.split(".") + if len(parts) == 2: # When maintenance version is an integer + major, minor = parts + maintenance, hotfix = 0, 0 + else: # When maintenance version includes hotfix + major, minor, maintenance = parts + if "-h" in maintenance: + maintenance, hotfix = maintenance.split("-h") + else: + hotfix = 0 + + return int(major), int(minor), int(maintenance), int(hotfix) + + +def resolve_hostname(hostname: str) -> bool: + """ + Checks if a given hostname can be resolved via DNS query. + + This function attempts to resolve the specified hostname using DNS. It queries the DNS servers + that the operating system is configured to use. The function is designed to return a boolean + value indicating whether the hostname could be successfully resolved or not. Parameters ---------- - filter_string : str - The string to be parsed into key-value pairs. It should follow the format 'key1=value1,key2=value2,...'. - If the string is empty or improperly formatted, an empty dictionary is returned. + hostname : str + The hostname (e.g., 'example.com') to be resolved. Returns ------- - dict - A dictionary with keys and values derived from the `filter_string`. Keys are the substrings before each '=' - character, and values are the corresponding substrings after the '=' character. - - Examples - -------- - Converting a filter string to a dictionary: - >>> filter_string_to_dict("hostname=test,serial=11111") - {'hostname': 'test', 'serial': '11111'} - - Handling an empty or improperly formatted string: - >>> filter_string_to_dict("") - {} - >>> filter_string_to_dict("invalid_format_string") - {} + bool + Returns True if the hostname can be resolved, False otherwise. - Notes - ----- - - The function does not perform validation on the key-value pairs. It's assumed that the input string is - correctly formatted. - - In case of duplicate keys, the last occurrence of the key in the string will determine its value in the - resulting dictionary. + Raises + ------ + None + This function does not raise any exceptions. It handles all exceptions internally and + returns False in case of any issues during the resolution process. """ - result = {} - for substr in filter_string.split(","): - k, v = substr.split("=") - result[k] = v - - return result + try: + dns.resolver.resolve(hostname) + return True + except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.exception.Timeout) as err: + # Optionally log or handle err here if needed + logging.debug(f"Hostname resolution failed: {err}") + return False # ---------------------------------------------------------------------------- @@ -1998,7 +2214,11 @@ def main( ] = "", log_level: Annotated[ str, - typer.Option("--log-level", "-l", help="Set the logging output level"), + typer.Option( + "--log-level", + "-l", + help="Set the logging output level", + ), ] = "info", ): """ @@ -2098,6 +2318,9 @@ def main( firewalls_to_upgrade = get_firewalls_from_panorama( device, **filter_string_to_dict(filter) ) + logging.debug( + f"{get_emoji('report')} Firewalls to upgrade: {firewalls_to_upgrade}" + ) # Run the upgrade process for each identified firewall. Note this runs serially, i.e one after the other. # This is also not yet "HA aware". From 6c3808b4d5588d17e11812c9a5492f33c9ddb7e6 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Wed, 24 Jan 2024 11:30:18 -0600 Subject: [PATCH 02/13] Add firewalls_to_revisit list and lock for storing firewalls to revisit, updated HA logic to add firewalls to list rather than creating a peer firewall connection. --- pan_os_upgrade/upgrade.py | 187 +++++++++++++++++++------------------- 1 file changed, 91 insertions(+), 96 deletions(-) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index 7cde21e..70bddaa 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -45,6 +45,7 @@ import time import re from logging.handlers import RotatingFileHandler +from threading import Lock from typing import Dict, List, Optional, Tuple, Union from typing_extensions import Annotated @@ -246,6 +247,13 @@ class AssuranceOptions: ] +# ---------------------------------------------------------------------------- +# Global list and lock for storing firewalls to revisit +# ---------------------------------------------------------------------------- +firewalls_to_revisit = [] +firewalls_to_revisit_lock = Lock() + + # ---------------------------------------------------------------------------- # Core Upgrade Functions # ---------------------------------------------------------------------------- @@ -341,51 +349,6 @@ def create_firewall_object( pass -def create_peer_firewall(firewall: Firewall) -> Optional[Firewall]: - """ - Creates a Firewall object representing the HA peer firewall. - - Parameters - ---------- - firewall : Firewall - The current firewall instance to find the HA peer for. - - Returns - ------- - Optional[Firewall] - A Firewall object representing the HA peer, or None if the serial number of the peer cannot be found. - - Notes - ----- - - Retrieves the HA peer's serial number using an operational command. - - If the serial number is found, creates and returns a Firewall object for the HA peer. - - If the serial number is not found, logs an error and returns None. - """ - try: - ha_peer_serial_response = firewall.op( - "peer.cfg.platform.serial", - cmd_xml=False, - ) - serial_string = ha_peer_serial_response.find(".//result").text - peer_serial_parsed = re.search(r"\b\d+\b", serial_string) - - if peer_serial_parsed: - peer_serial = peer_serial_parsed.group() - peer_firewall = Firewall(serial=peer_serial) - if firewall.parent: - firewall.parent.add(peer_firewall) - return peer_firewall - else: - logging.error(f"{get_emoji('error')} Serial number not found for HA peer") - return None - - except Exception as e: - logging.error( - f"{get_emoji('error')} Error creating HA peer firewall object: {e}" - ) - return None - - def determine_upgrade( firewall: Firewall, target_major: int, @@ -552,14 +515,13 @@ def handle_ha_logic( # If the active and passive firewalls are running the same version if version_comparison == "equal": if local_state == "active": - # Target the passive firewall first - logging.debug(f"{get_emoji('report')} Firewall is active") - peer_firewall = create_peer_firewall(firewall) - if peer_firewall: - logging.debug( - f"{get_emoji('report')} Peer firewall: {peer_firewall.about()}" - ) - return False, peer_firewall + # Add the active firewall to the list and exit the upgrade process + with firewalls_to_revisit_lock: + firewalls_to_revisit.append(firewall) + logging.info( + f"{get_emoji('info')} Detected active firewall in HA pair running the same version as its peer. Added firewall to revisit list." + ) + return False, None elif local_state == "passive": # Continue with upgrade process on the passive firewall logging.debug(f"{get_emoji('report')} Firewall is passive") @@ -584,6 +546,38 @@ def handle_ha_logic( return False, None +def perform_ha_sync_check( + firewall: Firewall, ha_details: dict, strict_sync_check: bool = True +) -> bool: + """ + Checks the HA synchronization status and handles the result based on the strictness of the check. + + Parameters: + firewall (Firewall): The firewall instance. + ha_details (dict): High Availability details of the firewall. + strict_sync_check (bool): If True, the function will exit the script if sync is not achieved. If False, it will only log a warning. + + Returns: + bool: True if the HA synchronization is successful, False otherwise. + """ + logging.info(f"{get_emoji('start')} Checking if HA peer is in sync...") + if ha_details["result"]["group"]["running-sync"] == "synchronized": + logging.info(f"{get_emoji('success')} HA peer sync test has been completed.") + return True + else: + if strict_sync_check: + logging.error( + f"{get_emoji('error')} HA peer state is not in sync, please try again." + ) + logging.error(f"{get_emoji('stop')} Halting script.") + sys.exit(1) + else: + logging.warning( + f"{get_emoji('warning')} HA peer state is not in sync. This will be noted, but the script will continue." + ) + return False + + def perform_readiness_checks( firewall: Firewall, hostname: str, @@ -725,16 +719,17 @@ def perform_reboot( # Wait for the firewall reboot process to initiate before checking status time.sleep(60) + # Counter that tracks if the rebooted firewall is online but not yet synced on configuration + reboot_and_sync_check = 0 + while not rebooted: # Check if HA details are available if ha_details: try: deploy_info, current_ha_details = get_ha_status(firewall) + logging.debug(f"{get_emoji('report')} deploy_info: {deploy_info}") logging.debug( - f"{get_emoji('report')} deploy_info: {deploy_info}", - ) - logging.debug( - f"{get_emoji('report')} current_ha_details: {current_ha_details}", + f"{get_emoji('report')} current_ha_details: {current_ha_details}" ) if current_ha_details and deploy_info in ["active", "passive"]: @@ -747,10 +742,19 @@ def perform_reboot( ) rebooted = True else: - logging.info( - f"{get_emoji('working')} HA passive firewall rebooted but not yet synchronized with its peer. Will try again in 30 seconds." - ) - time.sleep(60) + reboot_and_sync_check += 1 + if reboot_and_sync_check >= 5: + logging.warning( + f"{get_emoji('warning')} HA passive firewall rebooted but did not complete a configuration sync with the active after 5 attempts." + ) + # Set rebooted to True to exit the loop + rebooted = True + break + else: + logging.info( + f"{get_emoji('working')} HA passive firewall rebooted but not yet synchronized with its peer. Will try again in 60 seconds." + ) + time.sleep(60) except (PanXapiError, PanConnectionTimeout, PanURLError): logging.info(f"{get_emoji('working')} Firewall is rebooting...") time.sleep(60) @@ -777,10 +781,10 @@ def perform_reboot( logging.info(f"{get_emoji('working')} Firewall is rebooting...") time.sleep(60) - # Check if 20 minutes have passed - if time.time() - reboot_start_time > 1200: # 20 minutes in seconds + # Check if 30 minutes have passed + if time.time() - reboot_start_time > 1800: # 30 minutes in seconds logging.error( - f"{get_emoji('error')} Firewall did not become available and/or establish a Connected sync state with its HA peer after 20 minutes. Please check the firewall status manually." + f"{get_emoji('error')} Firewall did not become available and/or establish a Connected sync state with its HA peer after 30 minutes. Please check the firewall status manually." ) break @@ -1248,7 +1252,6 @@ def software_update_check( # retrieve available versions of PAN-OS firewall.software.check() available_versions = firewall.software.versions - logging.debug(f"Available PAN-OS versions: {available_versions}") # check to see if specified version is available for upgrade if version in available_versions: @@ -1353,14 +1356,6 @@ def suspend_ha_passive(firewall: Firewall) -> bool: return False -def upgrade_firewall( - firewall: Firewall, - target_version: str, - dry_run: bool, -) -> None: - pass - - def upgrade_single_firewall( firewall: Firewall, target_version: str, @@ -1433,15 +1428,9 @@ def upgrade_single_firewall( logging.info( f"{get_emoji('start')} Switching control to the peer firewall for upgrade." ) - # Here you would add the logic to switch control to the peer firewall - # This could involve a recursive call to upgrade_single_firewall or a different approach upgrade_single_firewall(peer_firewall, target_version, dry_run) - return else: - logging.error( - f"{get_emoji('error')} Unable to determine HA peer for upgrade." - ) - sys.exit(1) + return # Exit the function without proceeding to upgrade # Check to see if the firewall is ready for an upgrade logging.debug( @@ -1455,7 +1444,6 @@ def upgrade_single_firewall( logging.error( f"{get_emoji('error')} Firewall is not ready for upgrade to {target_version}.", ) - sys.exit(1) # Download the target PAN-OS version @@ -1492,20 +1480,15 @@ def upgrade_single_firewall( f'assurance/readiness_checks/{firewall_details.hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.json', ) - # If the firewall is in an HA pair, check the HA peer to ensure sync has been enabled - if ha_details: - logging.info( - f"{get_emoji('start')} Performing test to see if HA peer is in sync..." - ) - if ha_details["result"]["group"]["running-sync"] == "synchronized": - logging.info(f"{get_emoji('success')} HA peer sync test has been completed") - else: - logging.error( - f"{get_emoji('error')} HA peer state is not in sync, please try again" - ) - logging.error(f"{get_emoji('stop')} Halting script.") + # Determine strictness of HA sync check + with firewalls_to_revisit_lock: + is_firewall_to_revisit = firewall in firewalls_to_revisit - sys.exit(1) + perform_ha_sync_check( + firewall, + ha_details, + strict_sync_check=not is_firewall_to_revisit, + ) # Back up configuration to local filesystem logging.info( @@ -2322,11 +2305,23 @@ def main( f"{get_emoji('report')} Firewalls to upgrade: {firewalls_to_upgrade}" ) - # Run the upgrade process for each identified firewall. Note this runs serially, i.e one after the other. - # This is also not yet "HA aware". + # Initial pass of upgrades for firewall in firewalls_to_upgrade: upgrade_single_firewall(firewall, target_version, dry_run) + # Revisit the firewalls that were skipped in the initial pass + if firewalls_to_revisit: + logging.info( + f"{get_emoji('info')} Revisiting firewalls that were active in an HA pair and had the same version as their peers." + ) + with firewalls_to_revisit_lock: + for firewall in firewalls_to_revisit: + logging.info( + f"{get_emoji('start')} Revisiting firewall: {firewall.hostname}" + ) + upgrade_single_firewall(firewall, target_version, dry_run) + firewalls_to_revisit.clear() # Clear the list after revisiting + if __name__ == "__main__": app() From 9ff05645f9ee983941fe0c254fedd6c26b7d5b89 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Wed, 24 Jan 2024 19:01:30 -0600 Subject: [PATCH 03/13] adding multi-threading into the solution for #53, HA workflow handling for #50 --- pan_os_upgrade/upgrade.py | 50 ++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index 70bddaa..ab05bb0 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -44,6 +44,7 @@ import sys import time import re +from concurrent.futures import ThreadPoolExecutor, as_completed from logging.handlers import RotatingFileHandler from threading import Lock from typing import Dict, List, Optional, Tuple, Union @@ -2305,21 +2306,54 @@ def main( f"{get_emoji('report')} Firewalls to upgrade: {firewalls_to_upgrade}" ) - # Initial pass of upgrades - for firewall in firewalls_to_upgrade: - upgrade_single_firewall(firewall, target_version, dry_run) + # Using ThreadPoolExecutor to manage threads + with ThreadPoolExecutor(max_workers=2) as executor: + # Store future objects along with firewalls for reference + future_to_firewall = { + executor.submit( + upgrade_single_firewall, fw, target_version, dry_run + ): fw + for fw in firewalls_to_upgrade + } + + # Process completed tasks + for future in as_completed(future_to_firewall): + firewall = future_to_firewall[future] + try: + future.result() + except Exception as exc: + logging.error( + f"{get_emoji('error')} Firewall {firewall.hostname} generated an exception: {exc}" + ) # Revisit the firewalls that were skipped in the initial pass if firewalls_to_revisit: logging.info( f"{get_emoji('info')} Revisiting firewalls that were active in an HA pair and had the same version as their peers." ) + + # Using ThreadPoolExecutor to manage threads for revisiting firewalls + with ThreadPoolExecutor(max_workers=2) as executor: + future_to_firewall = { + executor.submit( + upgrade_single_firewall, fw, target_version, dry_run + ): fw + for fw in firewalls_to_revisit + } + + for future in as_completed(future_to_firewall): + firewall = future_to_firewall[future] + try: + future.result() + logging.info( + f"{get_emoji('success')} Completed revisiting firewall: {firewall.hostname}" + ) + except Exception as exc: + logging.error( + f"{get_emoji('error')} Exception while revisiting firewall {firewall.hostname}: {exc}" + ) + with firewalls_to_revisit_lock: - for firewall in firewalls_to_revisit: - logging.info( - f"{get_emoji('start')} Revisiting firewall: {firewall.hostname}" - ) - upgrade_single_firewall(firewall, target_version, dry_run) firewalls_to_revisit.clear() # Clear the list after revisiting From de0899a8e3d12128d3ff291e66a330edb3499610 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Wed, 24 Jan 2024 19:40:51 -0600 Subject: [PATCH 04/13] update docstrings on core upgrade functions --- pan_os_upgrade/upgrade.py | 576 ++++++++++++++++++++++---------------- 1 file changed, 336 insertions(+), 240 deletions(-) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index ab05bb0..983a91d 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -1,41 +1,44 @@ """ -upgrade.py: A script to automate the upgrade process of PAN-OS firewalls. +Upgrade.py: Automating the Upgrade Process for PAN-OS Firewalls -This module contains functionality to perform automated upgrade procedures on Palo Alto Networks firewalls. -It handles various PAN-OS operations, system settings management, error handling specific to PAN-OS, -and interactions with the panos-upgrade-assurance tool. The script is intended for use as a standalone utility or -as part of larger automation workflows. It uses the Typer library for command-line interface creation, replacing -the previous argparse implementation. Authentication is now exclusively username/password-based, with no option for -API key authentication. Additionally, the script no longer searches for settings in a .env file but accepts necessary -parameters directly via command-line arguments. +This script provides a comprehensive solution for automating the upgrade of Palo Alto Networks firewalls. +It covers a broad range of functionalities essential for successful PAN-OS upgrades, including interaction +with the panos-upgrade-assurance tool, system settings management, and PAN-OS specific error handling. +Designed for both standalone utility and integration into larger workflows, the script leverages Typer for +command-line interface creation and supports username/password-based authentication. + +Features: +- Automated upgrade procedures for both standalone and Panorama-managed Palo Alto Networks firewalls. +- Extensive error handling specific to PAN-OS, ensuring robust operation under various scenarios. +- Utilization of the panos-upgrade-assurance tool for pre and post-upgrade checks. +- Direct command-line argument input for parameters, moving away from .env file reliance. Imports: Standard Libraries: - ipaddress: For handling IP addresses. - logging: For providing a logging interface. - os: For interacting with the operating system. - sys: For accessing system-specific parameters and functions. - time: For time-related functions. - RotatingFileHandler (logging.handlers): For handling log file rotation. + - concurrent, threading: Provides multi-threading capabilities. + - ipaddress: Handles IP address manipulations. + - logging: Provides logging functionalities. + - os, sys: Interacts with the operating system and accesses system-specific parameters. + - time: Manages time-related functions. + - RotatingFileHandler (logging.handlers): Manages log file rotation. External Libraries: - xml.etree.ElementTree (ET): For XML tree manipulation. - panos: For interacting with Palo Alto Networks devices. - PanDevice, SystemSettings (panos.base, panos.device): For base PAN-OS device operations. - PanConnectionTimeout, PanDeviceError, PanDeviceXapiError, PanURLError, PanXapiError (panos.errors): - For handling specific PAN-OS errors. - Firewall (panos.firewall): For handling firewall-specific operations. + - xml.etree.ElementTree (ET): Manipulates XML tree structures. + - panos: Interfaces with Palo Alto Networks devices. + - PanDevice, SystemSettings (panos.base, panos.device): Manages base PAN-OS device operations. + - Error handling modules (panos.errors): Manages specific PAN-OS errors. + - Firewall (panos.firewall): Handles firewall-specific operations. panos-upgrade-assurance package: - CheckFirewall, FirewallProxy (panos_upgrade_assurance): For performing checks and acting as a proxy to the firewall. + - CheckFirewall, FirewallProxy: Performs checks and acts as a proxy to the firewall. Third-party libraries: - xmltodict: For converting XML data to Python dictionaries. - typer: For building command-line interface applications. - BaseModel (pydantic): For creating Pydantic base models. + - xmltodict: Converts XML data to Python dictionaries. + - typer: Builds command-line interface applications. + - BaseModel (pydantic): Creates Pydantic base models. Project-specific imports: - SnapshotReport, ReadinessCheckReport (pan_os_upgrade.models): For handling snapshot and readiness check reports. + - SnapshotReport, ReadinessCheckReport (pan_os_upgrade.models): Manages snapshot and readiness check reports. """ # standard library imports import ipaddress @@ -265,41 +268,40 @@ def backup_configuration( """ Backs up the current running configuration of a specified firewall to a local file. - This function retrieves the running configuration from the firewall and saves it as an XML file - at the specified file path. It checks the validity of the retrieved XML data and logs the success - or failure of the backup process. + This function retrieves the current running configuration from the specified firewall and + saves it as an XML file at the provided file path. It performs checks to ensure the + validity of the retrieved XML data and logs the outcome of the backup process. Parameters ---------- firewall : Firewall - The firewall instance from which the configuration is to be backed up. + The instance of the firewall from which the running configuration is to be backed up. file_path : str - The path where the configuration backup file will be saved. + The filesystem path where the configuration backup file will be stored. Returns ------- bool - Returns True if the backup is successfully created, False otherwise. + True if the backup is successfully created; False if any error occurs during the backup process. Raises ------ Exception - Raises an exception if any error occurs during the backup process. + If any error occurs during the retrieval or saving of the configuration data. Notes ----- - - The function verifies the XML structure of the retrieved configuration. - - Ensures the directory for the backup file exists. - - The backup file is saved in XML format. + - The function checks the XML structure of the retrieved configuration to ensure its integrity. + - The directory for the backup file is verified, and if it does not exist, it is created. + - The configuration data is saved in XML format to the specified path. Example -------- - Backing up the firewall configuration: - >>> firewall = Firewall(hostname='192.168.1.1', 'admin', 'password') - >>> backup_configuration(firewall, '/path/to/config_backup.xml') - # Configuration is backed up to the specified file. + Backing up the configuration of a firewall: + >>> firewall_instance = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> backup_configuration(firewall_instance, '/path/to/config_backup.xml') + True # Indicates that the backup was successful. """ - try: # Run operational command to retrieve configuration config_xml = firewall.op("show config running") @@ -343,13 +345,6 @@ def backup_configuration( return False -def create_firewall_object( - serial_number: str, - panorama: Panorama, -) -> Firewall: - pass - - def determine_upgrade( firewall: Firewall, target_major: int, @@ -357,38 +352,44 @@ def determine_upgrade( target_maintenance: Union[int, str], ) -> None: """ - Determines the necessity of an upgrade for a firewall to a specific PAN-OS version. + Determines whether an upgrade is necessary for a firewall to a specific PAN-OS version. - This function assesses if upgrading the firewall's PAN-OS version is required by comparing its current - version with the specified target version. The target version is defined by major, minor, and maintenance - version numbers, where the maintenance version can also include hotfix information. The function logs - the current and target versions, and establishes the need for an upgrade if the current version is lower - than the target. If the current version is equal to or higher than the target, it suggests that an upgrade - is unnecessary or a downgrade is being attempted, leading to termination of the script. + This function evaluates whether the firewall's current PAN-OS version needs to be upgraded by + comparing it with the specified target version. The target version is detailed by major, minor, + and maintenance version numbers. The maintenance version may be an integer or a string including hotfix information. + The function logs both the current and target versions. If the current version is lower than the target, + an upgrade is deemed necessary. If the current version is equal to or higher than the target, it implies + no upgrade is needed, or a downgrade is attempted, and the script exits. Parameters ---------- firewall : Firewall - The instance of the Firewall whose PAN-OS version is being evaluated. + The Firewall instance whose PAN-OS version is under evaluation. target_major : int - Major version number of the target PAN-OS. + The major version number of the target PAN-OS. target_minor : int - Minor version number of the target PAN-OS. + The minor version number of the target PAN-OS. target_maintenance : Union[int, str] - Maintenance or hotfix version number of the target PAN-OS, can be an integer or string. + The maintenance or hotfix version number of the target PAN-OS, which can be either an integer or a string. Raises ------ SystemExit - Exits the script if the target version is not an upgrade, indicating either a downgrade attempt - or that the current version already meets or exceeds the target version. + Exits the script if the target version does not necessitate an upgrade, suggesting either a downgrade attempt + or the current version is already at or beyond the target version. Notes ----- - - Parses the PAN-OS version strings into tuples of integers for accurate comparison. - - Utilizes emojis in logging for clear and user-friendly status indication. - """ + - The function parses PAN-OS version strings into tuples of integers for an accurate comparison. + - Logging with emojis is used for clear and user-friendly status updates. + Examples + -------- + Determining the need for an upgrade: + >>> firewall_instance = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> determine_upgrade(firewall_instance, 10, 0, 1) + # Logs information about the current version and the necessity of an upgrade to version 10.0.1. + """ current_version = parse_version(firewall.version) if isinstance(target_maintenance, int): @@ -405,56 +406,53 @@ def determine_upgrade( f"{get_emoji('report')} Target PAN-OS version: {target_major}.{target_minor}.{target_maintenance}" ) - upgrade_needed = current_version < target_version - if upgrade_needed: + if current_version < target_version: logging.info( - f"{get_emoji('success')} Confirmed that moving from {firewall.version} to {target_major}.{target_minor}.{target_maintenance} is an upgrade" + f"{get_emoji('success')} Upgrade required from {firewall.version} to {target_major}.{target_minor}.{target_maintenance}" ) - return - else: logging.error( - f"{get_emoji('error')} Upgrade is not required or a downgrade was attempted." + f"{get_emoji('error')} No upgrade required or downgrade attempt detected." ) logging.error(f"{get_emoji('stop')} Halting script.") - sys.exit(1) def get_ha_status(firewall: Firewall) -> Tuple[str, Optional[dict]]: """ - Determines the High-Availability (HA) deployment status and configuration of a specified Firewall appliance. + Retrieves the High-Availability (HA) status and configuration details of a specified firewall. - This function queries a firewall to determine its HA deployment status. It can identify if the firewall - operates in a standalone mode, as part of an HA pair (either active/passive or active/active), or within - a cluster configuration. It fetches and logs both the deployment status and, if applicable, detailed - configuration information about the HA setup. + This function queries the specified firewall to determine its HA deployment status. It can distinguish + between standalone mode, active/passive HA pair, active/active HA pair, or cluster configurations. + The function fetches both the deployment type (as a string) and, if applicable, a dictionary containing + detailed HA configuration information. Parameters ---------- firewall : Firewall - An instance of the Firewall class representing the firewall whose HA status is to be assessed. + The firewall instance to query for HA status. Returns ------- Tuple[str, Optional[dict]] - A tuple containing two elements: + A tuple containing: - A string indicating the HA deployment type (e.g., 'standalone', 'active/passive', 'active/active'). - - An optional dictionary with detailed HA configuration information. The dictionary is provided if - the firewall is part of an HA setup; otherwise, None is returned. + - An optional dictionary with detailed HA configuration. Provided if the firewall is in an HA setup; + otherwise, None is returned. Example ------- - >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> ha_status, ha_details = get_ha_status(firewall) - >>> print(ha_status) # Example output: 'active/passive' - >>> print(ha_details) # Example output: {'ha_details': {...}} + Assessing the HA status of a firewall: + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> ha_status, ha_config = get_ha_status(firewall) + >>> print(ha_status) # e.g., 'active/passive' + >>> print(ha_config) # e.g., {'local-info': {...}, 'peer-info': {...}} Notes ----- - - This function uses the 'show_highavailability_state' method from the Firewall class to retrieve HA status. - - For processing the XML response, it employs the 'flatten_xml_to_dict' helper function to translate the - data into a Python dictionary, providing a more accessible format for further operations or analysis. + - The function employs the 'show_highavailability_state' method from the Firewall class for querying HA status. + - Uses 'flatten_xml_to_dict' to convert XML responses into a more accessible dictionary format. + - This function is crucial for understanding the HA configuration of a firewall, especially in complex network setups. """ logging.debug( f"{get_emoji('start')} Getting {firewall.serial} deployment information..." @@ -478,29 +476,42 @@ def handle_ha_logic( dry_run: bool, ) -> Tuple[bool, Optional[Firewall]]: """ - Handles the logic specific to High Availability (HA) configurations. + Manages High Availability (HA) specific logic during the upgrade process of a firewall. + + This function assesses the HA role of a specified firewall and determines the appropriate action + in the context of upgrading to a target PAN-OS version. It considers whether the firewall is active + or passive in an HA configuration and whether it is appropriate to proceed with the upgrade. In a dry run, + the function simulates the HA logic without actual state changes. Parameters ---------- firewall : Firewall - The firewall instance to evaluate for HA logic. + The firewall instance to be evaluated for HA related upgrade logic. target_version : str - The target PAN-OS version for the upgrade. + The target PAN-OS version intended for the upgrade. dry_run : bool - If True, simulates the logic without making changes. + If True, simulates the HA logic without executing state changes. Returns ------- Tuple[bool, Optional[Firewall]] - A tuple where the first element is a boolean indicating whether to proceed with the upgrade, - and the second element is an optional Firewall object representing the peer firewall if the - current firewall is not the target for upgrade. + A tuple where the first element is a boolean indicating whether the upgrade should proceed, + and the second element is an optional Firewall instance representing the HA peer if it should be + the target for the upgrade. + + Example + ------- + Handling HA logic for a firewall upgrade: + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> proceed, peer_firewall = handle_ha_logic(firewall, '10.1.0', dry_run=False) + >>> print(proceed) # True or False + >>> print(peer_firewall) # Firewall instance or None Notes ----- - - This function determines if the firewall is part of an HA pair and its role (active/passive). - - It evaluates if the HA peer firewall needs to be upgraded first. - - In dry run mode, it simulates the HA logic without performing actual operations. + - The function determines the HA status and version comparison between the HA pair. + - For active firewalls with passive peers on the same version, the function defers the upgrade process. + - In dry run mode, the function does not perform state changes like suspending HA states. """ deploy_info, ha_details = get_ha_status(firewall) @@ -551,15 +562,45 @@ def perform_ha_sync_check( firewall: Firewall, ha_details: dict, strict_sync_check: bool = True ) -> bool: """ - Checks the HA synchronization status and handles the result based on the strictness of the check. + Verifies the synchronization status of the High Availability (HA) peer firewall. - Parameters: - firewall (Firewall): The firewall instance. - ha_details (dict): High Availability details of the firewall. - strict_sync_check (bool): If True, the function will exit the script if sync is not achieved. If False, it will only log a warning. + This function checks whether the HA peer firewall is synchronized with the primary firewall. + It logs the synchronization status and takes action based on the strictness of the sync check. + In strict mode, the script exits if synchronization is not achieved; otherwise, it logs a warning + but continues execution. - Returns: - bool: True if the HA synchronization is successful, False otherwise. + Parameters + ---------- + firewall : Firewall + The firewall instance whose HA synchronization status is to be checked. + ha_details : dict + A dictionary containing the HA status details of the firewall. + strict_sync_check : bool, optional + Determines the strictness of the synchronization check. If True, the function will halt the script on + unsuccessful synchronization. Defaults to True. + + Returns + ------- + bool + True if the HA synchronization is successful, False otherwise. + + Raises + ------ + SystemExit + Exits the script if strict synchronization check fails. + + Example + -------- + Checking HA synchronization status: + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> ha_details = {'result': {'group': {'running-sync': 'synchronized'}}} + >>> perform_ha_sync_check(firewall, ha_details, strict_sync_check=True) + True # If the HA peer is synchronized + + Notes + ----- + - The function logs detailed synchronization status, aiding in debugging and operational monitoring. + - It is essential in maintaining HA integrity during operations like upgrades or configuration changes. """ logging.info(f"{get_emoji('start')} Checking if HA peer is in sync...") if ha_details["result"]["group"]["running-sync"] == "synchronized": @@ -585,35 +626,43 @@ def perform_readiness_checks( file_path: str, ) -> None: """ - Executes readiness checks on a specified firewall and saves the results as a JSON file. + Executes and records readiness checks for a specified firewall prior to operations like upgrades. - This function initiates a series of readiness checks on the firewall to assess its state before - proceeding with operations like upgrades. The checks cover aspects like configuration status, - content version, license validity, HA status, and more. The results of these checks are logged, - and a detailed report is generated and saved to the provided file path. + This function conducts a variety of checks on a firewall to ensure it is ready for further operations, + such as upgrades. These checks include verifying configuration status, content version, license validity, + High Availability (HA) status, and more. The results are logged and stored in a JSON report at a specified + file path. Parameters ---------- firewall : Firewall - The firewall instance on which to perform the readiness checks. + The firewall instance for which the readiness checks are to be performed. hostname : str - Hostname of the firewall, used primarily for logging purposes. + The hostname of the firewall. This is used for logging and reporting purposes. file_path : str - Path to the file where the readiness check report JSON will be saved. + The file path where the JSON report of the readiness checks will be stored. + + Returns + ------- + None + + Raises + ------ + IOError + Raises an IOError if the report file cannot be written. Notes ----- - - Utilizes the `run_assurance` function to perform the readiness checks. - - Ensures the existence of the directory where the report file will be saved. - - Logs the outcome of the readiness checks and saves the report in JSON format. - - Logs an error message if the readiness check creation fails. + - The function employs the `run_assurance` function for executing the readiness checks. + - It ensures that the directory for the report file exists before writing the file. + - The readiness report is stored in JSON format for easy readability and parsing. Example - -------- - Conducting readiness checks: - >>> firewall = Firewall(hostname='192.168.1.1', 'username', 'password') + ------- + Executing readiness checks and saving the report: + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') >>> perform_readiness_checks(firewall, 'firewall1', '/path/to/readiness_report.json') - # Readiness report is saved to the specified path. + # The readiness report for 'firewall1' is saved at '/path/to/readiness_report.json'. """ logging.debug( @@ -663,41 +712,40 @@ def perform_reboot( ha_details: Optional[dict] = None, ) -> None: """ - Initiates and oversees the reboot process of a firewall, ensuring it reaches the specified target version. + Initiates the reboot of a firewall and ensures it successfully restarts with the target PAN-OS version. - This function triggers a reboot of the specified firewall and monitors its status throughout the process. - In HA (High Availability) setups, it confirms synchronization with the HA peer post-reboot. The function - includes robust handling of various states and errors, with detailed logging. It verifies the firewall - reaches the target PAN-OS version upon reboot completion. + This function manages the reboot process of a specified firewall, tracking its progress and + confirming it restarts with the intended PAN-OS version. In HA (High Availability) setups, it + additionally checks for synchronization with the HA peer post-reboot. It logs various steps and + handles different states and potential errors encountered during the reboot. Parameters ---------- firewall : Firewall - The firewall instance to be rebooted. + The firewall to be rebooted. target_version : str - The target PAN-OS version to confirm after reboot. + The target PAN-OS version to be verified post-reboot. ha_details : Optional[dict], optional - High Availability details of the firewall, if applicable. Default is None. + High Availability details of the firewall, used to check HA synchronization post-reboot. Default is None. Raises ------ SystemExit - Exits the script if the firewall fails to reboot to the target version, if HA synchronization issues - occur, or if critical errors are encountered during the reboot process. + Exits the script if the firewall fails to reboot to the target version, encounters HA synchronization issues, + or if critical errors arise during the reboot process. Notes ----- - - The function checks the firewall's version and HA synchronization status (if applicable) post-reboot. - - Confirms that the firewall has successfully rebooted to the target PAN-OS version. - - Script terminates if the firewall doesn't reach the target version or synchronize (in HA setups) within - 20 minutes. + - The function monitors the reboot process and verifies the firewall's PAN-OS version post-reboot. + - It confirms successful synchronization in HA setups. + - The script terminates if the firewall fails to reach the target version or synchronize within 30 minutes. Example ------- - Rebooting a firewall to a specific PAN-OS version: + Rebooting and verifying a firewall's version: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> perform_reboot(firewall, '10.2.0') - # The firewall undergoes a reboot and the script monitors until it reaches the target version 10.2.0. + >>> perform_reboot(firewall, '10.1.0') + # The firewall reboots and the script monitors until it successfully reaches version 10.1.0. """ reboot_start_time = time.time() @@ -874,46 +922,46 @@ def perform_upgrade( retry_interval: int = 60, ) -> None: """ - Initiates and manages the upgrade process of a firewall to a specified PAN-OS version. + Upgrades a specified firewall to a designated PAN-OS version, accounting for retries and HA considerations. - This function attempts to upgrade the firewall to the given PAN-OS version, handling potential issues - and retrying if necessary. It deals with High Availability (HA) considerations and ensures that the - upgrade process is robust against temporary failures or busy states. The function logs each step of the - process and exits the script if critical errors occur. + This function orchestrates the upgrade of the firewall to a specified PAN-OS version. It integrates logic for + handling High Availability (HA) setups and provides robust error handling, including retries for transient issues. + The function logs the progress of the upgrade and terminates the script in case of unrecoverable errors or after + exhausting the maximum retry attempts. The retry mechanism is particularly useful for handling scenarios where the + software manager is temporarily busy. Parameters ---------- firewall : Firewall - The firewall instance to be upgraded. + The firewall instance that is to be upgraded. hostname : str - The hostname of the firewall, used for logging purposes. + The hostname of the firewall, mainly used for logging. target_version : str - The target PAN-OS version for the upgrade. + The target version of PAN-OS to upgrade the firewall to. ha_details : Optional[dict], optional - High Availability details of the firewall, by default None. + High Availability details of the firewall, defaults to None. max_retries : int, optional - The maximum number of retry attempts for the upgrade, by default 3. + The maximum number of retry attempts in case of failures, defaults to 3. retry_interval : int, optional - The interval (in seconds) to wait between retry attempts, by default 60. + The interval in seconds between retry attempts, defaults to 60. Raises ------ SystemExit - Exits the script if the upgrade job fails, if HA synchronization issues occur, - or if critical errors are encountered during the upgrade process. + Exits the script if the upgrade fails or if critical errors are encountered. Notes ----- - - The function handles retries based on the 'max_retries' and 'retry_interval' parameters. - - In case of 'software manager is currently in use' errors, retries are attempted. - - Critical errors during the upgrade process lead to script termination. + - Handles retries based on 'max_retries' and 'retry_interval'. + - Specifically accounts for 'software manager is currently in use' errors. + - Ensures compatibility with HA configurations. Example ------- - Upgrading a firewall to a specific PAN-OS version: + Upgrading a firewall to a specific version with retry logic: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') >>> perform_upgrade(firewall, '192.168.1.1', '10.2.0', max_retries=2, retry_interval=30) - # The firewall is upgraded to PAN-OS version 10.2.0, with retries if necessary. + # The firewall is upgraded to version 10.2.0, with a maximum of 2 retries if needed. """ logging.info( @@ -970,49 +1018,49 @@ def run_assurance( config: Dict[str, Union[str, int, float, bool]], ) -> Union[SnapshotReport, ReadinessCheckReport, None]: """ - Executes specified operational tasks on the firewall and returns the results or reports. + Executes specified operational tasks on a firewall, returning reports or results based on the operation. - This function handles different operational tasks on the firewall based on the provided - 'operation_type'. It supports operations like performing readiness checks, capturing state - snapshots, and generating reports. The operation is executed according to the 'actions' and - 'config' specified. Successful operations return results or a report object. Invalid operations - or errors during execution result in logging an error and returning None. + This function facilitates various operational tasks on the firewall, such as readiness checks, state snapshots, + or report generation, depending on the 'operation_type' specified. It performs the tasks as defined in 'actions' + and 'config'. Successful executions return appropriate report objects, while invalid operations or execution errors + result in logging an error and returning None. The function ensures that the specified actions align with the + operation type and handles any exceptions that arise during task execution. Parameters ---------- firewall : Firewall - The firewall instance on which to perform the operations. + The firewall instance on which the operations will be performed. hostname : str - The ip address or dns hostname of the firewall. + The IP address or DNS hostname of the firewall. operation_type : str - The type of operation to perform (e.g., 'readiness_check', 'state_snapshot', 'report'). + The type of operation to perform, such as 'readiness_check', 'state_snapshot', or 'report'. actions : List[str] - A list of actions to be performed for the specified operation type. + A list specifying actions to execute for the operation. config : Dict[str, Union[str, int, float, bool]] - Configuration settings for the specified actions. + Configuration settings for executing the specified actions. Returns ------- Union[SnapshotReport, ReadinessCheckReport, None] - The results of the operation as a report object, or None if the operation type or action is invalid, or an error occurs. + Depending on the operation, returns either a SnapshotReport, a ReadinessCheckReport, or None in case of failure or invalid operations. Raises ------ SystemExit - Raised if an invalid action is specified for the operation type or if an exception occurs during execution. - - Example - -------- - Performing a state snapshot operation: - >>> firewall = Firewall(hostname='192.168.1.1', 'admin', 'password') - >>> run_assurance(firewall, 'firewall1', 'state_snapshot', ['arp_table', 'ip_sec_tunnels'], {}) - SnapshotReport object or None + Exits the script if an invalid action for the specified operation is encountered or in case of an exception during execution. Notes ----- - - The 'readiness_check' operation verifies the firewall's readiness for upgrade-related tasks. - - The 'state_snapshot' operation captures the current state of the firewall. - - The 'report' operation generates a report based on the specified actions. This is pending implementation. + - 'readiness_check' assesses the firewall's readiness for upgrades. + - 'state_snapshot' captures the current operational state of the firewall. + - 'report' (pending implementation) will generate detailed reports based on the action. + + Example + ------- + Running a state snapshot operation: + >>> firewall = Firewall(hostname='192.168.1.1', 'admin', 'password') + >>> result = run_assurance(firewall, 'firewall1', 'state_snapshot', ['arp_table', 'ip_sec_tunnels'], {}) + >>> print(result) # Outputs: SnapshotReport object or None """ # setup Firewall client proxy_firewall = FirewallProxy(firewall) @@ -1096,44 +1144,44 @@ def software_download( ha_details: dict, ) -> bool: """ - Initiates and monitors the download of a specified PAN-OS software version on the firewall. + Initiates and monitors the download of a specified PAN-OS version on a firewall. - This function starts the download process for the given target PAN-OS version on the specified - firewall. It continually checks and logs the download's progress. If the download is successful, - it returns True. If the download process encounters errors or fails, these are logged, and the - function returns False. Exceptions during the download process lead to script termination. + This function triggers the download of a target PAN-OS version on the provided firewall instance. + It continually monitors and logs the download progress. Upon successful completion of the download, + the function returns True; if errors are encountered or the download fails, it logs these issues + and returns False. In case of exceptions, the script is terminated for safety. Parameters ---------- firewall : Firewall - The Firewall instance on which the software is to be downloaded. + The instance of the Firewall where the software is to be downloaded. target_version : str - The PAN-OS version targeted for download. + The specific PAN-OS version targeted for download. ha_details : dict - High-availability details of the firewall, determining if HA synchronization is needed. + High Availability (HA) details of the firewall, if applicable. Returns ------- bool - True if the download is successful, False if the download fails or encounters an error. + True if the software download completes successfully, False otherwise. Raises ------ SystemExit - Raised if an exception occurs during the download process or if a critical error is encountered. + Exits the script if an exception occurs or if a critical error is encountered during the download. Example -------- - Initiating a PAN-OS version download: + Downloading a specific PAN-OS version: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> software_download(firewall, '10.1.0', ha_details={}) - True or False depending on the success of the download + >>> success = software_download(firewall, '10.1.0', ha_details={}) + >>> print(success) # Outputs: True if successful, False otherwise Notes ----- - - Before initiating the download, the function checks if the target version is already available on the firewall. - - It uses the 'download' method of the Firewall's software attribute to perform the download. - - The function sleeps for 30 seconds between each status check to allow time for the download to progress. + - The function first checks if the target version is already downloaded on the firewall. + - Utilizes the 'download' method from the Firewall's software module. + - The download status is checked at 30-second intervals to allow for progress. """ if firewall.software.versions[target_version]["downloaded"]: @@ -1207,38 +1255,43 @@ def software_update_check( """ Verifies the availability and readiness of a specified PAN-OS version for upgrade on a firewall. - This function checks if the target PAN-OS version is available for upgrade on the specified firewall. - It first refreshes the firewall's system information to ensure current data, then uses the - `determine_upgrade` function to validate if the target version is an upgrade compared to the current - version. It checks the list of available PAN-OS versions and verifies if the base image for the - target version is downloaded. The function returns True if the target version is available and the - base image is downloaded, and False if the version is not available, the base image is not downloaded, - or a downgrade attempt is identified. + This function checks if a target PAN-OS version is ready for upgrade on the given firewall. It first + refreshes the firewall's system information for current data and then uses the `determine_upgrade` function + to assess if the target version constitutes an upgrade. The function verifies the presence of the target + version in the list of available PAN-OS versions and checks if its base image is downloaded. It returns + True if the target version is available and the base image is present, and False if the version is unavailable, + the base image is not downloaded, or a downgrade is attempted. Parameters ---------- firewall : Firewall - The firewall instance to be checked for software update availability. + The instance of the Firewall to be checked for software update availability. version : str - The target PAN-OS version intended for the upgrade. + The target PAN-OS version for the upgrade. ha_details : dict - High-availability (HA) details of the firewall. Used to assess if HA synchronization is required for the update. + High-Availability (HA) details of the firewall, essential for considering HA synchronization during the update. Returns ------- bool - True if the target PAN-OS version is available and ready for upgrade, False otherwise. + True if the target PAN-OS version is available and ready for the upgrade, False otherwise. Raises ------ SystemExit - Exits the script if a downgrade attempt is identified or if the target version is not suitable for an upgrade. + Exits the script if a downgrade is attempted or if the target version is inappropriate for an upgrade. Example -------- - >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> software_update_check(firewall, '10.1.0', ha_details={}) - True # If the version 10.1.0 is available and ready for upgrade + Checking the availability of a specific PAN-OS version for upgrade: + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> software_update_check(firewall, '10.1.0', ha_details={}) + True # Indicates that version 10.1.0 is available and ready for upgrade. + + Notes + ----- + - This function is a prerequisite step before initiating a firewall upgrade. + - It is important to ensure that the target version is not only present but also compatible for an upgrade to avoid downgrade scenarios. """ # parse version major, minor, maintenance = version.split(".") @@ -1281,22 +1334,41 @@ def software_update_check( def suspend_ha_active(firewall: Firewall) -> bool: """ - Suspends the HA state of the active firewall in an HA pair. + Suspends the High-Availability (HA) state of the active firewall in an HA pair. + + This function issues a command to suspend the HA state on the specified firewall, + which is expected to be the active member in an HA configuration. Suspending the HA + state on the active firewall allows its passive counterpart to take over as the active unit. + The function logs the outcome of this operation and returns a boolean status indicating the + success or failure of the suspension. Parameters ---------- firewall : Firewall - The active firewall in the HA pair. + An instance of the Firewall class representing the active firewall in an HA pair. Returns ------- bool - Returns True if the HA state suspension is successful, False otherwise. + Returns True if the HA state is successfully suspended, or False in case of failure. + + Raises + ------ + Exception + Logs an error and returns False if an exception occurs during the HA suspension process. Notes ----- - - This function should be called only when it's confirmed that the firewall is the active member of an HA pair. - - It suspends the HA state to allow the passive firewall to become active. + - This function should be invoked only when it is confirmed that the firewall is the active member in an HA setup. + - The HA state suspension is a critical operation and should be handled with caution to avoid service disruptions. + + Example + ------- + Suspending the HA state of an active firewall in an HA pair: + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> suspend_ha_active(firewall) + True # Indicates successful suspension of the HA state. + """ try: suspension_response = firewall.op( @@ -1320,22 +1392,41 @@ def suspend_ha_active(firewall: Firewall) -> bool: def suspend_ha_passive(firewall: Firewall) -> bool: """ - Suspends the HA state of the passive firewall in an HA pair. + Suspends the High-Availability (HA) state of the passive firewall in an HA pair. + + This function issues a command to suspend the HA state on the specified firewall, + which is expected to be the passive member in an HA configuration. The suspension + prevents the passive firewall from becoming active, particularly useful during + maintenance or upgrade processes. The function logs the operation's outcome and + returns a boolean status indicating the success or failure of the suspension. Parameters ---------- firewall : Firewall - The passive firewall in the HA pair. + An instance of the Firewall class representing the passive firewall in an HA pair. Returns ------- bool - Returns True if the HA state suspension is successful, False otherwise. + Returns True if the HA state is successfully suspended, or False in case of failure. + + Raises + ------ + Exception + Logs an error and returns False if an exception occurs during the HA suspension process. Notes ----- - - This function should be called only when it's confirmed that the firewall is the passive member of an HA pair. - - It suspends the HA state to prevent it from becoming active during the upgrade process. + - This function should be invoked only when it is confirmed that the firewall is the passive member in an HA setup. + - Suspending the HA state on a passive firewall is a key step in controlled maintenance or upgrade procedures. + + Example + ------- + Suspending the HA state of a passive firewall in an HA pair: + >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') + >>> suspend_ha_passive(firewall) + True # Indicates successful suspension of the HA state. + """ try: suspension_response = firewall.op( @@ -1363,45 +1454,50 @@ def upgrade_single_firewall( dry_run: bool, ) -> None: """ - Manages the upgrade process for a single firewall appliance to a specified PAN-OS version. + Orchestrates the upgrade process of a single firewall to a specified PAN-OS version. - This function orchestrates a series of steps to upgrade a firewall, including readiness checks, - software download, configuration backup, and the actual upgrade and reboot processes. It supports a - 'dry run' mode to simulate the upgrade process without applying changes. The function is designed to handle - both standalone firewalls and firewalls in a High Availability (HA) setup. + This comprehensive function manages the entire upgrade process for a firewall. It includes + initial readiness checks, software download, configuration backup, and the execution of the upgrade + and reboot phases. The function supports a dry run mode, allowing simulation of the upgrade process + without applying actual changes. It is compatible with both standalone firewalls and those in High + Availability (HA) configurations. Parameters ---------- firewall : Firewall - An instance of the Firewall class representing the firewall to be upgraded. + The Firewall instance to be upgraded. target_version : str - The target PAN-OS version to upgrade the firewall to. + The PAN-OS version to upgrade the firewall to. dry_run : bool - If True, the function will simulate the upgrade process without making any changes. - If False, the function will proceed with the actual upgrade. + If True, performs a dry run of the upgrade process without making changes. + If False, executes the actual upgrade process. - Steps - ----- - 1. Refresh system information to ensure latest data is available. - 2. Determine if the firewall is standalone, part of HA, or in a cluster. - 3. Check firewall readiness for the specified target version. - 4. Download the target PAN-OS version if not already present. - 5. Perform pre-upgrade snapshots and readiness checks. - 6. Backup current configuration to the local filesystem. - 7. Proceed with upgrade and reboot if not a dry run. + Raises + ------ + SystemExit + Exits the script if a critical failure occurs at any stage of the upgrade process. + + Workflow + -------- + 1. Refreshes the firewall's system information. + 2. Determines the firewall's deployment mode (standalone, HA). + 3. Validates the readiness for the upgrade. + 4. Downloads the target PAN-OS version, if not already available. + 5. Executes pre-upgrade steps: snapshots, readiness checks, and configuration backup. + 6. Proceeds with the actual upgrade and subsequent reboot, unless in dry run mode. Notes ----- - - The script gracefully exits if the firewall is not ready for the upgrade. - - In HA setups, the script checks for synchronization status of the HA pair. - - In dry run mode, the script simulates the upgrade process without performing the actual upgrade. + - In HA configurations, additional checks and steps are performed to ensure synchronization and readiness. + - The script handles all logging, error checking, and state validation throughout the upgrade process. Example ------- Upgrading a firewall to a specific PAN-OS version: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') >>> upgrade_single_firewall(firewall, '10.1.0', dry_run=False) - # This will upgrade the firewall to PAN-OS version 10.1.0. + # Initiates the upgrade process of the firewall to PAN-OS version 10.1.0. + """ # Refresh system information to ensure we have the latest data logging.debug(f"{get_emoji('start')} Refreshing system information...") From 7b33ac7dda28b83bd1ff1a39a7daa08ba62f51ba Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Thu, 25 Jan 2024 07:51:41 -0600 Subject: [PATCH 05/13] update docstrings to conform to standard format --- pan_os_upgrade/upgrade.py | 629 ++++++++++++++++++++++++-------------- 1 file changed, 398 insertions(+), 231 deletions(-) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index 983a91d..021a907 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -1630,38 +1630,43 @@ def check_readiness_and_log( test_info: dict, ) -> None: """ - Evaluates and logs the results of a specified readiness test. + Assesses and logs the outcome of a specified readiness test for a firewall upgrade process. - This function assesses the outcome of a particular readiness test by examining its result. - It logs the outcome using varying log levels (info, warning, error), determined by the - test's importance and its result. If a test is marked as critical and fails, the script - may terminate execution. + This function examines the results of a designated readiness test, logging the outcome with an + appropriate level of severity (info, warning, error) based on the test's criticality and its results. + For critical tests marked as 'exit_on_failure', the function will halt the script execution if the + test fails, indicating a condition that precludes a successful upgrade. Parameters ---------- result : dict - A dictionary where each key corresponds to a readiness test name. The value is another dictionary - containing two keys: 'state' (a boolean indicating the test's success or failure) and 'reason' - (a string explaining the outcome). - + The result dictionary containing keys for each readiness test. Each key maps to another dictionary + with 'state' (boolean indicating pass or fail) and 'reason' (string describing the result). test_name : str - The name of the test to evaluate. This name should correspond to a key in the 'result' dictionary. - + The name of the readiness test to be evaluated, which should match a key in the 'result' dictionary. test_info : dict - Information about the test, including its description, log level (info, warning, error), and a flag - indicating whether to exit the script upon test failure (exit_on_failure). - - Notes - ----- - - The function utilizes the `get_emoji` helper function to add appropriate emojis to log messages, - enhancing readability and user experience. - - If 'state' in the test result is True, the test is logged as passed. Otherwise, it is either - logged as failed or skipped, based on the specified log level in 'test_info'. + A dictionary providing details about the test, including its description, logging level (info, warning, error), + and a boolean flag 'exit_on_failure' indicating whether script execution should be terminated on test failure. Raises ------ SystemExit - If a critical test (marked with "exit_on_failure": True) fails, the script will raise SystemExit. + If a test marked with 'exit_on_failure': True fails, the function will terminate the script execution to + prevent proceeding with an upgrade that is likely to fail or cause issues. + + Notes + ----- + - Utilizes custom logging levels and emojis for enhanced readability and user experience in log outputs. + - The function is part of a larger upgrade readiness assessment process, ensuring the firewall is prepared for an upgrade. + + Example + ------- + Evaluating and logging a readiness test result: + >>> result = {'test_connectivity': {'state': True, 'reason': 'Successful connection'}} + >>> test_name = 'test_connectivity' + >>> test_info = {'description': 'Test Connectivity', 'log_level': 'info', 'exit_on_failure': False} + >>> check_readiness_and_log(result, test_name, test_info) + # Logs "✅ Passed Readiness Check: Test Connectivity - Successful connection" """ test_result = result.get( test_name, {"state": False, "reason": "Test not performed"} @@ -1689,14 +1694,41 @@ def check_readiness_and_log( def compare_versions(version1: str, version2: str) -> str: """ - Compares two PAN-OS version strings. + Compares two PAN-OS version strings and determines their relative ordering. + + This function parses and compares two version strings to identify which is newer, older, or if they are identical. + It is designed to work with PAN-OS versioning scheme, handling standard major.minor.maintenance (and optional hotfix) formats. + The comparison is useful for upgrade processes, version checks, and ensuring compatibility or prerequisites are met. + + Parameters + ---------- + version1 : str + The first PAN-OS version string to compare. Example format: '10.0.1', '9.1.3-h3'. + version2 : str + The second PAN-OS version string to compare. Example format: '10.0.2', '9.1.4'. - Parameters: - version1 (str): First version string to compare. - version2 (str): Second version string to compare. + Returns + ------- + str + - 'older' if version1 is older than version2. + - 'newer' if version1 is newer than version2. + - 'equal' if both versions are the same. + + Notes + ----- + - Version strings are parsed and compared based on numerical ordering of their components (major, minor, maintenance, hotfix). + - Hotfix versions (if present) are considered in the comparison, with higher numbers indicating newer versions. + + Example + ------- + Comparing two PAN-OS versions: + >>> compare_versions('10.0.1', '10.0.2') + 'older' + >>> compare_versions('10.1.0-h3', '10.1.0') + 'newer' + >>> compare_versions('9.1.3-h3', '9.1.3-h3') + 'equal' - Returns: - str: 'older' if version1 < version2, 'newer' if version1 > version2, 'equal' if they are the same. """ parsed_version1 = parse_version(version1) parsed_version2 = parse_version(version2) @@ -1711,28 +1743,46 @@ def compare_versions(version1: str, version2: str) -> str: def configure_logging(level: str, encoding: str = "utf-8") -> None: """ - Sets up the logging configuration for the script with the specified logging level and encoding. + Configures the logging system for the application, specifying log level and file encoding. - This function initializes the global logger, sets the specified logging level, and configures two handlers: - one for console output and another for file output. It uses RotatingFileHandler for file logging to manage - file size and maintain backups. + Initializes the logging framework with a dual-handler approach: console and file output. The console output + provides real-time logging information in the terminal, while the file output stores log messages in a + rotating log file. The function allows customization of the logging level, impacting the verbosity of log + messages. The file handler employs a rotating mechanism to manage log file size and preserve log history. Parameters ---------- level : str - The desired logging level (e.g., 'debug', 'info', 'warning', 'error', 'critical'). - The input is case-insensitive. If an invalid level is provided, it defaults to 'info'. - + The logging level to set for the logger. Accepted values include 'DEBUG', 'INFO', 'WARNING', 'ERROR', + and 'CRITICAL', with case insensitivity. Defaults to 'INFO' if an unrecognized level is specified. encoding : str, optional - The encoding format for the file-based log handler, by default 'utf-8'. + Character encoding for log files. Defaults to 'utf-8', ensuring broad compatibility and support for + international characters. Notes ----- - - The Console Handler outputs log messages to the standard output. - - The File Handler logs messages to 'logs/upgrade.log'. This file is rotated when it reaches 1MB in size, - maintaining up to three backup files. - - The logging level influences the verbosity of the log messages. An invalid level defaults to 'info', - ensuring a baseline of logging. + - Logging setup includes formatting for both console and file handlers, with more detailed formatting applied + to file logs. + - The file logging employs `RotatingFileHandler` for automatic log rotation, maintaining up to three backup + files, each limited to 1MB. + - The function clears existing handlers to prevent duplication and ensure that logging configuration + reflects the specified parameters. + + Examples + -------- + Setting up logging with default encoding: + >>> configure_logging('debug') + # Configures logging with DEBUG level and utf-8 encoding. + + Setting up logging with custom encoding: + >>> configure_logging('info', 'iso-8859-1') + # Configures logging with INFO level and ISO-8859-1 encoding. + + Raises + ------ + ValueError + If the `level` parameter does not correspond to a valid logging level, a ValueError is raised to + indicate the invalid input. """ logging_level = getattr(logging, level.upper(), None) @@ -1781,41 +1831,51 @@ def connect_to_host( api_password: str, ) -> PanDevice: """ - Establishes a connection to a Panorama or PAN-OS firewall appliance using provided credentials. + Initiates a connection to a specified PAN-OS device or Panorama using API credentials. - This function uses the hostname, username, and password to attempt a connection to a target appliance, - which can be either a Panorama management server or a PAN-OS firewall. It identifies the type of - appliance based on the provided credentials and hostname. Upon successful connection, it returns an - appropriate PanDevice object (either Panorama or Firewall). + Attempts to connect to a Palo Alto Networks device (either a firewall or a Panorama management server) + using the hostname (or IP address) along with the API username and password provided. The function determines + the type of device based on the response and establishes a session. On successful connection, it returns an + instance of the device as a `PanDevice` object, which can be either a `Firewall` or `Panorama` instance, + depending on the target device. Parameters ---------- hostname : str - The DNS Hostname or IP address of the target appliance. + The IP address or DNS hostname of the target PAN-OS device or Panorama. api_username : str - Username for authentication. + The username for API access. api_password : str - Password for authentication. + The password for API access. Returns ------- PanDevice - An instance of PanDevice (either Panorama or Firewall), representing the established connection. + A `PanDevice` object representing the connected PAN-OS device or Panorama. Raises ------ SystemExit - If the connection attempt fails, such as due to a timeout, incorrect credentials, or other errors. + Terminates the script if there is a failure to connect, such as due to incorrect credentials, + network issues, or other connection errors. - Example + Examples -------- - Connecting to a Panorama management server: - >>> connect_to_host('panorama.example.com', 'admin', 'password') - + Connecting to a firewall device: + >>> device = connect_to_host('192.168.1.1', 'apiuser', 'apipass') + >>> print(type(device)) + + + Connecting to a Panorama device: + >>> panorama = connect_to_host('panorama.company.com', 'apiuser', 'apipass') + >>> print(type(panorama)) + - Connecting to a PAN-OS firewall: - >>> connect_to_host('192.168.0.1', 'admin', 'password') - + Notes + ----- + - This function abstracts the connection logic, handling both firewall and Panorama connections seamlessly. + - Error handling within the function ensures that any connection issues are clearly logged, and the script + is exited gracefully to avoid proceeding without a valid connection. """ try: target_device = PanDevice.create_from_device( @@ -1828,14 +1888,14 @@ def connect_to_host( except PanConnectionTimeout: logging.error( - f"{get_emoji('error')} Connection to the {hostname} appliance timed out. Please check the DNS hostname or IP address and network connectivity." + f"Connection to the {hostname} appliance timed out. Please check the DNS hostname or IP address and network connectivity." ) sys.exit(1) except Exception as e: logging.error( - f"{get_emoji('error')} An error occurred while connecting to the {hostname} appliance: {e}" + f"An error occurred while connecting to the {hostname} appliance: {e}" ) sys.exit(1) @@ -1843,24 +1903,32 @@ def connect_to_host( def ensure_directory_exists(file_path: str) -> None: """ - Ensures the existence of the directory for a specified file path, creating it if necessary. + Checks and creates the directory structure for a given file path if it does not already exist. - This function checks if the directory for a given file path exists. If it does not exist, the function - creates the directory along with any necessary parent directories. This is particularly useful for - ensuring that the file system is prepared for file operations that require specific directory structures. + This utility function is used to verify the existence of a directory path derived from a full file path. + If the directory does not exist, the function creates it along with any intermediate directories. This + ensures that subsequent file operations such as saving or reading files can proceed without directory + not found errors. Parameters ---------- file_path : str - The file path whose directory needs to be verified and potentially created. The function extracts - the directory part of the file path to check its existence. + The full path to a file, including the filename. The function will extract the directory path from + this and ensure that the directory exists. + + Notes + ----- + - This function is useful for preparing the file system to store files at specified locations, + especially when the directory structure may not have been created in advance. + - The function uses `os.makedirs` which allows creating intermediate directories needed to ensure the + full path exists. Example ------- - Ensuring a directory exists for a file path: - >>> file_path = '/path/to/directory/file.txt' + Creating a directory structure for storing a configuration backup: + >>> file_path = '/var/backups/firewall/config_backup.xml' >>> ensure_directory_exists(file_path) - # If '/path/to/directory/' does not exist, it is created. + # This will create the '/var/backups/firewall/' directory if it doesn't exist. """ directory = os.path.dirname(file_path) if not os.path.exists(directory): @@ -1869,42 +1937,47 @@ def ensure_directory_exists(file_path: str) -> None: def filter_string_to_dict(filter_string: str) -> dict: """ - Converts a string containing comma-separated key-value pairs into a dictionary. + Converts a filter string with comma-separated key-value pairs into a dictionary. - This utility function parses a string where each key-value pair is separated by a comma, and - each key is separated from its value by an equal sign ('='). It's useful for converting filter - strings into dictionary formats, commonly used in configurations and queries. + This function is designed to parse strings formatted with key-value pairs, where each pair is + separated by a comma, and the key and value within a pair are separated by an equal sign ('='). + It's particularly useful for processing query parameters or configuration settings where this + format is commonly used. The function ensures that even if the input string is empty or not properly + formatted, the operation is handled gracefully, returning an empty dictionary in such cases. Parameters ---------- filter_string : str - The string to be parsed into key-value pairs. It should follow the format 'key1=value1,key2=value2,...'. - If the string is empty or improperly formatted, an empty dictionary is returned. + A string containing key-value pairs separated by commas, e.g., 'key1=value1,key2=value2'. + Keys and values are expected to be strings. If the string is empty or does not conform to the + expected format, the function returns an empty dictionary. Returns ------- dict - A dictionary with keys and values derived from the `filter_string`. Keys are the substrings before each '=' - character, and values are the corresponding substrings after the '=' character. + A dictionary representation of the key-value pairs extracted from `filter_string`. + If `filter_string` is empty or malformatted, an empty dictionary is returned. Examples -------- - Converting a filter string to a dictionary: - >>> filter_string_to_dict("hostname=test,serial=11111") - {'hostname': 'test', 'serial': '11111'} + Converting a well-formed filter string: + >>> filter_string_to_dict('type=firewall,model=PA-220') + {'type': 'firewall', 'model': 'PA-220'} - Handling an empty or improperly formatted string: - >>> filter_string_to_dict("") + Handling an empty string: + >>> filter_string_to_dict('') {} - >>> filter_string_to_dict("invalid_format_string") + + Handling a string without equal signs: + >>> filter_string_to_dict('incorrect,format') {} Notes ----- - - The function does not perform validation on the key-value pairs. It's assumed that the input string is - correctly formatted. - - In case of duplicate keys, the last occurrence of the key in the string will determine its value in the - resulting dictionary. + - This function does not validate the keys and values extracted from the input string; it simply + splits the string based on the expected delimiters (',' and '='). + - If the same key appears multiple times in the input string, the value associated with the last + occurrence of the key will be retained in the output dictionary. """ result = {} for substr in filter_string.split(","): @@ -1916,41 +1989,47 @@ def filter_string_to_dict(filter_string: str) -> dict: def flatten_xml_to_dict(element: ET.Element) -> dict: """ - Converts a given XML element to a dictionary, flattening the XML structure. + Flattens an XML ElementTree element into a nested dictionary, preserving the hierarchical structure. - This function recursively processes an XML element, converting it and its children into a dictionary format. - The conversion flattens the XML structure, making it easier to adapt to model definitions. It treats elements - containing only text as leaf nodes, directly mapping their tags to their text content. For elements with child - elements, it continues the recursion. The function handles multiple occurrences of the same tag by aggregating - them into a list. Specifically, it always treats elements with the tag 'entry' as lists, reflecting their - common usage pattern in PAN-OS XML API responses. + This utility function is particularly useful for processing XML data returned by APIs, such as the PAN-OS XML API. + It iterates through the given XML element and its children, converting each element into a dictionary key-value pair, + where the key is the element's tag and the value is the element's text content or a nested dictionary representing + any child elements. Elements with the same tag name at the same level are grouped into a list. This function + simplifies complex XML structures, making them more accessible for Pythonic manipulation and analysis. Parameters ---------- element : ET.Element - The XML element to be converted. This should be an instance of ElementTree.Element, typically obtained - from parsing XML data using the ElementTree API. + The root XML element to convert into a dictionary. This element may contain nested child elements, which will + be recursively processed into nested dictionaries. Returns ------- dict - A dictionary representation of the XML element. The dictionary mirrors the structure of the XML, - with tags as keys and text content or nested dictionaries as values. Elements with the same tag - are aggregated into a list. + A dictionary representation of the input XML element. Each key in the dictionary corresponds to a tag in the XML, + and each value is either the text content of the element, a nested dictionary for child elements, or a list of + dictionaries for repeated child elements. Special handling is applied to elements with the tag 'entry', which are + always treated as lists to accommodate common XML structures in PAN-OS API responses. + + Examples + -------- + Converting a simple XML structure without attributes: + >>> xml_str = 'Firewall1Office' + >>> element = ET.fromstring(xml_str) + >>> flatten_xml_to_dict(element) + {'device': {'name': 'Firewall1', 'location': 'Office'}} + + Handling multiple child elements with the same tag: + >>> xml_str = 'Server1Server2' + >>> element = ET.fromstring(xml_str) + >>> flatten_xml_to_dict(element) + {'server': ['Server1', 'Server2']} Notes ----- - - This function is designed to work with PAN-OS XML API responses, which often use the 'entry' tag - to denote list items. - - The function does not preserve attributes of XML elements; it focuses solely on tags and text content. - - Example - ------- - Converting an XML element with nested children to a dictionary: - >>> xml_string = "valuesubvalue" - >>> xml_element = ET.fromstring(xml_string) - >>> flatten_xml_to_dict(xml_element) - {'child': ['value', {'subchild': 'subvalue'}]} + - The function ignores XML attributes and focuses solely on tags and text content. + - Repeated tags at the same level are grouped into a list to preserve the XML structure. + - The 'entry' tag, frequently used in PAN-OS XML API responses, is always treated as a list item to reflect its typical use as a container for multiple items. """ result = {} for child_element in element: @@ -1979,33 +2058,43 @@ def flatten_xml_to_dict(element: ET.Element) -> dict: def get_emoji(action: str) -> str: """ - Retrieves an emoji character corresponding to a specific action keyword. + Provides a visual representation in the form of an emoji for various logging and notification actions. - This function is used to enhance the visual appeal and readability of log messages or console outputs. - It maps predefined action keywords to their corresponding emoji characters. + This utility function maps a set of predefined action keywords to their corresponding emoji characters, + enhancing the user experience by adding a visual cue to log messages, console outputs, or user interfaces. + It supports a variety of action keywords, each associated with a specific emoji that intuitively represents + the action's nature or outcome. Parameters ---------- action : str - An action keyword for which an emoji is required. Supported keywords include 'success', - 'warning', 'error', 'working', 'report', 'search', 'save', 'stop', and 'start'. + The action keyword representing the specific operation or outcome. Supported keywords include 'success', + 'warning', 'error', 'working', 'report', 'search', 'save', 'stop', and 'start'. The function is designed + to be easily extendable with additional keywords and emojis as needed. Returns ------- str - The emoji character associated with the action keyword. If the keyword is not recognized, - returns an empty string. + An emoji character as a string corresponding to the provided action keyword. If the keyword is not + recognized, the function returns an empty string, ensuring graceful handling of unsupported actions. Examples -------- - >>> get_emoji('success') - '✅' # Indicates a successful operation + Adding visual cues to log messages: + >>> logging.info(f"{get_emoji('success')} Operation completed successfully.") + >>> logging.warning(f"{get_emoji('warning')} Proceed with caution.") + >>> logging.error(f"{get_emoji('error')} An error occurred.") - >>> get_emoji('error') - '❌' # Indicates an error + Enhancing console outputs: + >>> print(f"{get_emoji('start')} Starting the process...") + >>> print(f"{get_emoji('stop')} Process terminated.") - >>> get_emoji('start') - '🚀' # Indicates the start of a process + Notes + ----- + - The function is designed for extensibility, allowing easy addition of new action keywords and corresponding + emojis without impacting existing functionality. + - Emojis are selected to universally convey the essence of the action, ensuring clarity and immediacy in + communication. """ emoji_map = { "success": "✅", @@ -2023,33 +2112,41 @@ def get_emoji(action: str) -> str: def get_firewalls_from_panorama(panorama: Panorama, **filters) -> list[Firewall]: """ - Retrieves a list of Firewall objects associated with a Panorama appliance, filtered by specified criteria. + Retrieves a list of firewalls managed by a specified Panorama, optionally filtered by custom criteria. - This function queries a Panorama appliance for its managed firewalls and filters the results based on the - provided keyword arguments. The firewalls that match the specified filters are then instantiated as `Firewall` - objects. These firewall objects are also attached to the Panorama instance, allowing API calls to be proxied - through the Panorama. + This function interacts with a Panorama appliance to obtain a list of managed firewalls. It allows for + filtering the firewalls based on various attributes, such as model, serial number, or software version, + using regular expressions. Each matched firewall is instantiated as a `Firewall` object, facilitating + subsequent operations on these firewalls through their respective `Firewall` instances. The filtering + mechanism provides a flexible way to selectively work with subsets of firewalls under Panorama management. Parameters ---------- panorama : Panorama - An instance of the Panorama class, representing the Panorama appliance to query. - - filters : **kwargs - Keyword argument filters to apply. Each keyword should correspond to an attribute of the `ManagedDevice` - model class. The value for each keyword is a regex pattern to match against the corresponding attribute. + The Panorama instance through which the firewalls are managed. This should be an authenticated + instance with access to the Panorama's API. + **filters : dict + Arbitrary keyword arguments representing the filter criteria. Each keyword corresponds to a firewall + attribute (e.g., model, serial number), and its value is a regex pattern against which the attribute is matched. Returns ------- list[Firewall] - A list of `Firewall` instances that match the specified filters. + A list containing `Firewall` instances for each firewall managed by Panorama that matches the provided + filter criteria. If no filters are provided, all managed firewalls are returned. Example ------- - Getting firewalls from Panorama with specific model filters: - >>> panorama = Panorama('192.168.1.1', 'admin', 'password') - >>> firewalls = get_firewalls_from_panorama(panorama, model='.*220.*') - # Returns a list of `Firewall` instances for firewalls with models containing '220'. + Retrieving firewalls of a specific model from Panorama: + >>> panorama = Panorama(hostname='panorama.example.com', api_username='admin', api_password='password') + >>> filtered_firewalls = get_firewalls_from_panorama(panorama, model='PA-220') + # This will return all firewalls of model PA-220 managed by the specified Panorama. + + Notes + ----- + - The function requires an authenticated Panorama instance to query the Panorama API. + - Filters are applied using regular expressions, providing flexibility in specifying match criteria. + - Instantiated `Firewall` objects are linked to the Panorama instance, allowing API calls to be proxied. """ firewalls = [] for managed_device in get_managed_devices(panorama, **filters): @@ -2062,35 +2159,42 @@ def get_firewalls_from_panorama(panorama: Panorama, **filters) -> list[Firewall] def get_managed_devices(panorama: Panorama, **filters) -> list[ManagedDevice]: """ - Retrieves a filtered list of managed devices from a specified Panorama appliance. + Retrieves a list of devices managed by a specified Panorama, optionally filtered by custom criteria. - This function queries a Panorama appliance for its managed devices and filters the results - based on the provided keyword arguments. Each keyword argument must correspond to an - attribute of the `ManagedDevice` model. The function applies regex matching for each - filter, returning only those devices that match all specified filters. + This function communicates with a Panorama appliance to fetch a list of managed devices, allowing for + filtering based on various attributes such as hostname, model, serial number, etc., using regular expressions. + The matched devices are returned as instances of `ManagedDevice`, facilitating further operations on these + devices. The filtering mechanism provides a flexible way to work selectively with subsets of devices under + Panorama management. Parameters ---------- panorama : Panorama - An instance of the Panorama class, representing the Panorama appliance to query. - - filters : **kwargs - Keyword argument filters to apply. Each keyword should correspond to an attribute - of the `ManagedDevice` model class. The value for each keyword is a regex pattern - to match against the corresponding attribute. + The Panorama instance through which the managed devices are accessed. This should be an authenticated + instance with the capability to execute API calls against the Panorama. + **filters : dict + Arbitrary keyword arguments representing the filter criteria. Each keyword corresponds to a managed + device attribute (e.g., hostname, model), and its value is a regex pattern against which the attribute + is matched. Returns ------- list[ManagedDevice] - A list of `ManagedDevice` instances that match the specified filters. + A list containing `ManagedDevice` instances for each device managed by Panorama that matches the + provided filter criteria. If no filters are provided, all managed devices are returned. Example ------- - Retrieving devices from Panorama with specific hostname and model filters: - >>> panorama = Panorama('192.168.1.1', 'admin', 'password') - >>> managed_devices = get_managed_devices(panorama, hostname='^PA-220$', model='.*220.*') - # Returns a list of `ManagedDevice` instances for devices with hostnames matching 'PA-220' - # and model containing '220'. + Retrieving managed devices from Panorama with specific model filters: + >>> panorama = Panorama(hostname='panorama.example.com', api_username='admin', api_password='password') + >>> filtered_devices = get_managed_devices(panorama, model='PA-220') + # This will return all managed devices of model PA-220. + + Notes + ----- + - The function requires an authenticated Panorama instance to query the Panorama API. + - Filters are applied using regular expressions, providing flexibility in specifying match criteria. + - The `ManagedDevice` instances facilitate further interactions with the managed devices through the Panorama. """ managed_devices = model_from_api_response( panorama.op("show devices all"), ManagedDevices @@ -2104,31 +2208,43 @@ def get_managed_devices(panorama: Panorama, **filters) -> list[ManagedDevice]: def ip_callback(value: str) -> str: """ - Validates the input as a valid IP address or a resolvable hostname. + Validates and returns an IP address or resolvable hostname provided as a command-line argument. - This function first attempts to resolve the hostname via DNS query. If it fails, - it utilizes the ip_address function from the ipaddress standard library module to - validate the provided input as an IP address. It is designed to be used as a callback - function for Typer command-line argument parsing, ensuring that only valid IP addresses - or resolvable hostnames are accepted as input. + This callback function is intended for use with command-line interfaces built with Typer. It ensures that + the user-provided input is either a valid IPv4/IPv6 address or a hostname that can be resolved to an IP address. + The function first attempts to resolve the input as a hostname. If unsuccessful, it then checks if the input + is a valid IP address. This dual check ensures flexibility in accepting either form of network address identification. Parameters ---------- value : str - A string representing the IP address or hostname to be validated. + The user input string intended to represent an IP address or hostname. Returns ------- str - The validated IP address string or hostname. + The original input value if it is a valid IP address or resolvable hostname. Raises ------ typer.BadParameter - If the input string is not a valid IP address or a resolvable hostname, a typer.BadParameter - exception is raised with an appropriate error message. - """ + This exception is raised if the input value is neither a valid IP address nor a resolvable hostname, + providing feedback to the user to correct their input. + Example + -------- + Validating a user-provided IP address or hostname: + >>> @app.command() + >>> def command(hostname: str = typer.Option(..., callback=ip_callback)): + >>> print(f"Hostname/IP: {hostname}") + # This CLI command requires a valid hostname or IP address as an argument. + + Notes + ----- + - The function leverages the 'ipaddress' standard library for IP address validation and a custom + 'resolve_hostname' function (not shown) for DNS resolution. + - Intended for use with Typer-based CLI applications to validate network address inputs. + """ # First, try to resolve as a hostname if resolve_hostname(value): return value @@ -2149,43 +2265,89 @@ def model_from_api_response( model: type[FromAPIResponseMixin], ) -> FromAPIResponseMixin: """ - Converts an XML Element, typically from an API response, into a specified Pydantic model. + Transforms an XML element or tree from an API response into a structured Pydantic model. - This function facilitates the transformation of XML data into a structured Pydantic model. - It first flattens the XML Element into a dictionary and then maps this dictionary to the - specified Pydantic model. This approach simplifies the handling of complex XML structures - often returned by APIs, enabling easier manipulation and access to the data within Python. + This utility function streamlines the conversion of XML data, commonly encountered in API responses, + into a structured format by leveraging Pydantic models. It employs a two-step process: first, it flattens + the XML structure into a dictionary using a recursive approach, and then it maps this dictionary onto a + Pydantic model that's capable of handling data derived from API responses. This process facilitates the + extraction and utilization of specific data points from complex XML structures in a more Pythonic and + accessible manner. Parameters ---------- element : Union[ET.Element, ET.ElementTree] - The XML Element or ElementTree to be converted. This is typically obtained from parsing - XML data returned by an API call. - + An XML element or tree obtained from parsing an API's XML response, representing the data to be + converted into a Pydantic model. model : type[FromAPIResponseMixin] - The Pydantic model class into which the XML data will be converted. This model must - inherit from the FromAPIResponseMixin, indicating it can handle data derived from - API responses. + A Pydantic model class that includes FromAPIResponseMixin, indicating it's designed to be populated + with data from an API response. This model defines the structure and fields expected from the XML data. Returns ------- FromAPIResponseMixin - An instance of the specified Pydantic model, populated with data extracted from the - provided XML Element. + An instance of the specified Pydantic model populated with the data extracted from the input XML element + or tree. The model instance provides structured access to the data, adhering to the definitions within the model. Example ------- - Converting an XML response to a Pydantic model: - >>> xml_element = ET.fromstring('value') - >>> MyModel = type('MyModel', (FromAPIResponseMixin, BaseModel), {}) - >>> model_instance = model_from_api_response(xml_element, MyModel) - # 'model_instance' is now an instance of 'MyModel' with data from 'xml_element'. + Parsing an API's XML response into a Pydantic model: + >>> xml_response = ET.fromstring('John Doejohn@example.com') + >>> UserModel = type('UserModel', (FromAPIResponseMixin, BaseModel), {'name': str, 'email': str}) + >>> user = model_from_api_response(xml_response, UserModel) + # 'user' is an instance of 'UserModel' with 'name' and 'email' fields populated from 'xml_response'. + + Notes + ----- + - The function assumes that the input XML element/tree structure corresponds to the structure expected by + the Pydantic model. Mismatches between the XML data and the model's fields may result in incomplete or + incorrect model instantiation. + - This function is particularly useful in scenarios where API responses need to be deserialized into + concrete Python objects for further processing, validation, or manipulation. """ result_dict = flatten_xml_to_dict(element) return model.from_api_response(result_dict) def parse_version(version: str) -> Tuple[int, int, int, int]: + """ + Parses a version string into a tuple of integers representing its components. + + This function takes a PAN-OS version string and splits it into major, minor, maintenance, + and hotfix components. The version string is expected to be in the format 'major.minor.maintenance' + or 'major.minor.maintenance-hhotfix', where 'major', 'minor', 'maintenance', and 'hotfix' are integers. + If the 'maintenance' version includes a hotfix indicated by '-h', it is separated into its own component. + Missing components are defaulted to 0. + + Parameters + ---------- + version : str + The version string to be parsed. Expected formats include 'major.minor.maintenance' or + 'major.minor.maintenance-hhotfix'. + + Returns + ------- + Tuple[int, int, int, int] + A tuple containing four integers representing the major, minor, maintenance, and hotfix + components of the version. Missing components are defaulted to 0. + + Example + ------- + Parsing a version string without a hotfix: + >>> parse_version("10.1.2") + (10, 1, 2, 0) + + Parsing a version string with a hotfix: + >>> parse_version("10.1.2-h3") + (10, 1, 2, 3) + + Notes + ----- + - The function assumes that the version string is correctly formatted. Malformed strings may + lead to unexpected results. + - This utility is particularly useful for comparing PAN-OS versions, facilitating upgrades and + ensuring compatibility requirements are met. + """ parts = version.split(".") if len(parts) == 2: # When maintenance version is an integer major, minor = parts @@ -2202,27 +2364,41 @@ def parse_version(version: str) -> Tuple[int, int, int, int]: def resolve_hostname(hostname: str) -> bool: """ - Checks if a given hostname can be resolved via DNS query. + Attempts to resolve a hostname to an IP address using DNS lookup. - This function attempts to resolve the specified hostname using DNS. It queries the DNS servers - that the operating system is configured to use. The function is designed to return a boolean - value indicating whether the hostname could be successfully resolved or not. + This function checks if the provided hostname is resolvable by performing a DNS lookup. + It uses the DNS resolver settings configured on the system to query for the IP address associated + with the hostname. A successful resolution indicates network connectivity and DNS functionality + with respect to the hostname, while a failure may suggest issues with the hostname, DNS configuration, + or network connectivity. Parameters ---------- hostname : str - The hostname (e.g., 'example.com') to be resolved. + The hostname (e.g., 'example.com') that needs to be resolved to check its validity and network + accessibility. Returns ------- bool - Returns True if the hostname can be resolved, False otherwise. + True if the hostname is successfully resolved to an IP address, indicating it is valid and + accessible. False if the hostname cannot be resolved, suggesting it may be invalid, the DNS + servers are unreachable, or the network is experiencing issues. - Raises - ------ - None - This function does not raise any exceptions. It handles all exceptions internally and - returns False in case of any issues during the resolution process. + Example + ------- + Checking if a hostname can be resolved: + >>> resolve_hostname('www.example.com') + True # Assuming 'www.example.com' is resolvable + + >>> resolve_hostname('nonexistent.hostname') + False # Assuming 'nonexistent.hostname' cannot be resolved + + Notes + ----- + - This function can be used as a preliminary check before attempting network connections to a hostname. + - It handles exceptions internally and logs them for debugging purposes, ensuring the calling code + can make decisions based on the boolean return value without handling exceptions directly. """ try: dns.resolver.resolve(hostname) @@ -2290,6 +2466,7 @@ def main( "--filter", "-f", help="Filter string - when connecting to Panorama, defines which devices we are to upgrade.", + prompt="Filter string (only applicable for Panorama)", ), ] = "", log_level: Annotated[ @@ -2302,60 +2479,50 @@ def main( ] = "info", ): """ - Orchestrates the upgrade process for PAN-OS firewalls, including both standalone and HA configurations. + Orchestrates the upgrade process for Palo Alto Networks PAN-OS devices. - This script automates the process of upgrading Palo Alto Networks firewalls to a specified PAN-OS version. It - supports both standalone firewalls and those managed by Panorama. The script can perform a full upgrade process - or a dry run, which includes all pre-upgrade checks without applying the actual upgrade. It handles various - aspects like readiness checks, configuration backup, software download, and reboot procedures. + This script automates the upgrade of PAN-OS firewalls and Panorama management servers to a specified version. + It encompasses various stages including connection establishment, device filtering (for Panorama), pre-upgrade + checks, software download, and the upgrade process itself. The script supports a dry run mode to simulate the + upgrade process without making changes. It is designed to be run from the command line and accepts various + parameters to control its operation. Parameters ---------- hostname : str - Hostname or IP address of the Panorama or firewall appliance. + The hostname or IP address of the Panorama or firewall device. username : str - Username for authentication with the appliance. + The administrative username for device authentication. password : str - Password for authentication. + The administrative password for device authentication. target_version : str - Target PAN-OS version for the upgrade. + The target PAN-OS version to upgrade the device(s) to. dry_run : bool, optional - If True, performs a dry run without executing the actual upgrade (default is False). + If True, simulates the upgrade process without applying changes (default is False). filter : str, optional - When connecting to Panorama, defines the filter criteria for selecting devices to upgrade (default is ""). + A filter string to select specific devices managed by Panorama for the upgrade (default is ""). log_level : str, optional - Sets the logging level for script execution (default is "info"). + Specifies the logging level for the script's output (default is "info"). - Workflow + Raises + ------ + SystemExit + The script will exit if it encounters critical errors during execution, such as connection failures, + invalid filter strings, or errors during the upgrade process. + + Examples -------- - 1. Initializes necessary directories and logging configuration. - 2. Establishes a connection to the specified Panorama or firewall appliance. - 3. If connected to Panorama, filters and retrieves the list of firewalls to upgrade. - 4. Sequentially processes each firewall, performing readiness checks, downloading necessary software, and executing the upgrade and reboot steps. + Upgrading a standalone firewall: + $ python upgrade.py --hostname 192.168.1.1 --username admin --password secret --version 10.1.0 - Exits - ------ - - On critical errors that prevent continuation of the script. - - After successfully completing a dry run. - - If the firewall is not ready for the intended upgrade. - - If HA synchronization issues are detected in HA configurations. - - Example Usage - -------------- - Upgrading a firewall to version '10.2.7': - ```bash - python upgrade.py --hostname 192.168.1.1 --username admin --password secret --version 10.2.7 - ``` - Upgrading a firewall to version '10.2.7' by using Panorama appliance as a proxy: - ```bash - python upgrade.py --hostname panorama.cdot.io --filter "hostname=houston" --username admin --password secret --version 10.2.7 - ``` + Performing a dry run on a Panorama-managed device: + $ python upgrade.py --hostname panorama.example.com --username admin --password secret --version 10.1.0 --dry-run --filter "serial=0123456789" Notes ----- - - The script operates serially on each identified firewall. - - Currently, the script is not HA-aware, meaning it does not handle upgrades of both firewalls in an HA pair simultaneously. - - The script includes extensive logging, providing detailed feedback throughout the upgrade process. + - The script uses threads to parallelize upgrades for multiple devices managed by Panorama. + - It is recommended to back up the device configuration before running the script, especially for production environments. + - The `--filter` option is applicable only when connecting to Panorama and must conform to the syntax expected by the `get_firewalls_from_panorama` function. """ # Create necessary directories From c4241ed5b2727ccae0c708b26e7eb87342e17137 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Thu, 25 Jan 2024 09:00:34 -0600 Subject: [PATCH 06/13] adding hostname to log messages to differentiate within multi-threaded context --- pan_os_upgrade/upgrade.py | 459 ++++++++++++++++++++++++-------------- 1 file changed, 296 insertions(+), 163 deletions(-) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index 021a907..2e952fe 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -263,6 +263,7 @@ class AssuranceOptions: # ---------------------------------------------------------------------------- def backup_configuration( firewall: Firewall, + hostname: str, file_path: str, ) -> bool: """ @@ -276,6 +277,8 @@ def backup_configuration( ---------- firewall : Firewall The instance of the firewall from which the running configuration is to be backed up. + hostname : str + The hostname of the firewall. This is used for logging and reporting purposes. file_path : str The filesystem path where the configuration backup file will be stored. @@ -299,7 +302,7 @@ def backup_configuration( -------- Backing up the configuration of a firewall: >>> firewall_instance = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> backup_configuration(firewall_instance, '/path/to/config_backup.xml') + >>> backup_configuration(firewall_instance, 'firewall1', '/path/to/config_backup.xml') True # Indicates that the backup was successful. """ try: @@ -307,7 +310,7 @@ def backup_configuration( config_xml = firewall.op("show config running") if config_xml is None: logging.error( - f"{get_emoji('error')} Failed to retrieve running configuration." + f"{get_emoji('error')} {hostname}: Failed to retrieve running configuration." ) return False @@ -318,7 +321,7 @@ def backup_configuration( or config_xml[0].tag != "result" ): logging.error( - f"{get_emoji('error')} Unexpected XML structure in configuration data." + f"{get_emoji('error')} {hostname}: Unexpected XML structure in configuration data." ) return False @@ -336,17 +339,20 @@ def backup_configuration( file.write(config_str) logging.debug( - f"{get_emoji('save')} Configuration backed up successfully to {file_path}" + f"{get_emoji('save')} {hostname}: Configuration backed up successfully to {file_path}" ) return True except Exception as e: - logging.error(f"{get_emoji('error')} Error backing up configuration: {e}") + logging.error( + f"{get_emoji('error')} {hostname}: Error backing up configuration: {e}" + ) return False def determine_upgrade( firewall: Firewall, + hostname: str, target_major: int, target_minor: int, target_maintenance: Union[int, str], @@ -365,6 +371,8 @@ def determine_upgrade( ---------- firewall : Firewall The Firewall instance whose PAN-OS version is under evaluation. + hostname : str + The hostname of the firewall. This is used for logging and reporting purposes. target_major : int The major version number of the target PAN-OS. target_minor : int @@ -387,7 +395,7 @@ def determine_upgrade( -------- Determining the need for an upgrade: >>> firewall_instance = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> determine_upgrade(firewall_instance, 10, 0, 1) + >>> determine_upgrade(firewall_instance, 'firewall1', 10, 0, 1) # Logs information about the current version and the necessity of an upgrade to version 10.0.1. """ current_version = parse_version(firewall.version) @@ -401,24 +409,29 @@ def determine_upgrade( f"{target_major}.{target_minor}.{target_maintenance}" ) - logging.info(f"{get_emoji('report')} Current PAN-OS version: {firewall.version}") logging.info( - f"{get_emoji('report')} Target PAN-OS version: {target_major}.{target_minor}.{target_maintenance}" + f"{get_emoji('report')} {hostname}: Current PAN-OS version: {firewall.version}" + ) + logging.info( + f"{get_emoji('report')} {hostname}: Target PAN-OS version: {target_major}.{target_minor}.{target_maintenance}" ) if current_version < target_version: logging.info( - f"{get_emoji('success')} Upgrade required from {firewall.version} to {target_major}.{target_minor}.{target_maintenance}" + f"{get_emoji('success')} {hostname}: Upgrade required from {firewall.version} to {target_major}.{target_minor}.{target_maintenance}" ) else: logging.error( - f"{get_emoji('error')} No upgrade required or downgrade attempt detected." + f"{get_emoji('error')} {hostname}: No upgrade required or downgrade attempt detected." ) - logging.error(f"{get_emoji('stop')} Halting script.") + logging.error(f"{get_emoji('stop')} {hostname}: Halting script.") sys.exit(1) -def get_ha_status(firewall: Firewall) -> Tuple[str, Optional[dict]]: +def get_ha_status( + firewall: Firewall, + hostname: str, +) -> Tuple[str, Optional[dict]]: """ Retrieves the High-Availability (HA) status and configuration details of a specified firewall. @@ -431,6 +444,8 @@ def get_ha_status(firewall: Firewall) -> Tuple[str, Optional[dict]]: ---------- firewall : Firewall The firewall instance to query for HA status. + hostname : str + The hostname of the firewall. This is used for logging and reporting purposes. Returns ------- @@ -444,7 +459,7 @@ def get_ha_status(firewall: Firewall) -> Tuple[str, Optional[dict]]: ------- Assessing the HA status of a firewall: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> ha_status, ha_config = get_ha_status(firewall) + >>> ha_status, ha_config = get_ha_status(firewall, 'firewall1') >>> print(ha_status) # e.g., 'active/passive' >>> print(ha_config) # e.g., {'local-info': {...}, 'peer-info': {...}} @@ -455,15 +470,17 @@ def get_ha_status(firewall: Firewall) -> Tuple[str, Optional[dict]]: - This function is crucial for understanding the HA configuration of a firewall, especially in complex network setups. """ logging.debug( - f"{get_emoji('start')} Getting {firewall.serial} deployment information..." + f"{get_emoji('start')} {hostname}: Getting {firewall.serial} deployment information..." ) deployment_type = firewall.show_highavailability_state() - logging.debug(f"{get_emoji('report')} Firewall deployment: {deployment_type[0]}") + logging.debug( + f"{get_emoji('report')} {hostname}: Firewall deployment: {deployment_type[0]}" + ) if deployment_type[1]: ha_details = flatten_xml_to_dict(deployment_type[1]) logging.debug( - f"{get_emoji('report')} Firewall deployment details: {ha_details}" + f"{get_emoji('report')} {hostname}: Firewall deployment details: {ha_details}" ) return deployment_type[0], ha_details else: @@ -472,7 +489,7 @@ def get_ha_status(firewall: Firewall) -> Tuple[str, Optional[dict]]: def handle_ha_logic( firewall: Firewall, - target_version: str, + hostname: str, dry_run: bool, ) -> Tuple[bool, Optional[Firewall]]: """ @@ -487,8 +504,8 @@ def handle_ha_logic( ---------- firewall : Firewall The firewall instance to be evaluated for HA related upgrade logic. - target_version : str - The target PAN-OS version intended for the upgrade. + hostname : str + The hostname of the firewall. This is used for logging and reporting purposes. dry_run : bool If True, simulates the HA logic without executing state changes. @@ -503,7 +520,7 @@ def handle_ha_logic( ------- Handling HA logic for a firewall upgrade: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> proceed, peer_firewall = handle_ha_logic(firewall, '10.1.0', dry_run=False) + >>> proceed, peer_firewall = handle_ha_logic(firewall, 'firewall1', dry_run=False) >>> print(proceed) # True or False >>> print(peer_firewall) # Firewall instance or None @@ -513,7 +530,10 @@ def handle_ha_logic( - For active firewalls with passive peers on the same version, the function defers the upgrade process. - In dry run mode, the function does not perform state changes like suspending HA states. """ - deploy_info, ha_details = get_ha_status(firewall) + deploy_info, ha_details = get_ha_status( + firewall, + hostname, + ) # If the firewall is not part of an HA configuration, proceed with the upgrade if not ha_details: @@ -531,35 +551,51 @@ def handle_ha_logic( with firewalls_to_revisit_lock: firewalls_to_revisit.append(firewall) logging.info( - f"{get_emoji('info')} Detected active firewall in HA pair running the same version as its peer. Added firewall to revisit list." + f"{get_emoji('search')} {hostname}: Detected active firewall in HA pair running the same version as its peer. Added firewall to revisit list." ) return False, None elif local_state == "passive": # Continue with upgrade process on the passive firewall - logging.debug(f"{get_emoji('report')} Firewall is passive") + logging.debug(f"{get_emoji('report')} {hostname}: Firewall is passive") return True, None elif version_comparison == "older": - logging.debug(f"{get_emoji('report')} Firewall is on an older version") + logging.debug( + f"{get_emoji('report')} {hostname}: Firewall is on an older version" + ) # Suspend HA state of active if the passive is on a later release if local_state == "active" and not dry_run: - logging.debug(f"{get_emoji('report')} Suspending HA state of active") - suspend_ha_active(firewall) + logging.debug( + f"{get_emoji('report')} {hostname}: Suspending HA state of active" + ) + suspend_ha_active( + firewall, + hostname, + ) return True, None elif version_comparison == "newer": - logging.debug(f"{get_emoji('report')} Firewall is on a newer version") + logging.debug( + f"{get_emoji('report')} {hostname}: Firewall is on a newer version" + ) # Suspend HA state of passive if the active is on a later release if local_state == "passive" and not dry_run: - logging.debug(f"{get_emoji('report')} Suspending HA state of passive") - suspend_ha_passive(firewall) + logging.debug( + f"{get_emoji('report')} {hostname}: Suspending HA state of passive" + ) + suspend_ha_passive( + firewall, + hostname, + ) return True, None return False, None def perform_ha_sync_check( - firewall: Firewall, ha_details: dict, strict_sync_check: bool = True + hostname: str, + ha_details: dict, + strict_sync_check: bool = True, ) -> bool: """ Verifies the synchronization status of the High Availability (HA) peer firewall. @@ -571,8 +607,8 @@ def perform_ha_sync_check( Parameters ---------- - firewall : Firewall - The firewall instance whose HA synchronization status is to be checked. + hostname : str + The hostname of the firewall. This is used for logging and reporting purposes. ha_details : dict A dictionary containing the HA status details of the firewall. strict_sync_check : bool, optional @@ -602,20 +638,22 @@ def perform_ha_sync_check( - The function logs detailed synchronization status, aiding in debugging and operational monitoring. - It is essential in maintaining HA integrity during operations like upgrades or configuration changes. """ - logging.info(f"{get_emoji('start')} Checking if HA peer is in sync...") + logging.info(f"{get_emoji('start')} {hostname}: Checking if HA peer is in sync...") if ha_details["result"]["group"]["running-sync"] == "synchronized": - logging.info(f"{get_emoji('success')} HA peer sync test has been completed.") + logging.info( + f"{get_emoji('success')} {hostname}: HA peer sync test has been completed." + ) return True else: if strict_sync_check: logging.error( - f"{get_emoji('error')} HA peer state is not in sync, please try again." + f"{get_emoji('error')} {hostname}: HA peer state is not in sync, please try again." ) - logging.error(f"{get_emoji('stop')} Halting script.") + logging.error(f"{get_emoji('stop')} {hostname}: Halting script.") sys.exit(1) else: logging.warning( - f"{get_emoji('warning')} HA peer state is not in sync. This will be noted, but the script will continue." + f"{get_emoji('warning')} {hostname}: HA peer state is not in sync. This will be noted, but the script will continue." ) return False @@ -666,7 +704,7 @@ def perform_readiness_checks( """ logging.debug( - f"{get_emoji('start')} Performing readiness checks of target firewall..." + f"{get_emoji('start')} {hostname}: Performing readiness checks of target firewall..." ) readiness_check = run_assurance( @@ -690,9 +728,11 @@ def perform_readiness_checks( # Check if a readiness check was successfully created if isinstance(readiness_check, ReadinessCheckReport): # Do something with the readiness check report, e.g., log it, save it, etc. - logging.info(f"{get_emoji('success')} Readiness Checks completed") + logging.info(f"{get_emoji('success')} {hostname}: Readiness Checks completed") readiness_check_report_json = readiness_check.model_dump_json(indent=4) - logging.debug(readiness_check_report_json) + logging.debug( + f"{get_emoji('save')} {hostname}: Readiness Check Report: {readiness_check_report_json}" + ) ensure_directory_exists(file_path) @@ -700,14 +740,17 @@ def perform_readiness_checks( file.write(readiness_check_report_json) logging.debug( - f"{get_emoji('save')} Readiness checks completed for {hostname}, saved to {file_path}" + f"{get_emoji('save')} {hostname}: Readiness checks completed for {hostname}, saved to {file_path}" ) else: - logging.error(f"{get_emoji('error')} Failed to create readiness check") + logging.error( + f"{get_emoji('error')} {hostname}: Failed to create readiness check" + ) def perform_reboot( firewall: Firewall, + hostname: str, target_version: str, ha_details: Optional[dict] = None, ) -> None: @@ -723,6 +766,8 @@ def perform_reboot( ---------- firewall : Firewall The firewall to be rebooted. + hostname : str + The hostname of the firewall. This is used for logging and reporting purposes. target_version : str The target PAN-OS version to be verified post-reboot. ha_details : Optional[dict], optional @@ -744,7 +789,7 @@ def perform_reboot( ------- Rebooting and verifying a firewall's version: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> perform_reboot(firewall, '10.1.0') + >>> perform_reboot(firewall, 'firewall1, '10.1.0') # The firewall reboots and the script monitors until it successfully reaches version 10.1.0. """ @@ -753,17 +798,21 @@ def perform_reboot( # Check if HA details are available if ha_details: - logging.info(f"{get_emoji('start')} Rebooting the passive HA firewall...") + logging.info( + f"{get_emoji('start')} {hostname}: Rebooting the passive HA firewall..." + ) # Reboot standalone firewall else: - logging.info(f"{get_emoji('start')} Rebooting the standalone firewall...") + logging.info( + f"{get_emoji('start')} {hostname}: Rebooting the standalone firewall..." + ) reboot_job = firewall.op( "", cmd_xml=False ) reboot_job_result = flatten_xml_to_dict(reboot_job) - logging.info(f"{get_emoji('report')} {reboot_job_result['result']}") + logging.info(f"{get_emoji('report')} {hostname}: {reboot_job_result['result']}") # Wait for the firewall reboot process to initiate before checking status time.sleep(60) @@ -775,10 +824,15 @@ def perform_reboot( # Check if HA details are available if ha_details: try: - deploy_info, current_ha_details = get_ha_status(firewall) - logging.debug(f"{get_emoji('report')} deploy_info: {deploy_info}") + deploy_info, current_ha_details = get_ha_status( + firewall, + hostname, + ) logging.debug( - f"{get_emoji('report')} current_ha_details: {current_ha_details}" + f"{get_emoji('report')} {hostname}: deploy_info: {deploy_info}" + ) + logging.debug( + f"{get_emoji('report')} {hostname}: current_ha_details: {current_ha_details}" ) if current_ha_details and deploy_info in ["active", "passive"]: @@ -787,25 +841,27 @@ def perform_reboot( == "synchronized" ): logging.info( - f"{get_emoji('success')} HA passive firewall rebooted and synchronized with its peer in {int(time.time() - reboot_start_time)} seconds" + f"{get_emoji('success')} {hostname}: HA passive firewall rebooted and synchronized with its peer in {int(time.time() - reboot_start_time)} seconds" ) rebooted = True else: reboot_and_sync_check += 1 if reboot_and_sync_check >= 5: logging.warning( - f"{get_emoji('warning')} HA passive firewall rebooted but did not complete a configuration sync with the active after 5 attempts." + f"{get_emoji('warning')} {hostname}: HA passive firewall rebooted but did not complete a configuration sync with the active after 5 attempts." ) # Set rebooted to True to exit the loop rebooted = True break else: logging.info( - f"{get_emoji('working')} HA passive firewall rebooted but not yet synchronized with its peer. Will try again in 60 seconds." + f"{get_emoji('working')} {hostname}: HA passive firewall rebooted but not yet synchronized with its peer. Will try again in 60 seconds." ) time.sleep(60) except (PanXapiError, PanConnectionTimeout, PanURLError): - logging.info(f"{get_emoji('working')} Firewall is rebooting...") + logging.info( + f"{get_emoji('working')} {hostname}: Firewall is rebooting..." + ) time.sleep(60) # Reboot standalone firewall @@ -813,27 +869,29 @@ def perform_reboot( try: firewall.refresh_system_info() logging.info( - f"{get_emoji('report')} Firewall version: {firewall.version}" + f"{get_emoji('report')} {hostname}: Firewall version: {firewall.version}" ) if firewall.version == target_version: logging.info( - f"{get_emoji('success')} Firewall rebooted in {int(time.time() - reboot_start_time)} seconds" + f"{get_emoji('success')} {hostname}: Firewall rebooted in {int(time.time() - reboot_start_time)} seconds" ) rebooted = True else: logging.error( - f"{get_emoji('stop')} Firewall rebooted but running the target version. Please try again." + f"{get_emoji('stop')} {hostname}: Firewall rebooted but running the target version. Please try again." ) sys.exit(1) except (PanXapiError, PanConnectionTimeout, PanURLError): - logging.info(f"{get_emoji('working')} Firewall is rebooting...") + logging.info( + f"{get_emoji('working')} {hostname}: Firewall is rebooting..." + ) time.sleep(60) # Check if 30 minutes have passed - if time.time() - reboot_start_time > 1800: # 30 minutes in seconds + if time.time() - reboot_start_time > 1800: logging.error( - f"{get_emoji('error')} Firewall did not become available and/or establish a Connected sync state with its HA peer after 30 minutes. Please check the firewall status manually." + f"{get_emoji('error')} {hostname}: Firewall did not become available and/or establish a Connected sync state with its HA peer after 30 minutes. Please check the firewall status manually." ) break @@ -875,7 +933,7 @@ def perform_snapshot( """ logging.info( - f"{get_emoji('start')} Performing snapshot of network state information..." + f"{get_emoji('start')} {hostname}: Performing snapshot of network state information..." ) # take snapshots @@ -897,9 +955,13 @@ def perform_snapshot( # Check if a readiness check was successfully created if isinstance(network_snapshot, SnapshotReport): - logging.info(f"{get_emoji('success')} Network snapshot created successfully") + logging.info( + f"{get_emoji('success')} {hostname}: Network snapshot created successfully" + ) network_snapshot_json = network_snapshot.model_dump_json(indent=4) - logging.debug(network_snapshot_json) + logging.debug( + f"{get_emoji('success')} {hostname}: Network snapshot JSON {network_snapshot_json}" + ) ensure_directory_exists(file_path) @@ -907,10 +969,10 @@ def perform_snapshot( file.write(network_snapshot_json) logging.debug( - f"{get_emoji('save')} Network state snapshot collected from {hostname}, saved to {file_path}" + f"{get_emoji('save')} {hostname}: Network state snapshot collected and saved to {file_path}" ) else: - logging.error(f"{get_emoji('error')} Failed to create snapshot") + logging.error(f"{get_emoji('error')} {hostname}: Failed to create snapshot") def perform_upgrade( @@ -965,47 +1027,49 @@ def perform_upgrade( """ logging.info( - f"{get_emoji('start')} Performing upgrade on {hostname} to version {target_version}..." + f"{get_emoji('start')} {hostname}: Performing upgrade to version {target_version}..." ) attempt = 0 while attempt < max_retries: try: logging.info( - f"{get_emoji('start')} Attempting upgrade {hostname} to version {target_version} (Attempt {attempt + 1} of {max_retries})..." + f"{get_emoji('start')} {hostname}: Attempting upgrade to version {target_version} (Attempt {attempt + 1} of {max_retries})..." ) install_job = firewall.software.install(target_version, sync=True) if install_job["success"]: logging.info( - f"{get_emoji('success')} {hostname} upgrade completed successfully" + f"{get_emoji('success')} {hostname}: Upgrade completed successfully" + ) + logging.debug( + f"{get_emoji('report')} {hostname}: Install Job {install_job}" ) - logging.debug(f"{get_emoji('report')} {install_job}") break # Exit loop on successful upgrade else: - logging.error(f"{get_emoji('error')} {hostname} upgrade job failed.") + logging.error(f"{get_emoji('error')} {hostname}: Upgrade job failed.") attempt += 1 if attempt < max_retries: logging.info( - f"{get_emoji('warning')} Retrying in {retry_interval} seconds..." + f"{get_emoji('warning')} {hostname}: Retrying in {retry_interval} seconds..." ) time.sleep(retry_interval) except PanDeviceError as upgrade_error: logging.error( - f"{get_emoji('error')} {hostname} upgrade error: {upgrade_error}" + f"{get_emoji('error')} {hostname}: Upgrade error: {upgrade_error}" ) error_message = str(upgrade_error) if "software manager is currently in use" in error_message: attempt += 1 if attempt < max_retries: logging.info( - f"{get_emoji('warning')} Software manager is busy. Retrying in {retry_interval} seconds..." + f"{get_emoji('warning')} {hostname}: Software manager is busy. Retrying in {retry_interval} seconds..." ) time.sleep(retry_interval) else: logging.error( - f"{get_emoji('stop')} Critical error during upgrade. Halting script." + f"{get_emoji('stop')} {hostname}: Critical error during upgrade. Halting script." ) sys.exit(1) @@ -1072,14 +1136,14 @@ def run_assurance( for action in actions: if action not in AssuranceOptions.READINESS_CHECKS.keys(): logging.error( - f"{get_emoji('error')} Invalid action for readiness check: {action}" + f"{get_emoji('error')} {hostname}: Invalid action for readiness check: {action}" ) sys.exit(1) try: logging.info( - f"{get_emoji('start')} Performing readiness checks to determine if firewall is ready for upgrade..." + f"{get_emoji('start')} {hostname}: Performing readiness checks to determine if firewall is ready for upgrade..." ) result = checks_firewall.run_readiness_checks(actions) @@ -1087,12 +1151,14 @@ def run_assurance( test_name, test_info, ) in AssuranceOptions.READINESS_CHECKS.items(): - check_readiness_and_log(result, test_name, test_info) + check_readiness_and_log(result, hostname, test_name, test_info) return ReadinessCheckReport(**result) except Exception as e: - logging.error(f"{get_emoji('error')} Error running readiness checks: {e}") + logging.error( + f"{get_emoji('error')} {hostname}: Error running readiness checks: {e}" + ) return None @@ -1101,15 +1167,17 @@ def run_assurance( for action in actions: if action not in AssuranceOptions.STATE_SNAPSHOTS: logging.error( - f"{get_emoji('error')} Invalid action for state snapshot: {action}" + f"{get_emoji('error')} {hostname}: Invalid action for state snapshot: {action}" ) return # take snapshots try: - logging.debug("Running snapshots...") + logging.debug(f"{get_emoji('start')} {hostname}: Performing snapshots...") results = checks_firewall.run_snapshots(snapshots_config=actions) - logging.debug(results) + logging.debug( + f"{get_emoji('report')} {hostname}: Snapshot results {results}" + ) if results: # Pass the results to the SnapshotReport model @@ -1118,21 +1186,27 @@ def run_assurance( return None except Exception as e: - logging.error(f"{get_emoji('error')} Error running snapshots: %s", e) + logging.error( + f"{get_emoji('error')} {hostname}: Error running snapshots: %s", e + ) return elif operation_type == "report": for action in actions: if action not in AssuranceOptions.REPORTS: logging.error( - f"{get_emoji('error')} Invalid action for report: {action}" + f"{get_emoji('error')} {hostname}: Invalid action for report: {action}" ) return - logging.info(f"{get_emoji('report')} Generating report: {action}") + logging.info( + f"{get_emoji('report')} {hostname}: Generating report: {action}" + ) # result = getattr(Report(firewall), action)(**config) else: - logging.error(f"{get_emoji('error')} Invalid operation type: {operation_type}") + logging.error( + f"{get_emoji('error')} {hostname}: Invalid operation type: {operation_type}" + ) return return results @@ -1140,6 +1214,7 @@ def run_assurance( def software_download( firewall: Firewall, + hostname: str, target_version: str, ha_details: dict, ) -> bool: @@ -1155,6 +1230,8 @@ def software_download( ---------- firewall : Firewall The instance of the Firewall where the software is to be downloaded. + hostname : str + The hostname of the firewall, used for logging and reporting purposes. target_version : str The specific PAN-OS version targeted for download. ha_details : dict @@ -1174,7 +1251,7 @@ def software_download( -------- Downloading a specific PAN-OS version: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> success = software_download(firewall, '10.1.0', ha_details={}) + >>> success = software_download(firewall, 'firewall1', '10.1.0', ha_details={}) >>> print(success) # Outputs: True if successful, False otherwise Notes @@ -1186,7 +1263,7 @@ def software_download( if firewall.software.versions[target_version]["downloaded"]: logging.info( - f"{get_emoji('success')} PAN-OS version {target_version} already on firewall." + f"{get_emoji('success')} {hostname}: PAN-OS version {target_version} already on firewall." ) return True @@ -1195,18 +1272,20 @@ def software_download( or firewall.software.versions[target_version]["downloaded"] != "downloading" ): logging.info( - f"{get_emoji('search')} PAN-OS version {target_version} is not on the firewall" + f"{get_emoji('search')} {hostname}: PAN-OS version {target_version} is not on the firewall" ) start_time = time.time() try: logging.info( - f"{get_emoji('start')} PAN-OS version {target_version} is beginning download" + f"{get_emoji('start')} {hostname}: PAN-OS version {target_version} is beginning download" ) firewall.software.download(target_version) except PanDeviceXapiError as download_error: - logging.error(f"{get_emoji('error')} {download_error}") + logging.error( + f"{get_emoji('error')} {hostname}: Download Error {download_error}" + ) sys.exit(1) @@ -1217,7 +1296,7 @@ def software_download( if dl_status is True: logging.info( - f"{get_emoji('success')} {target_version} downloaded in {elapsed_time} seconds", + f"{get_emoji('success')} {hostname}: {target_version} downloaded in {elapsed_time} seconds", ) return True elif dl_status in (False, "downloading"): @@ -1229,26 +1308,31 @@ def software_download( ) if ha_details: logging.info( - f"{get_emoji('working')} {status_msg} - HA will sync image - Elapsed time: {elapsed_time} seconds" + f"{get_emoji('working')} {hostname}: {status_msg} - HA will sync image - Elapsed time: {elapsed_time} seconds" ) else: - logging.info(f"{status_msg} - Elapsed time: {elapsed_time} seconds") + logging.info( + f"{get_emoji('working')} {hostname}: {status_msg} - Elapsed time: {elapsed_time} seconds" + ) else: logging.error( - f"{get_emoji('error')} Download failed after {elapsed_time} seconds" + f"{get_emoji('error')} {hostname}: Download failed after {elapsed_time} seconds" ) return False time.sleep(30) else: - logging.error(f"{get_emoji('error')} Error downloading {target_version}.") + logging.error( + f"{get_emoji('error')} {hostname}: Error downloading {target_version}." + ) sys.exit(1) def software_update_check( firewall: Firewall, + hostname: str, version: str, ha_details: dict, ) -> bool: @@ -1266,6 +1350,8 @@ def software_update_check( ---------- firewall : Firewall The instance of the Firewall to be checked for software update availability. + hostname : str + The hostname of the firewall, used for logging and reporting purposes. version : str The target PAN-OS version for the upgrade. ha_details : dict @@ -1297,11 +1383,19 @@ def software_update_check( major, minor, maintenance = version.split(".") # Make sure we know about the system details - if we have connected via Panorama, this can be null without this. - logging.debug("Refreshing running system information") + logging.debug( + f"{get_emoji('working')} {hostname}: Refreshing running system information" + ) firewall.refresh_system_info() # check to see if the specified version is older than the current version - determine_upgrade(firewall, major, minor, maintenance) + determine_upgrade( + firewall, + hostname, + major, + minor, + maintenance, + ) # retrieve available versions of PAN-OS firewall.software.check() @@ -1310,29 +1404,32 @@ def software_update_check( # check to see if specified version is available for upgrade if version in available_versions: logging.info( - f"{get_emoji('success')} PAN-OS version {version} is available for download" + f"{get_emoji('success')} {hostname}: PAN-OS version {version} is available for download" ) # validate the specified version's base image is already downloaded if available_versions[f"{major}.{minor}.0"]["downloaded"]: logging.info( - f"{get_emoji('success')} Base image for {version} is already downloaded" + f"{get_emoji('success')} {hostname}: Base image for {version} is already downloaded" ) return True else: logging.error( - f"{get_emoji('error')} Base image for {version} is not downloaded" + f"{get_emoji('error')} {hostname}: Base image for {version} is not downloaded" ) return False else: logging.error( - f"{get_emoji('error')} PAN-OS version {version} is not available for download" + f"{get_emoji('error')} {hostname}: PAN-OS version {version} is not available for download" ) return False -def suspend_ha_active(firewall: Firewall) -> bool: +def suspend_ha_active( + firewall: Firewall, + hostname: str, +) -> bool: """ Suspends the High-Availability (HA) state of the active firewall in an HA pair. @@ -1346,6 +1443,8 @@ def suspend_ha_active(firewall: Firewall) -> bool: ---------- firewall : Firewall An instance of the Firewall class representing the active firewall in an HA pair. + hostname: str + The hostname of the firewall, used for logging and reporting purposes. Returns ------- @@ -1366,7 +1465,7 @@ def suspend_ha_active(firewall: Firewall) -> bool: ------- Suspending the HA state of an active firewall in an HA pair: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> suspend_ha_active(firewall) + >>> suspend_ha_active(firewall, 'firewall1') True # Indicates successful suspension of the HA state. """ @@ -1376,21 +1475,26 @@ def suspend_ha_active(firewall: Firewall) -> bool: cmd_xml=False, ) if "success" in suspension_response.text: - logging.info(f"{get_emoji('success')} Active firewall HA state suspended.") + logging.info( + f"{get_emoji('success')} {hostname}: Active firewall HA state suspended." + ) return True else: logging.error( - f"{get_emoji('error')} Failed to suspend active firewall HA state." + f"{get_emoji('error')} {hostname}: Failed to suspend active firewall HA state." ) return False except Exception as e: logging.error( - f"{get_emoji('error')} Error suspending active firewall HA state: {e}" + f"{get_emoji('error')} {hostname}: Error suspending active firewall HA state: {e}" ) return False -def suspend_ha_passive(firewall: Firewall) -> bool: +def suspend_ha_passive( + firewall: Firewall, + hostname: str, +) -> bool: """ Suspends the High-Availability (HA) state of the passive firewall in an HA pair. @@ -1404,6 +1508,8 @@ def suspend_ha_passive(firewall: Firewall) -> bool: ---------- firewall : Firewall An instance of the Firewall class representing the passive firewall in an HA pair. + hostname: str + The hostname of the firewall, used for logging and reporting purposes. Returns ------- @@ -1424,7 +1530,7 @@ def suspend_ha_passive(firewall: Firewall) -> bool: ------- Suspending the HA state of a passive firewall in an HA pair: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> suspend_ha_passive(firewall) + >>> suspend_ha_passive(firewall, 'firewall1') True # Indicates successful suspension of the HA state. """ @@ -1434,21 +1540,23 @@ def suspend_ha_passive(firewall: Firewall) -> bool: cmd_xml=False, ) if "success" in suspension_response.text: - logging.info(f"{get_emoji('success')} Passive firewall HA state suspended.") + logging.info( + f"{get_emoji('success')} {hostname}: Passive firewall HA state suspended." + ) return True else: logging.error( - f"{get_emoji('error')} Failed to suspend passive firewall HA state." + f"{get_emoji('error')} {hostname}: Failed to suspend passive firewall HA state." ) return False except Exception as e: logging.error( - f"{get_emoji('error')} Error suspending passive firewall HA state: {e}" + f"{get_emoji('error')} {hostname}: Error suspending passive firewall HA state: {e}" ) return False -def upgrade_single_firewall( +def upgrade_firewall( firewall: Firewall, target_version: str, dry_run: bool, @@ -1495,86 +1603,104 @@ def upgrade_single_firewall( ------- Upgrading a firewall to a specific PAN-OS version: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> upgrade_single_firewall(firewall, '10.1.0', dry_run=False) + >>> upgrade_firewall(firewall, '10.1.0', dry_run=False) # Initiates the upgrade process of the firewall to PAN-OS version 10.1.0. """ # Refresh system information to ensure we have the latest data logging.debug(f"{get_emoji('start')} Refreshing system information...") firewall_details = SystemSettings.refreshall(firewall)[0] + hostname = firewall_details.hostname logging.info( - f"{get_emoji('report')} {firewall.serial} {firewall_details.hostname} {firewall_details.ip_address}" + f"{get_emoji('report')} {hostname}: {firewall.serial} {firewall_details.ip_address}" ) # Determine if the firewall is standalone, HA, or in a cluster logging.debug( - f"{get_emoji('start')} Performing test to see if firewall is standalone, HA, or in a cluster..." + f"{get_emoji('start')} {hostname}: Performing test to see if firewall is standalone, HA, or in a cluster..." + ) + deploy_info, ha_details = get_ha_status( + firewall, + hostname, ) - deploy_info, ha_details = get_ha_status(firewall) - logging.info(f"{get_emoji('report')} Firewall HA mode: {deploy_info}") - logging.debug(f"{get_emoji('report')} Firewall HA details: {ha_details}") + logging.info(f"{get_emoji('report')} {hostname}: HA mode: {deploy_info}") + logging.debug(f"{get_emoji('report')} {hostname}: HA details: {ha_details}") # If firewall is part of HA pair, determine if it's active or passive if ha_details: proceed_with_upgrade, peer_firewall = handle_ha_logic( - firewall, target_version, dry_run + firewall, + hostname, + dry_run, ) if not proceed_with_upgrade: if peer_firewall: logging.info( - f"{get_emoji('start')} Switching control to the peer firewall for upgrade." + f"{get_emoji('start')} {hostname}: Switching control to the peer firewall for upgrade." ) - upgrade_single_firewall(peer_firewall, target_version, dry_run) + upgrade_firewall(peer_firewall, target_version, dry_run) else: return # Exit the function without proceeding to upgrade # Check to see if the firewall is ready for an upgrade logging.debug( - f"{get_emoji('start')} Performing test to validate firewall's readiness..." + f"{get_emoji('start')} {hostname}: Performing tests to validate firewall's readiness..." + ) + update_available = software_update_check( + firewall, + hostname, + target_version, + ha_details, ) - update_available = software_update_check(firewall, target_version, ha_details) - logging.debug(f"{get_emoji('report')} Firewall readiness check complete") + logging.debug(f"{get_emoji('report')} {hostname}: Readiness check complete") # gracefully exit if the firewall is not ready for an upgrade to target version if not update_available: logging.error( - f"{get_emoji('error')} Firewall is not ready for upgrade to {target_version}.", + f"{get_emoji('error')} {hostname}: Not ready for upgrade to {target_version}.", ) sys.exit(1) # Download the target PAN-OS version logging.info( - f"{get_emoji('start')} Performing test to see if {target_version} is already downloaded..." + f"{get_emoji('start')} {hostname}: Performing test to see if {target_version} is already downloaded..." + ) + image_downloaded = software_download( + firewall, + hostname, + target_version, + ha_details, ) - image_downloaded = software_download(firewall, target_version, ha_details) if deploy_info == "active" or deploy_info == "passive": logging.info( - f"{get_emoji('success')} {target_version} has been downloaded and sync'd to HA peer." + f"{get_emoji('success')} {hostname}: {target_version} has been downloaded and sync'd to HA peer." ) else: logging.info( - f"{get_emoji('success')} PAN-OS version {target_version} has been downloaded." + f"{get_emoji('success')} {hostname}: PAN-OS version {target_version} has been downloaded." ) # Begin snapshots of the network state if not image_downloaded: - logging.error(f"{get_emoji('error')} Image not downloaded, exiting...") + logging.error( + f"{get_emoji('error')} {hostname}: Image not downloaded, exiting..." + ) sys.exit(1) # Perform the pre-upgrade snapshot perform_snapshot( firewall, - firewall_details.hostname, - f'assurance/snapshots/{firewall_details.hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.json', + hostname, + f'assurance/snapshots/{hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.json', ) # Perform Readiness Checks perform_readiness_checks( firewall, - firewall_details.hostname, - f'assurance/readiness_checks/{firewall_details.hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.json', + hostname, + f'assurance/readiness_checks/{hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.json', ) # Determine strictness of HA sync check @@ -1582,33 +1708,36 @@ def upgrade_single_firewall( is_firewall_to_revisit = firewall in firewalls_to_revisit perform_ha_sync_check( - firewall, + hostname, ha_details, strict_sync_check=not is_firewall_to_revisit, ) # Back up configuration to local filesystem logging.info( - f"{get_emoji('start')} Performing backup of {firewall_details.hostname}'s configuration to local filesystem..." + f"{get_emoji('start')} {hostname}: Performing backup of configuration to local filesystem..." ) backup_config = backup_configuration( firewall, - f'assurance/configurations/{firewall_details.hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.xml', + hostname, + f'assurance/configurations/{hostname}/pre/{time.strftime("%Y-%m-%d_%H-%M-%S")}.xml', ) - logging.debug(f"{get_emoji('report')} {backup_config}") + logging.debug(f"{get_emoji('report')} {hostname}: {backup_config}") # Exit execution is dry_run is True if dry_run is True: - logging.info(f"{get_emoji('success')} Dry run complete, exiting...") - logging.info(f"{get_emoji('stop')} Halting script.") + logging.info(f"{get_emoji('success')} {hostname}: Dry run complete, exiting...") + logging.info(f"{get_emoji('stop')} {hostname}: Halting script.") sys.exit(0) else: - logging.info(f"{get_emoji('start')} Not a dry run, continue with upgrade...") + logging.info( + f"{get_emoji('start')} {hostname}: Not a dry run, continue with upgrade..." + ) # Perform the upgrade perform_upgrade( firewall=firewall, - hostname=firewall_details.hostname, + hostname=hostname, target_version=target_version, ha_details=ha_details, ) @@ -1616,6 +1745,7 @@ def upgrade_single_firewall( # Perform the reboot perform_reboot( firewall=firewall, + hostname=hostname, target_version=target_version, ha_details=ha_details, ) @@ -1626,6 +1756,7 @@ def upgrade_single_firewall( # ---------------------------------------------------------------------------- def check_readiness_and_log( result: dict, + hostname: str, test_name: str, test_info: dict, ) -> None: @@ -1642,6 +1773,8 @@ def check_readiness_and_log( result : dict The result dictionary containing keys for each readiness test. Each key maps to another dictionary with 'state' (boolean indicating pass or fail) and 'reason' (string describing the result). + hostname : str + The hostname of the firewall, used primarily for logging purposes. test_name : str The name of the readiness test to be evaluated, which should match a key in the 'result' dictionary. test_info : dict @@ -1675,21 +1808,23 @@ def check_readiness_and_log( if test_result["state"]: logging.info( - f"{get_emoji('success')} Passed Readiness Check: {test_info['description']}" + f"{get_emoji('success')} {hostname}: Passed Readiness Check: {test_info['description']}" ) else: if test_info["log_level"] == "error": - logging.error(f"{get_emoji('error')} {log_message}") + logging.error(f"{get_emoji('error')} {hostname}: {log_message}") if test_info["exit_on_failure"]: - logging.error(f"{get_emoji('stop')} Halting script.") + logging.error(f"{get_emoji('stop')} {hostname}: Halting script.") sys.exit(1) elif test_info["log_level"] == "warning": logging.debug( - f"{get_emoji('report')} Skipped Readiness Check: {test_info['description']}" + f"{get_emoji('report')} {hostname}: Skipped Readiness Check: {test_info['description']}" ) else: - logging.debug(log_message) + logging.debug( + f"{get_emoji('report')} {hostname}: Log Message {log_message}" + ) def compare_versions(version1: str, version2: str) -> str: @@ -1888,14 +2023,14 @@ def connect_to_host( except PanConnectionTimeout: logging.error( - f"Connection to the {hostname} appliance timed out. Please check the DNS hostname or IP address and network connectivity." + f"{get_emoji('error')} {hostname}: Connection to the appliance timed out. Please check the DNS hostname or IP address and network connectivity." ) sys.exit(1) except Exception as e: logging.error( - f"An error occurred while connecting to the {hostname} appliance: {e}" + f"{get_emoji('error')} {hostname}: An error occurred while connecting to the appliance: {e}" ) sys.exit(1) @@ -2541,7 +2676,7 @@ def main( configure_logging(log_level) # Create our connection to the firewall - logging.debug(f"{get_emoji('start')} Connecting to PAN-OS device...") + logging.debug(f"{get_emoji('start')} {hostname}: Connecting to PAN-OS device...") device = connect_to_host( hostname=hostname, api_username=username, @@ -2550,32 +2685,32 @@ def main( firewalls_to_upgrade = [] if type(device) is Firewall: - logging.info(f"{get_emoji('success')} Connection to firewall established") + logging.info( + f"{get_emoji('success')} {hostname}: Connection to firewall established" + ) firewalls_to_upgrade.append(device) elif type(device) is Panorama: if not filter: logging.error( - f"{get_emoji('error')} Specified device is Panorama, but no filter string was provided." + f"{get_emoji('error')} {hostname}: Specified device is Panorama, but no filter string was provided." ) sys.exit(1) logging.info( - f"{get_emoji('success')} Connection to Panorama established. Firewall connections will be proxied!" + f"{get_emoji('success')} {hostname}: Connection to Panorama established. Firewall connections will be proxied!" ) firewalls_to_upgrade = get_firewalls_from_panorama( device, **filter_string_to_dict(filter) ) logging.debug( - f"{get_emoji('report')} Firewalls to upgrade: {firewalls_to_upgrade}" + f"{get_emoji('report')} {hostname}: Firewalls to upgrade: {firewalls_to_upgrade}" ) # Using ThreadPoolExecutor to manage threads with ThreadPoolExecutor(max_workers=2) as executor: # Store future objects along with firewalls for reference future_to_firewall = { - executor.submit( - upgrade_single_firewall, fw, target_version, dry_run - ): fw + executor.submit(upgrade_firewall, fw, target_version, dry_run): fw for fw in firewalls_to_upgrade } @@ -2586,21 +2721,19 @@ def main( future.result() except Exception as exc: logging.error( - f"{get_emoji('error')} Firewall {firewall.hostname} generated an exception: {exc}" + f"{get_emoji('error')} {hostname}: Firewall {firewall.hostname} generated an exception: {exc}" ) # Revisit the firewalls that were skipped in the initial pass if firewalls_to_revisit: logging.info( - f"{get_emoji('info')} Revisiting firewalls that were active in an HA pair and had the same version as their peers." + f"{get_emoji('start')} {hostname}: Revisiting firewalls that were active in an HA pair and had the same version as their peers." ) # Using ThreadPoolExecutor to manage threads for revisiting firewalls with ThreadPoolExecutor(max_workers=2) as executor: future_to_firewall = { - executor.submit( - upgrade_single_firewall, fw, target_version, dry_run - ): fw + executor.submit(upgrade_firewall, fw, target_version, dry_run): fw for fw in firewalls_to_revisit } @@ -2609,11 +2742,11 @@ def main( try: future.result() logging.info( - f"{get_emoji('success')} Completed revisiting firewall: {firewall.hostname}" + f"{get_emoji('success')} {hostname}: Completed revisiting firewall: {firewall.hostname}" ) except Exception as exc: logging.error( - f"{get_emoji('error')} Exception while revisiting firewall {firewall.hostname}: {exc}" + f"{get_emoji('error')} {hostname}: Exception while revisiting firewall {firewall.hostname}: {exc}" ) with firewalls_to_revisit_lock: From 7f066f50e51d52a1adc0d088b2c639a656823cef Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Thu, 25 Jan 2024 09:26:43 -0600 Subject: [PATCH 07/13] updating docstrings to reflect the passing of hostname into functions --- pan_os_upgrade/upgrade.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index 2e952fe..8a9a038 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -630,7 +630,7 @@ def perform_ha_sync_check( Checking HA synchronization status: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') >>> ha_details = {'result': {'group': {'running-sync': 'synchronized'}}} - >>> perform_ha_sync_check(firewall, ha_details, strict_sync_check=True) + >>> perform_ha_sync_check('firewall1', ha_details, strict_sync_check=True) True # If the HA peer is synchronized Notes @@ -1022,7 +1022,7 @@ def perform_upgrade( ------- Upgrading a firewall to a specific version with retry logic: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> perform_upgrade(firewall, '192.168.1.1', '10.2.0', max_retries=2, retry_interval=30) + >>> perform_upgrade(firewall, 'firewall1', '10.2.0', max_retries=2, retry_interval=30) # The firewall is upgraded to version 10.2.0, with a maximum of 2 retries if needed. """ @@ -1371,7 +1371,7 @@ def software_update_check( -------- Checking the availability of a specific PAN-OS version for upgrade: >>> firewall = Firewall(hostname='192.168.1.1', api_username='admin', api_password='password') - >>> software_update_check(firewall, '10.1.0', ha_details={}) + >>> software_update_check(firewall, 'firewall1', '10.1.0', ha_details={}) True # Indicates that version 10.1.0 is available and ready for upgrade. Notes @@ -1798,7 +1798,7 @@ def check_readiness_and_log( >>> result = {'test_connectivity': {'state': True, 'reason': 'Successful connection'}} >>> test_name = 'test_connectivity' >>> test_info = {'description': 'Test Connectivity', 'log_level': 'info', 'exit_on_failure': False} - >>> check_readiness_and_log(result, test_name, test_info) + >>> check_readiness_and_log(result, 'firewall1', test_name, test_info) # Logs "✅ Passed Readiness Check: Test Connectivity - Successful connection" """ test_result = result.get( @@ -1827,7 +1827,10 @@ def check_readiness_and_log( ) -def compare_versions(version1: str, version2: str) -> str: +def compare_versions( + version1: str, + version2: str, +) -> str: """ Compares two PAN-OS version strings and determines their relative ordering. @@ -1876,7 +1879,10 @@ def compare_versions(version1: str, version2: str) -> str: return "equal" -def configure_logging(level: str, encoding: str = "utf-8") -> None: +def configure_logging( + level: str, + encoding: str = "utf-8", +) -> None: """ Configures the logging system for the application, specifying log level and file encoding. @@ -2245,7 +2251,10 @@ def get_emoji(action: str) -> str: return emoji_map.get(action, "") -def get_firewalls_from_panorama(panorama: Panorama, **filters) -> list[Firewall]: +def get_firewalls_from_panorama( + panorama: Panorama, + **filters, +) -> list[Firewall]: """ Retrieves a list of firewalls managed by a specified Panorama, optionally filtered by custom criteria. @@ -2292,7 +2301,10 @@ def get_firewalls_from_panorama(panorama: Panorama, **filters) -> list[Firewall] return firewalls -def get_managed_devices(panorama: Panorama, **filters) -> list[ManagedDevice]: +def get_managed_devices( + panorama: Panorama, + **filters, +) -> list[ManagedDevice]: """ Retrieves a list of devices managed by a specified Panorama, optionally filtered by custom criteria. @@ -2710,7 +2722,12 @@ def main( with ThreadPoolExecutor(max_workers=2) as executor: # Store future objects along with firewalls for reference future_to_firewall = { - executor.submit(upgrade_firewall, fw, target_version, dry_run): fw + executor.submit( + upgrade_firewall, + fw, + target_version, + dry_run, + ): fw for fw in firewalls_to_upgrade } From 309f1952d4bcfb15bd34202334a3bd6c86342c1f Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Thu, 25 Jan 2024 09:40:10 -0600 Subject: [PATCH 08/13] update emoji to appease whitespace aesthetics --- pan_os_upgrade/upgrade.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index 8a9a038..8cb33d4 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -2239,9 +2239,9 @@ def get_emoji(action: str) -> str: """ emoji_map = { "success": "✅", - "warning": "⚠️", + "warning": "🟧", "error": "❌", - "working": "⚙️", + "working": "🔧", "report": "📝", "search": "🔍", "save": "💾", From f862a9adb990b6702c2d737a13b7ff9675cb54e2 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Thu, 25 Jan 2024 09:42:53 -0600 Subject: [PATCH 09/13] increase threading count to 10 --- pan_os_upgrade/upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index 8cb33d4..5ff4a3d 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -2719,7 +2719,7 @@ def main( ) # Using ThreadPoolExecutor to manage threads - with ThreadPoolExecutor(max_workers=2) as executor: + with ThreadPoolExecutor(max_workers=10) as executor: # Store future objects along with firewalls for reference future_to_firewall = { executor.submit( From 520b16fdefab2261a51b327e82cf6c3745c1a871 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Thu, 25 Jan 2024 10:02:15 -0600 Subject: [PATCH 10/13] correcting an issue where single target firewalls did not initiate upgrade workflow --- pan_os_upgrade/upgrade.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index 5ff4a3d..ec6b722 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -2701,6 +2701,30 @@ def main( f"{get_emoji('success')} {hostname}: Connection to firewall established" ) firewalls_to_upgrade.append(device) + + # Using ThreadPoolExecutor to manage threads + with ThreadPoolExecutor(max_workers=1) as executor: + # Store future objects along with firewalls for reference + future_to_firewall = { + executor.submit( + upgrade_firewall, + fw, + target_version, + dry_run, + ): fw + for fw in firewalls_to_upgrade + } + + # Process completed tasks + for future in as_completed(future_to_firewall): + firewall = future_to_firewall[future] + try: + future.result() + except Exception as exc: + logging.error( + f"{get_emoji('error')} {hostname}: Firewall {firewall.hostname} generated an exception: {exc}" + ) + elif type(device) is Panorama: if not filter: logging.error( From 94d810143080ab68c6e1868cf422ee8107bfedbe Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Thu, 25 Jan 2024 10:02:34 -0600 Subject: [PATCH 11/13] version bump --- docker/Dockerfile | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 5cceb9b..cea6b8b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,8 +11,8 @@ RUN apk add --no-cache gcc musl-dev libffi-dev make WORKDIR /app # Install any needed packages specified in requirements.txt -# Note: The requirements.txt should contain pan-os-upgrade==0.25 -RUN pip install --no-cache-dir pan-os-upgrade==0.25 +# Note: The requirements.txt should contain pan-os-upgrade==0.3.0 +RUN pip install --no-cache-dir pan-os-upgrade==0.3.0 # Set the locale to avoid issues with emoji rendering ENV LANG C.UTF-8 diff --git a/pyproject.toml b/pyproject.toml index c8cb819..37f1b29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pan-os-upgrade" -version = "0.25" +version = "0.3.0" description = "Python script to automate the upgrade process of PAN-OS firewalls." authors = ["Calvin Remsburg "] license = "Apache 2.0" From 359854cc1883e912fb8024a50e5d560cf2b9d442 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Thu, 25 Jan 2024 10:30:37 -0600 Subject: [PATCH 12/13] Refactor HA sync check and update log messages --- pan_os_upgrade/upgrade.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pan_os_upgrade/upgrade.py b/pan_os_upgrade/upgrade.py index ec6b722..f3abc23 100644 --- a/pan_os_upgrade/upgrade.py +++ b/pan_os_upgrade/upgrade.py @@ -1707,11 +1707,13 @@ def upgrade_firewall( with firewalls_to_revisit_lock: is_firewall_to_revisit = firewall in firewalls_to_revisit - perform_ha_sync_check( - hostname, - ha_details, - strict_sync_check=not is_firewall_to_revisit, - ) + # Perform HA sync check, skipping standalone firewalls + if ha_details: + perform_ha_sync_check( + hostname, + ha_details, + strict_sync_check=not is_firewall_to_revisit, + ) # Back up configuration to local filesystem logging.info( @@ -2613,7 +2615,7 @@ def main( "--filter", "-f", help="Filter string - when connecting to Panorama, defines which devices we are to upgrade.", - prompt="Filter string (only applicable for Panorama)", + prompt="Filter string (only applicable for Panorama connections)", ), ] = "", log_level: Annotated[ @@ -2783,11 +2785,11 @@ def main( try: future.result() logging.info( - f"{get_emoji('success')} {hostname}: Completed revisiting firewall: {firewall.hostname}" + f"{get_emoji('success')} {hostname}: Completed revisiting firewalls" ) except Exception as exc: logging.error( - f"{get_emoji('error')} {hostname}: Exception while revisiting firewall {firewall.hostname}: {exc}" + f"{get_emoji('error')} {hostname}: Exception while revisiting firewalls: {exc}" ) with firewalls_to_revisit_lock: From 3231f87ff230f475dd62c7f751bb8d8b615f8574 Mon Sep 17 00:00:00 2001 From: Calvin Remsburg Date: Thu, 25 Jan 2024 10:41:18 -0600 Subject: [PATCH 13/13] Update documentation to reflect HA and multithreading --- README.md | 314 +++++++++++++++------------- docs/about/release-notes.md | 11 + docs/user-guide/docker/execution.md | 102 +++++++++ docs/user-guide/introduction.md | 9 +- docs/user-guide/python/execution.md | 227 +++++++++++--------- 5 files changed, 417 insertions(+), 246 deletions(-) diff --git a/README.md b/README.md index 4659cdf..82bfc94 100644 --- a/README.md +++ b/README.md @@ -44,67 +44,117 @@ This project is a comprehensive Python-based solution for automating PAN-OS upgrades. It's designed to provide network administrators and security professionals with an efficient tool to manage upgrades, configurations, and system checks of Palo Alto Networks appliances. -Key Features: +### Key Features -* Automates routine tasks, reducing manual errors and saving time. -* Connect to firewalls directly or through a Panorama appliance as a proxy. -* Customizable scripts to fit various network environments and requirements. -* Extensive interaction with Palo Alto Networks appliances for operations like readiness checks, state snapshots, and report generation. +- **Automation of Routine Tasks**: Reduces manual errors and saves time by automating upgrades, configurations, and system checks. +- **Support for Direct and Proxy Connections**: Connect directly to firewalls or through a Panorama appliance, with support for targeting specific devices using filters. +- **Active/Passive High Availability (HA) Workflow**: Fully supports upgrading devices in active/passive HA configurations, ensuring both members are properly upgraded and synchronized. +- **Multi-threading for Efficiency**: Utilizes multi-threading to parallelize upgrades, especially beneficial when upgrading multiple devices through Panorama, enhancing performance and reducing overall upgrade time. +- **Customizable and Extensible**: Scripts can be tailored to fit diverse network environments and requirements, offering flexibility for various deployment scenarios. +- **Comprehensive PAN-OS Interactions**: Facilitates extensive interactions with Palo Alto Networks appliances for operations like readiness checks, state snapshots, and report generation. -> Note: this script is targeted towards standalone and `active-passive` HA environments, no testing has been performed against `active-active` or clustered firewalls. +> **Note**: While this script is optimized for standalone and active/passive HA environments, it has not been tested against active/active or clustered firewalls. Example Execution
```console -pan-os-upgrade --hostname 192.168.255.211 --username admin --password secret --version 10.2.0-h2 -INFO - ✅ Connection to firewall established -INFO - 📝 007054000123456 houston 192.168.255.211 -INFO - 📝 Firewall HA mode: disabled -INFO - 📝 Current PAN-OS version: 10.2.0 -INFO - 📝 Target PAN-OS version: 10.2.0-h2 -INFO - ✅ Confirmed that moving from 10.2.0 to 10.2.0-h2 is an upgrade -INFO - ✅ Target PAN-OS version 10.2.0-h2 is available for download -INFO - ✅ Base image for 10.2.0-h2 is already downloaded -INFO - 🚀 Performing test to see if 10.2.0-h2 is already downloaded... -INFO - 🔍 PAN-OS version 10.2.0-h2 is not on the firewall -INFO - 🚀 PAN-OS version 10.2.0-h2 is beginning download -INFO - Device 007054000123456 downloading version: 10.2.0-h2 -INFO - ⚙️ Downloading PAN-OS version 10.2.0-h2 - Elapsed time: 4 seconds -INFO - ⚙️ Downloading PAN-OS version 10.2.0-h2 - Elapsed time: 36 seconds -INFO - ⚙️ Downloading PAN-OS version 10.2.0-h2 - Elapsed time: 71 seconds -INFO - ✅ 10.2.0-h2 downloaded in 103 seconds -INFO - ✅ PAN-OS version 10.2.0-h2 has been downloaded. -INFO - 🚀 Performing snapshot of network state information... -INFO - ✅ Network snapshot created successfully -INFO - 🚀 Performing readiness checks to determine if firewall is ready for upgrade... -INFO - ✅ Passed Readiness Check: Check if there are pending changes on device -INFO - ✅ Passed Readiness Check: No Expired Licenses -INFO - ✅ Passed Readiness Check: Check if a there is enough space on the `/opt/panrepo` volume for downloading an PanOS image. -INFO - ✅ Passed Readiness Check: Check if NTP is synchronized -INFO - ✅ Passed Readiness Check: Check connectivity with the Panorama appliance -INFO - ✅ Readiness Checks completed -INFO - 🚀 Performing backup of houston's configuration to local filesystem... -INFO - 🚀 Not a dry run, continue with upgrade... -INFO - 🚀 Performing upgrade on houston to version 10.2.0-h2... -INFO - 🚀 Attempting upgrade houston to version 10.2.0-h2 (Attempt 1 of 3)... -INFO - Device 007054000123456 installing version: 10.2.0-h2 -INFO - ✅ houston upgrade completed successfully -INFO - 🚀 Rebooting the firewall... -INFO - 📝 Command succeeded with no output -INFO - ⚙️ Firewall is responding to requests but hasn't finished its reboot process... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is responding to requests but hasn't finished its reboot process... -INFO - ⚙️ Firewall is responding to requests but hasn't finished its reboot process... -INFO - ⚙️ Firewall is responding to requests but hasn't finished its reboot process... -INFO - ✅ Firewall upgraded and rebooted in 542 seconds +$ pan-os-upgrade +Hostname or IP: panorama.cdot.io +Username: cdot +Password: +Target PAN-OS version: 10.2.2-h2 +Filter string (only applicable for Panorama) []: hostname=Woodlands* +✅ panorama.cdot.io: Connection to Panorama established. Firewall connections will be proxied! +📝 Woodlands-fw1: 007954000123451 192.168.255.43 +📝 Woodlands-fw2: 007954000123452 192.168.255.44 +📝 Woodlands-fw1: HA mode: passive +📝 Woodlands-fw2: HA mode: active +🔍 Woodlands-fw2: Detected active firewall in HA pair running the same version as its peer. Added firewall to revisit list. +📝 Woodlands-fw1: Current PAN-OS version: 10.2.2 +📝 Woodlands-fw1: Target PAN-OS version: 10.2.2-h2 +✅ Woodlands-fw1: Upgrade required from 10.2.2 to 10.2.2-h2 +✅ Woodlands-fw1: PAN-OS version 10.2.2-h2 is available for download +✅ Woodlands-fw1: Base image for 10.2.2-h2 is already downloaded +🚀 Woodlands-fw1: Performing test to see if 10.2.2-h2 is already downloaded... +🔍 Woodlands-fw1: PAN-OS version 10.2.2-h2 is not on the firewall +🚀 Woodlands-fw1: PAN-OS version 10.2.2-h2 is beginning download +Device 007954000123451 downloading version: 10.2.2-h2 +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 5 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 37 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 68 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 100 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 133 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 167 seconds +✅ Woodlands-fw1: 10.2.2-h2 downloaded in 199 seconds +✅ Woodlands-fw1: 10.2.2-h2 has been downloaded and sync'd to HA peer. +🚀 Woodlands-fw1: Performing snapshot of network state information... +✅ Woodlands-fw1: Network snapshot created successfully +🚀 Woodlands-fw1: Performing readiness checks to determine if firewall is ready for upgrade... +✅ Woodlands-fw1: Passed Readiness Check: Check if there are pending changes on device +✅ Woodlands-fw1: Passed Readiness Check: No Expired Licenses +✅ Woodlands-fw1: Passed Readiness Check: Checks HA pair status from the perspective of the current device +✅ Woodlands-fw1: Passed Readiness Check: Check if NTP is synchronized +✅ Woodlands-fw1: Passed Readiness Check: Check connectivity with the Panorama appliance +✅ Woodlands-fw1: Readiness Checks completed +🚀 Woodlands-fw1: Checking if HA peer is in sync... +✅ Woodlands-fw1: HA peer sync test has been completed. +🚀 Woodlands-fw1: Performing backup of configuration to local filesystem... +🚀 Woodlands-fw1: Not a dry run, continue with upgrade... +🚀 Woodlands-fw1: Performing upgrade to version 10.2.2-h2... +🚀 Woodlands-fw1: Attempting upgrade to version 10.2.2-h2 (Attempt 1 of 3)... +Device 007954000123451 installing version: 10.2.2-h2 +✅ Woodlands-fw1: Upgrade completed successfully +🚀 Woodlands-fw1: Rebooting the passive HA firewall... +📝 Woodlands-fw1: Command succeeded with no output +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +✅ Woodlands-fw1: HA passive firewall rebooted and synchronized with its peer in 499 seconds +🚀 panorama.cdot.io: Revisiting firewalls that were active in an HA pair and had the same version as their peers. +📝 Woodlands-fw2: 007954000123452 192.168.255.44 +📝 Woodlands-fw2: HA mode: active +❌ Woodlands-fw2: Error suspending active firewall HA state: argument of type 'NoneType' is not iterable +📝 Woodlands-fw2: Current PAN-OS version: 10.2.2 +📝 Woodlands-fw2: Target PAN-OS version: 10.2.2-h2 +✅ Woodlands-fw2: Upgrade required from 10.2.2 to 10.2.2-h2 +✅ Woodlands-fw2: PAN-OS version 10.2.2-h2 is available for download +✅ Woodlands-fw2: Base image for 10.2.2-h2 is already downloaded +🚀 Woodlands-fw2: Performing test to see if 10.2.2-h2 is already downloaded... +✅ Woodlands-fw2: PAN-OS version 10.2.2-h2 already on firewall. +✅ Woodlands-fw2: 10.2.2-h2 has been downloaded and sync'd to HA peer. +🚀 Woodlands-fw2: Performing snapshot of network state information... +✅ Woodlands-fw2: Network snapshot created successfully +🚀 Woodlands-fw2: Performing readiness checks to determine if firewall is ready for upgrade... +✅ Woodlands-fw2: Passed Readiness Check: Check if there are pending changes on device +✅ Woodlands-fw2: Passed Readiness Check: No Expired Licenses +✅ Woodlands-fw2: Passed Readiness Check: Check if NTP is synchronized +✅ Woodlands-fw2: Passed Readiness Check: Check connectivity with the Panorama appliance +✅ Woodlands-fw2: Readiness Checks completed +🚀 Woodlands-fw2: Checking if HA peer is in sync... +✅ Woodlands-fw2: HA peer sync test has been completed. +🚀 Woodlands-fw2: Performing backup of configuration to local filesystem... +🚀 Woodlands-fw2: Not a dry run, continue with upgrade... +🚀 Woodlands-fw2: Performing upgrade to version 10.2.2-h2... +🚀 Woodlands-fw2: Attempting upgrade to version 10.2.2-h2 (Attempt 1 of 3)... +Device 007954000123452 installing version: 10.2.2-h2 +✅ Woodlands-fw2: Upgrade completed successfully +🚀 Woodlands-fw2: Rebooting the passive HA firewall... +📝 Woodlands-fw2: Command succeeded with no output +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +✅ Woodlands-fw2: HA passive firewall rebooted and synchronized with its peer in 483 seconds +✅ panorama.cdot.io: Completed revisiting firewalls ```
@@ -179,52 +229,49 @@ $ pan-os-upgrade Hostname or IP: houston.cdot.io Username: cdot Password: -Target PAN-OS version: 10.2.3-h4 -✅ Connection to firewall established -📝 007054000123456 houston 192.168.255.211 -📝 Firewall HA mode: disabled -📝 Current PAN-OS version: 10.2.3-h2 -📝 Target PAN-OS version: 10.2.3-h4 -✅ Confirmed that moving from 10.2.3-h2 to 10.2.3-h4 is an upgrade -✅ PAN-OS version 10.2.3-h4 is available for download -✅ Base image for 10.2.3-h4 is already downloaded -🚀 Performing test to see if 10.2.3-h4 is already downloaded... -🔍 PAN-OS version 10.2.3-h4 is not on the firewall -🚀 PAN-OS version 10.2.3-h4 is beginning download -Device 007054000123456 downloading version: 10.2.3-h4 -Downloading PAN-OS version 10.2.3-h4 - Elapsed time: 4 seconds -Downloading PAN-OS version 10.2.3-h4 - Elapsed time: 36 seconds -Downloading PAN-OS version 10.2.3-h4 - Elapsed time: 68 seconds -Downloading PAN-OS version 10.2.3-h4 - Elapsed time: 101 seconds -✅ 10.2.3-h4 downloaded in 134 seconds -✅ PAN-OS version 10.2.3-h4 has been downloaded. -🚀 Performing snapshot of network state information... -✅ Network snapshot created successfully -🚀 Performing readiness checks to determine if firewall is ready for upgrade... -✅ Passed Readiness Check: Check if there are pending changes on device -✅ Passed Readiness Check: No Expired Licenses -✅ Passed Readiness Check: Check if NTP is synchronized -✅ Passed Readiness Check: Check connectivity with the Panorama appliance -✅ Readiness Checks completed -🚀 Performing backup of houston's configuration to local filesystem... -🚀 Not a dry run, continue with upgrade... -🚀 Performing upgrade on houston to version 10.2.3-h4... -🚀 Attempting upgrade houston to version 10.2.3-h4 (Attempt 1 of 3)... -Device 007054000123456 installing version: 10.2.3-h4 -❌ houston upgrade error: Device 007054000123456 attempt to install version 10.2.3-h4 failed: ['Failed to install 10.2.3-h4 with the following errors.\nSW version is 10.2.3-h4\nThe software manager is currently in use. Please try again later.\nFailed to install version 10.2.3-h4 type panos\n\n'] -⚠️ Software manager is busy. Retrying in 60 seconds... -🚀 Attempting upgrade houston to version 10.2.3-h4 (Attempt 2 of 3)... -Device 007054000123456 installing version: 10.2.3-h4 -✅ houston upgrade completed successfully -🚀 Rebooting the standalone firewall... -📝 Command succeeded with no output -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -📝 Firewall version: 10.2.3-h4 -✅ Firewall rebooted in 473 seconds +Target PAN-OS version: 10.2.4 +Filter string (only applicable for Panorama connections) []: +✅ houston.cdot.io: Connection to firewall established +📝 houston: 007954000123453 192.168.255.211 +📝 houston: HA mode: disabled +📝 houston: Current PAN-OS version: 10.2.3-h4 +📝 houston: Target PAN-OS version: 10.2.4 +✅ houston: Upgrade required from 10.2.3-h4 to 10.2.4 +✅ houston: PAN-OS version 10.2.4 is available for download +✅ houston: Base image for 10.2.4 is already downloaded +🚀 houston: Performing test to see if 10.2.4 is already downloaded... +🔍 houston: PAN-OS version 10.2.4 is not on the firewall +🚀 houston: PAN-OS version 10.2.4 is beginning download +Device 007954000123453 downloading version: 10.2.4 +🔧 houston: Downloading PAN-OS version 10.2.4 - Elapsed time: 11 seconds +🔧 houston: Downloading PAN-OS version 10.2.4 - Elapsed time: 48 seconds +🔧 houston: Downloading PAN-OS version 10.2.4 - Elapsed time: 84 seconds +✅ houston: 10.2.4 downloaded in 118 seconds +✅ houston: PAN-OS version 10.2.4 has been downloaded. +🚀 houston: Performing snapshot of network state information... +✅ houston: Network snapshot created successfully +🚀 houston: Performing readiness checks to determine if firewall is ready for upgrade... +✅ houston: Passed Readiness Check: Check if there are pending changes on device +✅ houston: Passed Readiness Check: No Expired Licenses +✅ houston: Passed Readiness Check: Check if NTP is synchronized +✅ houston: Passed Readiness Check: Check connectivity with the Panorama appliance +✅ houston: Readiness Checks completed +🚀 houston: Performing backup of configuration to local filesystem... +🚀 houston: Not a dry run, continue with upgrade... +🚀 houston: Performing upgrade to version 10.2.4... +🚀 houston: Attempting upgrade to version 10.2.4 (Attempt 1 of 3)... +Device 007954000123453 installing version: 10.2.4 +✅ houston: Upgrade completed successfully +🚀 houston: Rebooting the standalone firewall... +📝 houston: Command succeeded with no output +🔧 houston: Firewall is rebooting... +🔧 houston: Firewall is rebooting... +🔧 houston: Firewall is rebooting... +🔧 houston: Firewall is rebooting... +🔧 houston: Firewall is rebooting... +🔧 houston: Firewall is rebooting... +📝 houston: Firewall version: 10.2.4 +✅ houston: Firewall rebooted in 516 seconds ``` As an alternative to targeting firewalls directly, you can target a Panorama appliance to act as the communication proxy. If you'd like to go down this path, make sure that you add an extra CLI option of `--filter` and pass a string representation of your filter. @@ -233,60 +280,33 @@ As of version 0.2.5, the available filters are: | filter type | description | example | | ----------- | ------------------------------------------------- | ----------------------------------- | -| hostname | use the firewall's hostname as selection criteria | `--filter "hostname=houston"` | +| hostname | use the firewall's hostname as selection criteria | `--filter "hostname=Woodlands*"` | | serial | use the firewall's serial as selection criteria | `--filter "serial=007054000123456"` | ```console -$ pan-os-upgrade --filter 'hostname=houston' +$ pan-os-upgrade Hostname or IP: panorama.cdot.io Username: cdot Password: -Target PAN-OS version: 10.2.3-h2 -✅ Connection to Panorama established. Firewall connections will be proxied! -📝 007054000123456 houston 192.168.255.211 -📝 Firewall HA mode: disabled -📝 Current PAN-OS version: 10.2.3 -📝 Target PAN-OS version: 10.2.3-h2 -✅ Confirmed that moving from 10.2.3 to 10.2.3-h2 is an upgrade -✅ PAN-OS version 10.2.3-h2 is available for download -✅ Base image for 10.2.3-h2 is already downloaded -🚀 Performing test to see if 10.2.3-h2 is already downloaded... -🔍 PAN-OS version 10.2.3-h2 is not on the firewall -🚀 PAN-OS version 10.2.3-h2 is beginning download -Device 007054000123456 downloading version: 10.2.3-h2 -Downloading PAN-OS version 10.2.3-h2 - Elapsed time: 8 seconds -Downloading PAN-OS version 10.2.3-h2 - Elapsed time: 42 seconds -Downloading PAN-OS version 10.2.3-h2 - Elapsed time: 75 seconds -Downloading PAN-OS version 10.2.3-h2 - Elapsed time: 110 seconds -Downloading PAN-OS version 10.2.3-h2 - Elapsed time: 151 seconds -✅ 10.2.3-h2 downloaded in 182 seconds -✅ PAN-OS version 10.2.3-h2 has been downloaded. -🚀 Performing snapshot of network state information... -✅ Network snapshot created successfully -🚀 Performing readiness checks to determine if firewall is ready for upgrade... -✅ Passed Readiness Check: Check if there are pending changes on device -✅ Passed Readiness Check: No Expired Licenses -✅ Passed Readiness Check: Check if NTP is synchronized -✅ Passed Readiness Check: Check if the clock is synchronized between dataplane and management plane -✅ Passed Readiness Check: Check connectivity with the Panorama appliance -✅ Readiness Checks completed -🚀 Performing backup of houston's configuration to local filesystem... -🚀 Not a dry run, continue with upgrade... -🚀 Performing upgrade on houston to version 10.2.3-h2... -🚀 Attempting upgrade houston to version 10.2.3-h2 (Attempt 1 of 3)... -Device 007054000123456 installing version: 10.2.3-h2 -✅ houston upgrade completed successfully -🚀 Rebooting the standalone firewall... -📝 Command succeeded with no output -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -📝 Firewall version: 10.2.3-h2 -✅ Firewall rebooted in 484 seconds +Target PAN-OS version: 10.2.2-h2 +Filter string (only applicable for Panorama connections) []: hostname=Woodlands* +✅ panorama.cdot.io: Connection to Panorama established. Firewall connections will be proxied! +📝 Woodlands-fw1: 007954000123451 192.168.255.43 +📝 Woodlands-fw2: 007954000123452 192.168.255.44 +📝 Woodlands-fw1: HA mode: passive +📝 Woodlands-fw2: HA mode: active +🔍 Woodlands-fw2: Detected active firewall in HA pair running the same version as its peer. Added firewall to revisit list. +📝 Woodlands-fw1: Current PAN-OS version: 10.2.2 +📝 Woodlands-fw1: Target PAN-OS version: 10.2.2-h2 +✅ Woodlands-fw1: Upgrade required from 10.2.2 to 10.2.2-h2 +✅ Woodlands-fw1: PAN-OS version 10.2.2-h2 is available for download +✅ Woodlands-fw1: Base image for 10.2.2-h2 is already downloaded +🚀 Woodlands-fw1: Performing test to see if 10.2.2-h2 is already downloaded... +🔍 Woodlands-fw1: PAN-OS version 10.2.2-h2 is not on the firewall +🚀 Woodlands-fw1: PAN-OS version 10.2.2-h2 is beginning download +Device 007954000123451 downloading version: 10.2.2-h2 +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 5 seconds +... shortened for brevity ... ``` ##### Option 2: Execute `pan-os-upgrade` Using Command-Line Arguments diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 597000f..f27baa3 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -2,10 +2,21 @@ 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 0.3.0 + +**Release Date:** *<20240125>* + +### What's New + +- Multi-threading added for concurrent upgrades (max limit of threads is 10). +- Gracefully handle HA upgrades for HA active/passive peers. +- Added hostname to log entries to differentiate threaded upgrades. + ## Version 0.2.5 **Release Date:** *<20240123>* + ### What's New - Supports the ability to connect to Panorama as a proxy for firewall connections diff --git a/docs/user-guide/docker/execution.md b/docs/user-guide/docker/execution.md index 5674835..65d04a6 100644 --- a/docs/user-guide/docker/execution.md +++ b/docs/user-guide/docker/execution.md @@ -44,6 +44,108 @@ docker run -v %CD%/assurance:/app/assurance -v %CD%/logs:/app/logs -it ghcr.io/c The container runs interactively, prompting you for details like IP address, username, password, and target PAN-OS version. If connecting to firewalls through Panorama as a proxy, you will also be prompted to provide a `--filter` option to specify the criteria for selecting the managed firewalls to upgrade. +
+ +```console +$ docker run -v $(pwd)/assurance:/app/assurance -v $(pwd)/logs:/app/logs -it ghcr.io/cdot65/pan-os-upgrade:latest +Hostname or IP: panorama.cdot.io +Username: cdot +Password: +Target PAN-OS version: 10.2.2-h2 +Filter string (only applicable for Panorama) []: hostname=Woodlands* +✅ panorama.cdot.io: Connection to Panorama established. Firewall connections will be proxied! +📝 Woodlands-fw1: 007954000123451 192.168.255.43 +📝 Woodlands-fw2: 007954000123452 192.168.255.44 +📝 Woodlands-fw1: HA mode: passive +📝 Woodlands-fw2: HA mode: active +🔍 Woodlands-fw2: Detected active firewall in HA pair running the same version as its peer. Added firewall to revisit list. +📝 Woodlands-fw1: Current PAN-OS version: 10.2.2 +📝 Woodlands-fw1: Target PAN-OS version: 10.2.2-h2 +✅ Woodlands-fw1: Upgrade required from 10.2.2 to 10.2.2-h2 +✅ Woodlands-fw1: PAN-OS version 10.2.2-h2 is available for download +✅ Woodlands-fw1: Base image for 10.2.2-h2 is already downloaded +🚀 Woodlands-fw1: Performing test to see if 10.2.2-h2 is already downloaded... +🔍 Woodlands-fw1: PAN-OS version 10.2.2-h2 is not on the firewall +🚀 Woodlands-fw1: PAN-OS version 10.2.2-h2 is beginning download +Device 007954000123451 downloading version: 10.2.2-h2 +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 5 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 37 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 68 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 100 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 133 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 167 seconds +✅ Woodlands-fw1: 10.2.2-h2 downloaded in 199 seconds +✅ Woodlands-fw1: 10.2.2-h2 has been downloaded and sync'd to HA peer. +🚀 Woodlands-fw1: Performing snapshot of network state information... +✅ Woodlands-fw1: Network snapshot created successfully +🚀 Woodlands-fw1: Performing readiness checks to determine if firewall is ready for upgrade... +✅ Woodlands-fw1: Passed Readiness Check: Check if there are pending changes on device +✅ Woodlands-fw1: Passed Readiness Check: No Expired Licenses +✅ Woodlands-fw1: Passed Readiness Check: Checks HA pair status from the perspective of the current device +✅ Woodlands-fw1: Passed Readiness Check: Check if NTP is synchronized +✅ Woodlands-fw1: Passed Readiness Check: Check connectivity with the Panorama appliance +✅ Woodlands-fw1: Readiness Checks completed +🚀 Woodlands-fw1: Checking if HA peer is in sync... +✅ Woodlands-fw1: HA peer sync test has been completed. +🚀 Woodlands-fw1: Performing backup of configuration to local filesystem... +🚀 Woodlands-fw1: Not a dry run, continue with upgrade... +🚀 Woodlands-fw1: Performing upgrade to version 10.2.2-h2... +🚀 Woodlands-fw1: Attempting upgrade to version 10.2.2-h2 (Attempt 1 of 3)... +Device 007954000123451 installing version: 10.2.2-h2 +✅ Woodlands-fw1: Upgrade completed successfully +🚀 Woodlands-fw1: Rebooting the passive HA firewall... +📝 Woodlands-fw1: Command succeeded with no output +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +✅ Woodlands-fw1: HA passive firewall rebooted and synchronized with its peer in 499 seconds +🚀 panorama.cdot.io: Revisiting firewalls that were active in an HA pair and had the same version as their peers. +📝 Woodlands-fw2: 007954000123452 192.168.255.44 +📝 Woodlands-fw2: HA mode: active +❌ Woodlands-fw2: Error suspending active firewall HA state: argument of type 'NoneType' is not iterable +📝 Woodlands-fw2: Current PAN-OS version: 10.2.2 +📝 Woodlands-fw2: Target PAN-OS version: 10.2.2-h2 +✅ Woodlands-fw2: Upgrade required from 10.2.2 to 10.2.2-h2 +✅ Woodlands-fw2: PAN-OS version 10.2.2-h2 is available for download +✅ Woodlands-fw2: Base image for 10.2.2-h2 is already downloaded +🚀 Woodlands-fw2: Performing test to see if 10.2.2-h2 is already downloaded... +✅ Woodlands-fw2: PAN-OS version 10.2.2-h2 already on firewall. +✅ Woodlands-fw2: 10.2.2-h2 has been downloaded and sync'd to HA peer. +🚀 Woodlands-fw2: Performing snapshot of network state information... +✅ Woodlands-fw2: Network snapshot created successfully +🚀 Woodlands-fw2: Performing readiness checks to determine if firewall is ready for upgrade... +✅ Woodlands-fw2: Passed Readiness Check: Check if there are pending changes on device +✅ Woodlands-fw2: Passed Readiness Check: No Expired Licenses +✅ Woodlands-fw2: Passed Readiness Check: Check if NTP is synchronized +✅ Woodlands-fw2: Passed Readiness Check: Check connectivity with the Panorama appliance +✅ Woodlands-fw2: Readiness Checks completed +🚀 Woodlands-fw2: Checking if HA peer is in sync... +✅ Woodlands-fw2: HA peer sync test has been completed. +🚀 Woodlands-fw2: Performing backup of configuration to local filesystem... +🚀 Woodlands-fw2: Not a dry run, continue with upgrade... +🚀 Woodlands-fw2: Performing upgrade to version 10.2.2-h2... +🚀 Woodlands-fw2: Attempting upgrade to version 10.2.2-h2 (Attempt 1 of 3)... +Device 007954000123452 installing version: 10.2.2-h2 +✅ Woodlands-fw2: Upgrade completed successfully +🚀 Woodlands-fw2: Rebooting the passive HA firewall... +📝 Woodlands-fw2: Command succeeded with no output +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +✅ Woodlands-fw2: HA passive firewall rebooted and synchronized with its peer in 483 seconds +✅ panorama.cdot.io: Completed revisiting firewalls +``` + +
+ ## Troubleshooting Panorama Proxy Connections When using Panorama as a connection proxy: diff --git a/docs/user-guide/introduction.md b/docs/user-guide/introduction.md index f57501f..39880e9 100644 --- a/docs/user-guide/introduction.md +++ b/docs/user-guide/introduction.md @@ -32,9 +32,12 @@ The Docker workflow simplifies the setup by encapsulating the tool and its depen `pan-os-upgrade` is equipped with several features for efficient and reliable upgrades: -- **Leveraging `panos-upgrade-assurance`**: It utilizes the `panos-upgrade-assurance` library to manage complex aspects of the upgrade process. -- **Data Validation with Pydantic**: Ensures robust data structure validation, minimizing bugs and streamlining workflow execution. -- **Flexible Connection Methods**: Connect to firewalls directly or by targeting a Panorama appliance with a `--filter` CLI option. +- **Automation of Routine Tasks**: Reduces manual errors and saves time by automating upgrades, configurations, and system checks. +- **Support for Direct and Proxy Connections**: Connect directly to firewalls or through a Panorama appliance, with support for targeting specific devices using filters. +- **Active/Passive High Availability (HA) Workflow**: Fully supports upgrading devices in active/passive HA configurations, ensuring both members are properly upgraded and synchronized. +- **Multi-threading for Efficiency**: Utilizes multi-threading to parallelize upgrades, especially beneficial when upgrading multiple devices through Panorama, enhancing performance and reducing overall upgrade time. +- **Customizable and Extensible**: Scripts can be tailored to fit diverse network environments and requirements, offering flexibility for various deployment scenarios. +- **Comprehensive PAN-OS Interactions**: Facilitates extensive interactions with Palo Alto Networks appliances for operations like readiness checks, state snapshots, and report generation. ## Next Steps diff --git a/docs/user-guide/python/execution.md b/docs/user-guide/python/execution.md index 4c9ce03..50b9ace 100644 --- a/docs/user-guide/python/execution.md +++ b/docs/user-guide/python/execution.md @@ -12,56 +12,46 @@ You can start the script interactively by simply issuing `pan-os-upgrade` from y ```console $ pan-os-upgrade -Hostname or IP: 192.168.255.1 -Username: admin +Hostname or IP: houston.cdot.io +Username: cdot Password: -Target PAN-OS version: 11.1.1 -INFO - ✅ Connection to firewall established -INFO - 📝 007054000123456 houston 192.168.255.211 -INFO - 📝 Firewall HA mode: disabled -INFO - 📝 Current PAN-OS version: 10.2.0 -INFO - 📝 Target PAN-OS version: 10.2.0-h2 -INFO - ✅ Confirmed that moving from 10.2.0 to 10.2.0-h2 is an upgrade -INFO - ✅ Target PAN-OS version 10.2.0-h2 is available for download -INFO - ✅ Base image for 10.2.0-h2 is already downloaded -INFO - 🚀 Performing test to see if 10.2.0-h2 is already downloaded... -INFO - 🔍 PAN-OS version 10.2.0-h2 is not on the firewall -INFO - 🚀 PAN-OS version 10.2.0-h2 is beginning download -INFO - Device 007054000123456 downloading version: 10.2.0-h2 -INFO - ⚙️ Downloading PAN-OS version 10.2.0-h2 - Elapsed time: 4 seconds -INFO - ⚙️ Downloading PAN-OS version 10.2.0-h2 - Elapsed time: 36 seconds -INFO - ⚙️ Downloading PAN-OS version 10.2.0-h2 - Elapsed time: 71 seconds -INFO - ✅ 10.2.0-h2 downloaded in 103 seconds -INFO - ✅ PAN-OS version 10.2.0-h2 has been downloaded. -INFO - 🚀 Performing snapshot of network state information... -INFO - ✅ Network snapshot created successfully -INFO - 🚀 Performing readiness checks to determine if firewall is ready for upgrade... -INFO - ✅ Passed Readiness Check: Check if there are pending changes on device -INFO - ✅ Passed Readiness Check: No Expired Licenses -INFO - ✅ Passed Readiness Check: Check if a there is enough space on the `/opt/panrepo` volume for downloading an PanOS image. -INFO - ✅ Passed Readiness Check: Check if NTP is synchronized -INFO - ✅ Passed Readiness Check: Check connectivity with the Panorama appliance -INFO - ✅ Readiness Checks completed -INFO - 🚀 Performing backup of houston's configuration to local filesystem... -INFO - 🚀 Not a dry run, continue with upgrade... -INFO - 🚀 Performing upgrade on houston to version 10.2.0-h2... -INFO - 🚀 Attempting upgrade houston to version 10.2.0-h2 (Attempt 1 of 3)... -INFO - Device 007054000123456 installing version: 10.2.0-h2 -INFO - ✅ houston upgrade completed successfully -INFO - 🚀 Rebooting the firewall... -INFO - 📝 Command succeeded with no output -INFO - ⚙️ Firewall is responding to requests but hasn't finished its reboot process... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is rebooting... -INFO - ⚙️ Firewall is responding to requests but hasn't finished its reboot process... -INFO - ⚙️ Firewall is responding to requests but hasn't finished its reboot process... -INFO - ⚙️ Firewall is responding to requests but hasn't finished its reboot process... -INFO - ✅ Firewall upgraded and rebooted in 542 seconds +Target PAN-OS version: 10.2.4 +Filter string (only applicable for Panorama connections) []: +✅ houston.cdot.io: Connection to firewall established +📝 houston: 007954000123453 192.168.255.211 +📝 houston: HA mode: disabled +📝 houston: Current PAN-OS version: 10.2.3-h4 +📝 houston: Target PAN-OS version: 10.2.4 +✅ houston: Upgrade required from 10.2.3-h4 to 10.2.4 +✅ houston: PAN-OS version 10.2.4 is available for download +✅ houston: Base image for 10.2.4 is already downloaded +🚀 houston: Performing test to see if 10.2.4 is already downloaded... +✅ houston: PAN-OS version 10.2.4 already on firewall. +✅ houston: PAN-OS version 10.2.4 has been downloaded. +🚀 houston: Performing snapshot of network state information... +✅ houston: Network snapshot created successfully +🚀 houston: Performing readiness checks to determine if firewall is ready for upgrade... +✅ houston: Passed Readiness Check: Check if there are pending changes on device +✅ houston: Passed Readiness Check: No Expired Licenses +✅ houston: Passed Readiness Check: Check if NTP is synchronized +✅ houston: Passed Readiness Check: Check connectivity with the Panorama appliance +✅ houston: Readiness Checks completed +🚀 houston: Performing backup of configuration to local filesystem... +🚀 houston: Not a dry run, continue with upgrade... +🚀 houston: Performing upgrade to version 10.2.4... +🚀 houston: Attempting upgrade to version 10.2.4 (Attempt 1 of 3)... +Device 007954000123453 installing version: 10.2.4 +✅ houston: Upgrade completed successfully +🚀 houston: Rebooting the standalone firewall... +📝 houston: Command succeeded with no output +🔧 houston: Firewall is rebooting... +🔧 houston: Firewall is rebooting... +🔧 houston: Firewall is rebooting... +🔧 houston: Firewall is rebooting... +🔧 houston: Firewall is rebooting... +🔧 houston: Firewall is rebooting... +📝 houston: Firewall version: 10.2.4 +✅ houston: Firewall rebooted in 516 seconds ``` @@ -91,56 +81,101 @@ $ pan-os-upgrade --hostname panorama.cdot.io --filter 'hostname=houston' --usern
```console -$ pan-os-upgrade --filter 'hostname=houston' +$ pan-os-upgrade Hostname or IP: panorama.cdot.io Username: cdot Password: -Target PAN-OS version: 10.2.3-h2 -✅ Connection to Panorama established. Firewall connections will be proxied! -📝 007054000123456 houston 192.168.255.211 -📝 Firewall HA mode: disabled -📝 Current PAN-OS version: 10.2.3 -📝 Target PAN-OS version: 10.2.3-h2 -✅ Confirmed that moving from 10.2.3 to 10.2.3-h2 is an upgrade -✅ PAN-OS version 10.2.3-h2 is available for download -✅ Base image for 10.2.3-h2 is already downloaded -🚀 Performing test to see if 10.2.3-h2 is already downloaded... -🔍 PAN-OS version 10.2.3-h2 is not on the firewall -🚀 PAN-OS version 10.2.3-h2 is beginning download -Device 007054000123456 downloading version: 10.2.3-h2 -Downloading PAN-OS version 10.2.3-h2 - Elapsed time: 8 seconds -Downloading PAN-OS version 10.2.3-h2 - Elapsed time: 42 seconds -Downloading PAN-OS version 10.2.3-h2 - Elapsed time: 75 seconds -Downloading PAN-OS version 10.2.3-h2 - Elapsed time: 110 seconds -Downloading PAN-OS version 10.2.3-h2 - Elapsed time: 151 seconds -✅ 10.2.3-h2 downloaded in 182 seconds -✅ PAN-OS version 10.2.3-h2 has been downloaded. -🚀 Performing snapshot of network state information... -✅ Network snapshot created successfully -🚀 Performing readiness checks to determine if firewall is ready for upgrade... -✅ Passed Readiness Check: Check if there are pending changes on device -✅ Passed Readiness Check: No Expired Licenses -✅ Passed Readiness Check: Check if NTP is synchronized -✅ Passed Readiness Check: Check if the clock is synchronized between dataplane and management plane -✅ Passed Readiness Check: Check connectivity with the Panorama appliance -✅ Readiness Checks completed -🚀 Performing backup of houston's configuration to local filesystem... -🚀 Not a dry run, continue with upgrade... -🚀 Performing upgrade on houston to version 10.2.3-h2... -🚀 Attempting upgrade houston to version 10.2.3-h2 (Attempt 1 of 3)... -Device 007054000123456 installing version: 10.2.3-h2 -✅ houston upgrade completed successfully -🚀 Rebooting the standalone firewall... -📝 Command succeeded with no output -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -⚙️ Firewall is rebooting... -📝 Firewall version: 10.2.3-h2 -✅ Firewall rebooted in 484 seconds +Target PAN-OS version: 10.2.2-h2 +Filter string (only applicable for Panorama) []: hostname=Woodlands* +✅ panorama.cdot.io: Connection to Panorama established. Firewall connections will be proxied! +📝 Woodlands-fw1: 007954000123451 192.168.255.43 +📝 Woodlands-fw2: 007954000123452 192.168.255.44 +📝 Woodlands-fw1: HA mode: passive +📝 Woodlands-fw2: HA mode: active +🔍 Woodlands-fw2: Detected active firewall in HA pair running the same version as its peer. Added firewall to revisit list. +📝 Woodlands-fw1: Current PAN-OS version: 10.2.2 +📝 Woodlands-fw1: Target PAN-OS version: 10.2.2-h2 +✅ Woodlands-fw1: Upgrade required from 10.2.2 to 10.2.2-h2 +✅ Woodlands-fw1: PAN-OS version 10.2.2-h2 is available for download +✅ Woodlands-fw1: Base image for 10.2.2-h2 is already downloaded +🚀 Woodlands-fw1: Performing test to see if 10.2.2-h2 is already downloaded... +🔍 Woodlands-fw1: PAN-OS version 10.2.2-h2 is not on the firewall +🚀 Woodlands-fw1: PAN-OS version 10.2.2-h2 is beginning download +Device 007954000123451 downloading version: 10.2.2-h2 +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 5 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 37 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 68 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 100 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 133 seconds +🔧 Woodlands-fw1: Downloading PAN-OS version 10.2.2-h2 - HA will sync image - Elapsed time: 167 seconds +✅ Woodlands-fw1: 10.2.2-h2 downloaded in 199 seconds +✅ Woodlands-fw1: 10.2.2-h2 has been downloaded and sync'd to HA peer. +🚀 Woodlands-fw1: Performing snapshot of network state information... +✅ Woodlands-fw1: Network snapshot created successfully +🚀 Woodlands-fw1: Performing readiness checks to determine if firewall is ready for upgrade... +✅ Woodlands-fw1: Passed Readiness Check: Check if there are pending changes on device +✅ Woodlands-fw1: Passed Readiness Check: No Expired Licenses +✅ Woodlands-fw1: Passed Readiness Check: Checks HA pair status from the perspective of the current device +✅ Woodlands-fw1: Passed Readiness Check: Check if NTP is synchronized +✅ Woodlands-fw1: Passed Readiness Check: Check connectivity with the Panorama appliance +✅ Woodlands-fw1: Readiness Checks completed +🚀 Woodlands-fw1: Checking if HA peer is in sync... +✅ Woodlands-fw1: HA peer sync test has been completed. +🚀 Woodlands-fw1: Performing backup of configuration to local filesystem... +🚀 Woodlands-fw1: Not a dry run, continue with upgrade... +🚀 Woodlands-fw1: Performing upgrade to version 10.2.2-h2... +🚀 Woodlands-fw1: Attempting upgrade to version 10.2.2-h2 (Attempt 1 of 3)... +Device 007954000123451 installing version: 10.2.2-h2 +✅ Woodlands-fw1: Upgrade completed successfully +🚀 Woodlands-fw1: Rebooting the passive HA firewall... +📝 Woodlands-fw1: Command succeeded with no output +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +🔧 Woodlands-fw1: Firewall is rebooting... +✅ Woodlands-fw1: HA passive firewall rebooted and synchronized with its peer in 499 seconds +🚀 panorama.cdot.io: Revisiting firewalls that were active in an HA pair and had the same version as their peers. +📝 Woodlands-fw2: 007954000123452 192.168.255.44 +📝 Woodlands-fw2: HA mode: active +❌ Woodlands-fw2: Error suspending active firewall HA state: argument of type 'NoneType' is not iterable +📝 Woodlands-fw2: Current PAN-OS version: 10.2.2 +📝 Woodlands-fw2: Target PAN-OS version: 10.2.2-h2 +✅ Woodlands-fw2: Upgrade required from 10.2.2 to 10.2.2-h2 +✅ Woodlands-fw2: PAN-OS version 10.2.2-h2 is available for download +✅ Woodlands-fw2: Base image for 10.2.2-h2 is already downloaded +🚀 Woodlands-fw2: Performing test to see if 10.2.2-h2 is already downloaded... +✅ Woodlands-fw2: PAN-OS version 10.2.2-h2 already on firewall. +✅ Woodlands-fw2: 10.2.2-h2 has been downloaded and sync'd to HA peer. +🚀 Woodlands-fw2: Performing snapshot of network state information... +✅ Woodlands-fw2: Network snapshot created successfully +🚀 Woodlands-fw2: Performing readiness checks to determine if firewall is ready for upgrade... +✅ Woodlands-fw2: Passed Readiness Check: Check if there are pending changes on device +✅ Woodlands-fw2: Passed Readiness Check: No Expired Licenses +✅ Woodlands-fw2: Passed Readiness Check: Check if NTP is synchronized +✅ Woodlands-fw2: Passed Readiness Check: Check connectivity with the Panorama appliance +✅ Woodlands-fw2: Readiness Checks completed +🚀 Woodlands-fw2: Checking if HA peer is in sync... +✅ Woodlands-fw2: HA peer sync test has been completed. +🚀 Woodlands-fw2: Performing backup of configuration to local filesystem... +🚀 Woodlands-fw2: Not a dry run, continue with upgrade... +🚀 Woodlands-fw2: Performing upgrade to version 10.2.2-h2... +🚀 Woodlands-fw2: Attempting upgrade to version 10.2.2-h2 (Attempt 1 of 3)... +Device 007954000123452 installing version: 10.2.2-h2 +✅ Woodlands-fw2: Upgrade completed successfully +🚀 Woodlands-fw2: Rebooting the passive HA firewall... +📝 Woodlands-fw2: Command succeeded with no output +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +🔧 Woodlands-fw2: Firewall is rebooting... +✅ Woodlands-fw2: HA passive firewall rebooted and synchronized with its peer in 483 seconds +✅ panorama.cdot.io: Completed revisiting firewalls ```