Skip to content

Commit

Permalink
feat: introduce --non-interactive option to firewall and batch comm…
Browse files Browse the repository at this point in the history
…ands

With the `--non-interactive` flag it is possible to upgrade firewalls
non-interactively without being prompted for confirmations.

Non-interactive mode requires parameters like hostname, username, password
to be passed in order to avoid prompts.

Dry run behaviour adjusted to be the default selection in interactive mode,
additionaly when `--dry-run` option is set "dry run" prompts are avoided.
You can only disable dry run by answering the dry run prompts as "no" or running
the tool in non-interactive mode.
  • Loading branch information
alperenkose committed Apr 17, 2024
1 parent db0ad37 commit b19ddf6
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 58 deletions.
53 changes: 19 additions & 34 deletions pan_os_upgrade/components/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,42 +35,34 @@

# Common setup for all subcommands
def common_setup(
hostname: str,
username: str,
password: str,
settings_file: LazySettings,
settings_file_path: Path,
) -> PanDevice:
) -> None:
"""
Initializes the environment for interacting with a Palo Alto Networks device, including directory setup, logging configuration, and establishing a device connection.
Initializes the environment for interacting with a Palo Alto Networks device, including directory setup and logging configuration.
This function consolidates essential preparatory steps required before performing operations on a Palo Alto Networks device. It ensures the creation of necessary directories for organized data storage and logs, sets up logging with a configurable verbosity level, and establishes a secure connection to the device using the provided API credentials. The function is designed to return a `PanDevice` object, which could be a `Firewall` or `Panorama` instance, ready for subsequent API interactions.
This function consolidates essential preparatory steps required before performing operations on a Palo Alto Networks device. It ensures the creation of necessary directories for organized data storage and logs, and sets up logging with a configurable verbosity level.
Parameters
----------
hostname : str
The network address or DNS name of the Palo Alto Networks device to connect to.
username : str
The API username for authenticating with the device.
password : str
The API password for authenticating with the device.
Returns
-------
PanDevice
A connected `PanDevice` instance, representing the target Palo Alto Networks device, fully initialized and ready for further API operations.
settings_file : LazySettings
The LazySettings object containing configurations loaded from the settings file.
settings_file_path : Path
The filesystem path to the settings.yaml file, which contains custom configuration settings.
Example
-------
Initializing the environment for a device:
>>> device = common_setup('10.0.0.1', 'apiuser', 'apipassword')
# Ensures necessary directories exist, logging is configured, and returns a connected `PanDevice` instance.
>>> common_setup(
settings_file=Dynaconf(settings_files=[str(SETTINGS_FILE_PATH)]),
settings_file_path=SETTINGS_FILE_PATH,
)
# Ensures necessary directories exist, and logging is configured.
Notes
-----
- Directory setup is performed only once; existing directories are not modified.
- Logging configuration affects the entire application's logging behavior; the log level can be overridden by `settings.yaml` if `SETTINGS_FILE_PATH` is detected in the function.
- A successful device connection is critical for the function to return; otherwise, it may raise exceptions based on connection issues.
The ability to override default settings with `settings.yaml` is supported for the log level configuration in this function if `SETTINGS_FILE_PATH` is utilized within `configure_logging`.
"""
Expand All @@ -93,14 +85,6 @@ def common_setup(
settings_file_path=settings_file_path,
)

# Connect to the device
device = connect_to_host(
hostname=hostname,
username=username,
password=password,
)
return device


