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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions components/ironic/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ conf:
enabled_console_interfaces: redfish-graphical,no-console
enabled_deploy_interfaces: direct,ramdisk,noop
enabled_firmware_interfaces: redfish,no-firmware
enabled_inspect_interfaces: redfish,idrac-redfish,agent,no-inspect
enabled_management_interfaces: redfish,idrac-redfish,noop
enabled_inspect_interfaces: redfish,idrac-redfish,agent,panos,no-inspect
enabled_management_interfaces: redfish,idrac-redfish,panos,noop
enabled_network_interfaces: neutron,noop
# remove fake once there is a noop that the ManualManagement uses, which netdev is derived from
enabled_power_interfaces: redfish,idrac-redfish,fake
Expand Down
27 changes: 27 additions & 0 deletions docs/design-guide/ironic.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,30 @@ bindings.

To use it, add `netdev` to `enabled_hardware_types` in `ironic.conf` and
ensure `noop` deploy and `neutron` network interfaces are also enabled.

### panos

The `panos` hardware type is a `netdev` for PAN-OS (Palo Alto) firewalls, so
these appliances can be told apart by their `driver`. It replaces the `netdev`
inspect and management interfaces with `panos` ones, which talk to the
appliance's XML API at `driver_info['management_ip']`. The no-op interfaces
remain available as fallbacks.

- **inspect** logs in and records the model and serial (only when enroll-fw
hasn't already set them) and `firmware_version`. LLDP-based port
discovery and HA peer discovery are not implemented yet.
- **management** has a `setup_initial_configuration` verify step, which
currently only logs, and the manual clean steps `reset_to_factory_defaults`
and `clear_configuration`. Those clean steps fail with "not implemented"
instead of reporting success.

Credentials come from the `[netdev_panos]` oslo.config group. Login tries the
standard password first, then preconfig, then factory, so it works with
appliances at any point in their setup. `[netdev_panos] verify_ssl`
(default off) controls TLS verification.

To use it, add `panos` to `enabled_hardware_types`, `enabled_inspect_interfaces`,
and `enabled_management_interfaces`. Nodes enrolled before the `panos`
interfaces existed keep their stored `no-inspect` and `noop` interfaces until
they are switched with
`openstack baremetal node set --inspect-interface panos --management-interface panos`.
6 changes: 6 additions & 0 deletions python/ironic-understack/ironic_understack/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ def setup_conf():
help="PAN-OS factory password",
),
cfg.StrOpt("panorama_master_key", secret=True, help="Panorama master key"),
cfg.BoolOpt(
"verify_ssl",
default=False,
help="Verify the TLS certificate of the PAN-OS XML API. Off by "
"default because appliances ship with self-signed certificates.",
),
]
panos_grp = cfg.OptGroup("netdev_panos", title="PAN-OS Credentials")
cfg.CONF.register_group(panos_grp)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
from ironic.drivers.modules import noop
from ironic.drivers.modules import noop_mgmt

from ironic_understack.drivers.netdev_hardware import NetdevHardware
from ironic_understack.drivers.panos_inspect import PanosInspect
from ironic_understack.drivers.panos_management import PanosManagement


class PanosHardware(NetdevHardware):
"""Hardware type for PAN-OS (Palo Alto) network appliances.

Behaves exactly like :class:`NetdevHardware` today. It exists as a distinct
hardware type so a PAN-OS appliance is identifiable from the node's
``driver`` alone.
A :class:`NetdevHardware` with PAN-OS inspect and management interfaces,
which talk to the appliance over its XML API. The netdev no-op interfaces
stay supported as fallbacks.
"""

@property
def supported_inspect_interfaces(self):
return [PanosInspect, noop.NoInspect]

@property
def supported_management_interfaces(self):
return [PanosManagement, noop_mgmt.NoopManagement]
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Inspect interface for PAN-OS (Palo Alto) appliances."""

from typing import ClassVar

import requests
from ironic.common import exception
from ironic.common import states
from ironic.drivers import base
from oslo_log import log

from ironic_understack.utils import panos_api

LOG = log.getLogger(__name__)


class PanosInspect(base.InspectInterface):
"""Inspect a PAN-OS appliance over its XML API.

