Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions panos/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@
logger = getlogger(__name__)


def _maintenance_num(version):
"""Hotfix number for a version. A base image such as 10.1.13 ranks as 0."""
if version.subrelease_num is None:
return 0
return version.subrelease_num


def _base_image_preferred(candidate, incumbent):
"""True when candidate should replace an equal-patch incumbent.

PAN-OS will not load 10.2.0-h3 until the 10.2.0 base image is downloaded,
so the base image is the right next hop when both are published.
"""
candidate_is_base = candidate.subrelease_type != "h"
incumbent_is_base = incumbent.subrelease_type != "h"
if candidate_is_base != incumbent_is_base:
return candidate_is_base
return _maintenance_num(candidate) < _maintenance_num(incumbent)


class Updater(object):
"""This class is instantiated by the PanDevice class as a software update subsystem"""

Expand Down Expand Up @@ -418,7 +438,7 @@ def _next_minor_version(self, version, available_version_list):

# Initial values before iterating over list of available versions
next_minor_found = False
next_minor_patch_number = 999
next_version = None

# Create a map or PanOSVersion objects to represent all the
# available versions the device has reported, and iterate
Expand All @@ -432,10 +452,16 @@ def _next_minor_version(self, version, available_version_list):
# Examples: 9.1 from 9.0, 10.2 from 10.1
next_minor_found = True

if available_version.patch < next_minor_patch_number:
# Looking for the lowest patch version of this newer
# minor version
next_minor_patch_number = available_version.patch
if (
next_version is None
or available_version.patch < next_version.patch
or (
available_version.patch == next_version.patch
and _base_image_preferred(available_version, next_version)
)
):
# Lowest patch of the next minor. On that patch, the base
# image (10.2.0) must be chosen ahead of a hotfix (10.2.0-h3).
next_version = available_version

if not next_minor_found:
Expand Down Expand Up @@ -516,7 +542,8 @@ def _direct_upgrade_possible(
current_version.patch < target_version.patch
or (
current_version.patch == target_version.patch
and current_version.subrelease_num < target_version.subrelease_num
and _maintenance_num(current_version)
< _maintenance_num(target_version)
)
)
):
Expand Down
26 changes: 26 additions & 0 deletions tests/test_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,32 @@ def test__direct_upgrade_possible_firewall_valid_expected_next_versions(
)


def test_direct_upgrade_from_base_release_to_hotfix():
"""Issue #565: 10.1.13 -> 10.1.13-h1 used to raise TypeError."""
fw = _fw()
assert fw.software._direct_upgrade_possible(
"10.1.13", "10.1.13-h1", ["10.1.13", "10.1.13-h1"]
)
assert fw.software._direct_upgrade_possible(
"10.1.13-h1", "10.1.13-h3", ["10.1.13-h1", "10.1.13-h3"]
)
assert not fw.software._direct_upgrade_possible(
"10.1.13-h1", "10.1.13", ["10.1.13", "10.1.13-h1"]
)


def test_next_minor_prefers_base_image_over_hotfix():
"""Issue #572: 10.2.0-h3 was selected even though 10.2.0 was published."""
fw = _fw()
versions = ["10.2.0-h3", "10.2.4", "10.2.0", "10.1.16"]
nxt = fw.software._next_minor_version(PanOSVersion("10.1.16"), versions)
assert str(nxt) == "10.2.0"

only_hotfix = ["10.2.0-h3", "10.1.16"]
nxt = fw.software._next_minor_version(PanOSVersion("10.1.16"), only_hotfix)
assert str(nxt) == "10.2.0-h3"


# Test invalid direct upgrades return False
@pytest.mark.parametrize(
"input_version, target_version", firewall_invalid_direct_upgrade_paths
Expand Down