def connect_to_host(
hostname: str,
Expand All @@ -115,16 +99,16 @@ def connect_to_host(
Parameters
----------
hostname : str
The hostname or IP address of the target Palo Alto Networks device.
api_username : str
The API username for authentication.
api_password : str
The password corresponding to the API username.
The network address or DNS name of the Palo Alto Networks device to connect to.
username : str
The API username for authenticating with the device.
password : str
The API password for authenticating with the device.
Returns
-------
PanDevice
A PanDevice object representing the connected device, which may be a Firewall or Panorama instance.
A PanDevice object representing the connected device, which may be a Firewall or Panorama instance, ready for further API operations.
Raises
------
Expand All @@ -146,6 +130,7 @@ def connect_to_host(
- Initiating a connection to a device is a prerequisite for performing any operational or configuration tasks via the API.
- The function's error handling provides clear diagnostics, aiding in troubleshooting connection issues.
- Configuration settings for the connection, such as timeout periods and retry attempts, can be customized through the `settings.yaml` file, if `settings_file_path` is utilized within the function.
- A successful device connection is critical for the function to return; otherwise, it may raise exceptions based on connection issues.
"""

try:
Expand Down
99 changes: 75 additions & 24 deletions pan_os_upgrade/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
from pathlib import Path
from threading import Lock
from typing_extensions import Annotated
from typing import Optional

# Palo Alto Networks imports
from panos.firewall import Firewall
Expand All @@ -93,6 +94,7 @@
from pan_os_upgrade.components.assurance import AssuranceOptions
from pan_os_upgrade.components.device import (
common_setup,
connect_to_host,
get_firewalls_from_panorama,
threaded_get_firewall_details,
)
Expand Down Expand Up @@ -182,14 +184,20 @@ def firewall(
),
],
dry_run: Annotated[
bool,
Optional[bool],
typer.Option(
"--dry-run",
"-d",
help="Perform a dry run of all tests and downloads without performing the actual upgrade",
prompt="Dry Run?",
),
] = True,
] = None,
non_interactive: Annotated[
bool,
typer.Option(
"--non-interactive",
help="Perform non-interactive upgrade with default options. Disables --dry-run option.",
),
] = False,
):
"""
Launches the upgrade process for a Palo Alto Networks firewall, facilitating a comprehensive and controlled upgrade workflow.
Expand All @@ -207,7 +215,9 @@ def firewall(
target_version : str
The version of PAN-OS to which the firewall is to be upgraded. Must be a valid and supported version for the device.
dry_run : bool, optional
When set to True, the function performs all preparatory and validation steps without executing the actual upgrade, defaulting to False.
When set, the function performs all preparatory and validation steps without executing the actual upgrade. Dry run is the default selection in interactive mode.
non_interactive: bool, optional
When set, the function performs all the upgrade steps without any prompts. Dry run is disabled in non interactive mode.
Examples
--------
Expand Down Expand Up @@ -235,12 +245,23 @@ def firewall(
typer.echo(banner)

# Perform common setup tasks, return a connected device
device = common_setup(
common_setup(
settings_file=SETTINGS_FILE,
settings_file_path=SETTINGS_FILE_PATH,
)

if non_interactive:
logging.info(
f"{get_emoji(action='skipped')} Non-interactive mode is set, ignoring --dry-run option."
)
dry_run = False # override dry run to false in non-interactive mode
elif dry_run is None: # if dry-run option is not set explicitly
dry_run = typer.confirm("Dry Run?", default=True)

device = connect_to_host(
hostname=hostname,
username=username,
password=password,
settings_file=SETTINGS_FILE,
settings_file_path=SETTINGS_FILE_PATH,
)

firewall_objects_for_upgrade = [device]
Expand Down Expand Up @@ -441,12 +462,15 @@ def panorama(
typer.echo(banner)

# Perform common setup tasks, return a connected device
device = common_setup(
common_setup(
settings_file=SETTINGS_FILE,
settings_file_path=SETTINGS_FILE_PATH,
)

device = connect_to_host(
hostname=hostname,
username=username,
password=password,
settings_file=SETTINGS_FILE,
settings_file_path=SETTINGS_FILE_PATH,
)

panorama_objects_for_upgrade = [device]
Expand Down Expand Up @@ -590,15 +614,20 @@ def batch(
),
],
dry_run: Annotated[
bool,
Optional[bool],
typer.Option(
"--dry-run",
"-d",
help="Perform a dry run of all tests and downloads without performing the actual upgrade",
prompt="Dry Run?",
is_flag=True,
),
] = True,
] = None,
non_interactive: Annotated[
bool,
typer.Option(
"--non-interactive",
help="Perform non-interactive upgrade with default options. Disables --dry-run option.",
),
] = False,
):
"""
Orchestrates a batch upgrade process for firewalls under Panorama's management. This command leverages Panorama
Expand All @@ -622,7 +651,9 @@ def batch(
target_version : str
The version of PAN-OS to which the firewalls should be upgraded.
dry_run : bool, optional
If set, the command simulates the upgrade process without making any changes to the devices. Defaults to True, meaning dry run is enabled by default.
If set, the command simulates the upgrade process without making any changes to the devices. Dry run is the default selection in interactive mode.
non_interactive: bool, optional
When set, the function performs all the upgrade steps without any prompts. Dry run is disabled in non interactive mode.
Examples
--------
Expand Down Expand Up @@ -665,12 +696,23 @@ def batch(
typer.echo(banner)

# Perform common setup tasks, return a connected device
device = common_setup(
common_setup(
settings_file=SETTINGS_FILE,
settings_file_path=SETTINGS_FILE_PATH,
)

if non_interactive:
logging.info(
f"{get_emoji(action='skipped')} Non-interactive mode is set, ignoring --dry-run option."
)
dry_run = False # override dry run to false in non-interactive mode
elif dry_run is None: # if dry-run option is not set explicitly
dry_run = typer.confirm("Dry Run?", default=True)

device = connect_to_host(
hostname=hostname,
username=username,
password=password,
settings_file=SETTINGS_FILE,
settings_file_path=SETTINGS_FILE_PATH,
)

# Exit script if device is Firewall (batch upgrade is only supported when connecting to Panorama)
Expand Down Expand Up @@ -711,9 +753,10 @@ def batch(

# If inventory.yaml does not exist, then prompt the user to select devices
else:
# Present a table of firewalls with detailed system information for selection
# Present a table of firewalls with detailed system information for selection - skipped in non interactive mode
user_selected_hostnames = select_devices_from_table(
firewall_mapping=firewall_mapping
) if not non_interactive else []
)

# Extracting the Firewall objects from the filtered mapping
Expand Down Expand Up @@ -747,8 +790,13 @@ def batch(
f"{get_emoji(action='report')} {hostname}: Please confirm the selected firewalls:\n{firewall_list}"
)

# Asking for user confirmation before proceeding
if dry_run:
# Proceed with upgrade if in non-interactive mode otherwise ask for user confirmation before proceeding
if non_interactive:
typer.echo(
f"{get_emoji(action='warning')} {hostname}: Non interactive mode is enabled, upgrade workflow will be executed without confirmation."
)
confirmation = True
elif dry_run:
typer.echo(
f"{get_emoji(action='warning')} {hostname}: Dry run mode is enabled, upgrade workflow will be skipped."
)
Expand Down Expand Up @@ -927,12 +975,15 @@ def inventory(
banner = console_welcome_banner(mode="inventory")
typer.echo(banner)

device = common_setup(
common_setup(
settings_file=SETTINGS_FILE,
settings_file_path=SETTINGS_FILE_PATH,
)

device = connect_to_host(
hostname=hostname,
username=username,
password=password,
settings_file=SETTINGS_FILE,
settings_file_path=SETTINGS_FILE_PATH,
)

if type(device) is Firewall:
Expand Down

0 comments on commit b19ddf6

Please sign in to comment.