No ramdisk is involved; everything goes over the management address in
``driver_info['management_ip']``, which enroll-fw sets.
"""

# A firewall has no memory_mb/local_gb/cpu_arch to report.
ESSENTIAL_PROPERTIES: ClassVar[set] = set()

def get_properties(self):
return {
"management_ip": "Address of the appliance's management interface, "
"used for the XML API. Required.",
}

def validate(self, task):
if not task.node.driver_info.get("management_ip"):
raise exception.MissingParameterValue(
f"Node {task.node.uuid} is missing driver_info management_ip"
)

def inspect_hardware(self, task):
node = task.node
address = node.driver_info["management_ip"]
client = panos_api.PanosClient(address)
try:
client.login()
LOG.debug("[node:%s] Logged in to %s", node.uuid, address)
system = client.system_info()
except (panos_api.PanosApiError, requests.RequestException) as e:
raise exception.HardwareInspectionFailure(
error=f"PAN-OS {address}: {e}"
) from e

_apply_system_info(node, system)

# TODO: discover data plane connectivity and create ports from it:
# - show interface all, for the data plane interfaces and their MACs
# - LLDP neighbors. Interfaces without config don't transmit LLDP, so
# this needs to enable LLDP and a transmit-receive profile on them,
# commit, wait for neighbors, collect them, then restore the
# original config and commit again.
# - an Ironic port per neighbor named "<node name>:<interface>", with
# local_link_connection from the neighbor and the remote interface
# as extra["bios_name"]
# TODO: show high-availability all, recording the HA peer serial in
# extra["mate_serial"] (which enroll-fw also sets) when HA is enabled.

node.save()
return states.MANAGEABLE


def _apply_system_info(node, system: dict[str, str]) -> None:
"""Record what ``show system info`` reported on the node.

model and serial are also set by enroll-fw from Nautobot, so those are only
filled in when missing; a disagreement is logged rather than overwritten.
"""
properties = dict(node.properties)
extra = dict(node.extra)
for target, key, field in (
(properties, "model", "model"),
(extra, "serial", "serial"),
):
found = system.get(field)
if not found:
continue
if not target.get(key):
target[key] = found
elif target[key] != found:
LOG.warning(
"[node:%s] Appliance reports %s %r but the node has %r",
node.uuid,
key,
found,
target[key],
)
if system.get("sw-version"):
properties["firmware_version"] = system["sw-version"]
node.properties = properties
node.extra = extra
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Management interface for PAN-OS (Palo Alto) appliances."""

from ironic.common import exception
from ironic.drivers import base
from ironic.drivers.modules import noop_mgmt
from oslo_log import log

LOG = log.getLogger(__name__)


class PanosManagement(noop_mgmt.NoopManagement):
"""Lifecycle steps for PAN-OS appliances.

Boot device methods don't apply to a firewall, so those are inherited from
NoopManagement.
"""

def get_properties(self):
return {
"management_ip": "Address of the appliance's management interface, "
"used for the XML API. Required.",
}

def validate(self, task):
if not task.node.driver_info.get("management_ip"):
raise exception.MissingParameterValue(
f"Node {task.node.uuid} is missing driver_info management_ip"
)

@base.verify_step(priority=10)
def setup_initial_configuration(self, task):
"""First-time setup of a factory-fresh appliance.

Runs on enroll -> manageable. It must be idempotent and skip an
appliance that has already been set up. Planned steps, committed once
at the end:

- management interface address, netmask, and gateway
- remove the factory defaults that conflict with production use: the
rule1 security rule, the trust and untrust zones, default-vwire, and
the ethernet1/1 and ethernet1/2 config
- jumbo frames
- admin credentials, moving from the factory to the standard password
- hostname from the node name
"""
# TODO: implement. Left as a no-op so enrollment isn't blocked on it.
LOG.warning(
"[node:%s] PAN-OS initial configuration is not implemented yet, skipping",
task.node.uuid,
)

@base.clean_step(priority=0, requires_ramdisk=False)
def reset_to_factory_defaults(self, task):
"""Factory reset the appliance, then rerun initial configuration."""
# TODO: implement. Fails rather than reporting a reset that didn't happen.
raise exception.NodeCleaningFailure(
node=task.node.uuid, reason="PAN-OS factory reset is not implemented yet"
)

@base.clean_step(priority=0, requires_ramdisk=False)
def clear_configuration(self, task):
"""Remove tenant configuration, without a full factory reset.

Removes security policies, NAT rules, and custom zones, interfaces,
and routing. Keeps the management interface, admin credentials, and
basic system settings.
"""
# TODO: implement. Fails rather than reporting a clean that didn't happen.
raise exception.NodeCleaningFailure(
node=task.node.uuid,
reason="PAN-OS configuration cleanup is not implemented yet",
)
Loading
Loading