Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3981b20
Remove dead code
stevekeay Nov 1, 2024
bf0c74b
Base Mock on original class to satisfy type checker
stevekeay Nov 1, 2024
043e44a
Add explicit return to give a consistent return type
stevekeay Nov 1, 2024
073edce
Add null value checks to avoid pyright warning and runtime errors
stevekeay Nov 1, 2024
4ce1236
Fix types in method signature to match implementation
stevekeay Nov 1, 2024
0ae6957
Cast to string to avoid pyright false positive
stevekeay Nov 1, 2024
7d6cab8
Catch null value to avoid pyright error and possible runtime error
stevekeay Nov 1, 2024
7ba03a4
Refactor code to avoid typing issues
stevekeay Nov 1, 2024
cdca9a5
Update type signature to match implementation
stevekeay Nov 1, 2024
cecdd92
Supply missing argument to avoid linter warning
stevekeay Nov 1, 2024
b8ae303
Refactor code for readability and to avoid type issues
stevekeay Nov 1, 2024
8197d4d
Add null checks to satisfy type checker and avoid runtime errors
stevekeay Nov 1, 2024
fcecd19
Refactor code to avoid type errors and avoid naming clash
stevekeay Nov 1, 2024
9894847
Cast to string to avoid typeing issue
stevekeay Nov 1, 2024
587fcfd
Suppress pyright checking for imports that don't play nicely
stevekeay Nov 1, 2024
d82c2e5
Add checks, casts and update signatures to avoid typing errors
stevekeay Nov 1, 2024
b6d7cd5
Allow for null Interface attributes
stevekeay Nov 1, 2024
1e1c0b4
Fail hard if there is a missing mac_addr in HP payload
stevekeay Nov 1, 2024
a8f3252
Fix code formatting
stevekeay Nov 1, 2024
ca4b15b
Get a None rather than empty list when there are no ip addresses
stevekeay Nov 1, 2024
eecc3ca
Fix types of newly-added fixtures
stevekeay Nov 8, 2024
1719ad5
Add pyright type checking for understack-workflows python code
stevekeay Oct 31, 2024
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
16 changes: 16 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,19 @@ repos:
rev: 38.114.0
hooks:
- id: renovate-config-validator
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.387
hooks:
- id: pyright
files: '^python/understack-workflows/'
args: ["--threads"]
additional_dependencies:
- "kubernetes"
- "pydantic"
- "pynautobot"
- "pytest"
- "pytest_lazy_fixtures"
- "python-ironicclient"
- "requests"
- "sushy"
- "types-requests"
8 changes: 4 additions & 4 deletions python/understack-workflows/tests/fixture_nautobot_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
description="Integrated NIC 1 Port 1",
mac_address="D4:04:E6:4F:8D:B4",
status="Active",
ip_address=[],
ip_address=None,
neighbor_device_id="275ef491-2b27-4d1b-bd45-330bd6b7e0cf",
neighbor_device_name="f20-2-1.iad3.rackspace.net",
neighbor_interface_id="f9a5cc87-d10a-4827-99e8-48961fd1d773",
Expand All @@ -32,7 +32,7 @@
description="Integrated NIC 1 Port 2",
mac_address="D4:04:E6:4F:8D:B5",
status="Active",
ip_address=[],
ip_address=None,
neighbor_device_id="05f6715a-4dbe-4fd6-af20-1e73adb285c2",
neighbor_device_name="f20-2-2.iad3.rackspace.net",
neighbor_interface_id="2148cf50-f70e-42c9-9f68-8ce98d61498c",
Expand All @@ -48,7 +48,7 @@
description="NIC in Slot 1 Port 1",
mac_address="14:23:F3:F5:25:F0",
status="Active",
ip_address=[],
ip_address=None,
neighbor_device_id="05f6715a-4dbe-4fd6-af20-1e73adb285c2",
neighbor_device_name="f20-2-2.iad3.rackspace.net",
neighbor_interface_id="f72bb830-3f3c-4aba-b7d5-9680ea4d358e",
Expand All @@ -64,7 +64,7 @@
description="NIC in Slot 1 Port 2",
mac_address="14:23:F3:F5:25:F1",
status="Active",
ip_address=[],
ip_address=None,
neighbor_device_id="275ef491-2b27-4d1b-bd45-330bd6b7e0cf",
neighbor_device_name="f20-2-1.iad3.rackspace.net",
neighbor_interface_id="c210be75-1038-4ba3-9923-60050e1c5362",
Expand Down
14 changes: 3 additions & 11 deletions python/understack-workflows/tests/test_bmc_chassis_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,19 @@
from ipaddress import IPv4Interface

from understack_workflows import bmc_chassis_info
from understack_workflows.bmc import Bmc

FIXTURE_PATH = "json_samples/bmc_chassis_info"


class FakeBmc:
class FakeBmc(Bmc):
def __init__(self, fixtures):
self.fixtures = fixtures
self.ip_address = "1.2.3.4"

def redfish_request(self, path: str) -> dict:
def redfish_request(self, path: str, *_args, **_kw) -> dict:
path = path.replace("/", "_") + ".json"
return self.fixtures[path]


def redfish_fixtures_by_platform() -> dict:
return {
platform: read_fixtures(FIXTURE_PATH.joinpath(platform))
for platform in sorted(os.listdir(FIXTURE_PATH))
}


def read_fixtures(path) -> dict:
path = pathlib.Path(__file__).parent.joinpath(path)
return {
Expand Down
4 changes: 3 additions & 1 deletion python/understack-workflows/understack_workflows/bmc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from understack_workflows.bmc_password_standard import standard_password
from understack_workflows.helpers import credential

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # type: ignore
logging.getLogger("urllib3").setLevel(logging.WARNING)

HEADERS = {
Expand Down Expand Up @@ -58,6 +58,8 @@ def redfish_request(
)
if r.text:
return r.json()
else:
return {}

def sushy(self):
return Sushy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from understack_workflows.helpers import setup_logger

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # type: ignore

FACTORY_PASSWORD = "calvin"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ def bmc_set_permanent_ip_addr(bmc: Bmc, interface_info: InterfaceInfo):
logger.info("BMC interface was not set to DHCP")
return

if not (interface_info.ipv4_address and interface_info.ipv4_gateway):
raise ValueError("BMC InterfaceInfo has missing IP information")

payload = {
"Attributes": {
"IPv4.1.DHCPEnable": "Disabled",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def delete_port(self, port_id: str):
port_id,
)

def list_ports(self, node_id: dict):
def list_ports(self, node_id: str):
self._ensure_logged_in()

return self.client.port.list(node=node_id, detail=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def create_or_update(
ironic_node = create_ironic_node(
client, node_uuid, device_hostname, driver, bmc
)
return ironic_node.provision_state # type: ignore

if ironic_node.provision_state in STATES_ALLOWING_UPDATES:
update_ironic_node(client, node_uuid, device_hostname, driver, bmc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from understack_workflows.bmc_password_standard import standard_password


def main(program_name, bmc_ip_address=None):
def main(program_name: str, bmc_ip_address: str | None = None):
"""CLI script to obtain standard BMC Password.

Requires the master secret to be available in BMC_MASTER environment
Expand All @@ -13,12 +13,11 @@ def main(program_name, bmc_ip_address=None):
if bmc_ip_address is None:
print(f"Usage: {program_name} <BMC IP Address>", file=sys.stderr)
exit(1)

if os.getenv("BMC_MASTER") is None:
print("Please set the BMC_MASTER environment variable", file=sys.stderr)
exit(1)

password = standard_password(bmc_ip_address, os.getenv("BMC_MASTER"))
password = standard_password(str(bmc_ip_address), str(os.getenv("BMC_MASTER")))
print(password)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def is_valid_domain(
) -> bool:
if only_domain is None:
return True
project = conn.identity.get_project(project_id.hex)
project = conn.identity.get_project(project_id.hex) # type: ignore
ret = project.domain_id == only_domain.hex
if not ret:
logger.info(
Expand All @@ -65,22 +65,22 @@ def is_valid_domain(

def handle_project_create(conn: Connection, nautobot: Nautobot, project_id: uuid.UUID):
logger.info(f"got request to create tenant {project_id!s}")
project = conn.identity.get_project(project_id.hex)
project = conn.identity.get_project(project_id.hex) # type: ignore
ten_api = nautobot.session.tenancy.tenants
ten_api.url = f"{ten_api.base_url}/plugins/uuid-api-endpoints/tenant"
ten = ten_api.create(
id=str(project_id), name=project.name, description=project.description
)
logger.info(f"tenant '{project_id!s}' created {ten.created}")
logger.info(f"tenant '{project_id!s}' created {ten.created}") # type: ignore


def handle_project_update(conn: Connection, nautobot: Nautobot, project_id: uuid.UUID):
logger.info(f"got request to update tenant {project_id!s}")
project = conn.identity.get_project(project_id.hex)
project = conn.identity.get_project(project_id.hex) # type: ignore
ten = nautobot.session.tenancy.tenants.get(project_id)
ten.description = project.description
ten.save()
logger.info(f"tenant '{project_id!s}' last updated {ten.last_updated}")
ten.description = project.description # type: ignore
ten.save() # type: ignore
logger.info(f"tenant '{project_id!s}' last updated {ten.last_updated}") # type: ignore


def handle_project_delete(conn: Connection, nautobot: Nautobot, project_id: uuid.UUID):
Expand All @@ -89,7 +89,7 @@ def handle_project_delete(conn: Connection, nautobot: Nautobot, project_id: uuid
if not ten:
logger.warn(f"tenant '{project_id!s}' does not exist already")
return
ten.delete()
ten.delete() # type: ignore
logger.info(f"deleted tenant {project_id!s}")


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def update_nautobot_for_provisioning(
interface = nautobot.update_switch_interface_status(
device_id, interface_mac, new_status
)
if not interface.device:
raise Exception("Interface has no associated device")
vlan_group_id = vlan_group_id_for(interface.device.id, nautobot)
logger.debug(
f"Switch interface {interface.device} {interface} found in {vlan_group_id=}"
Expand Down
59 changes: 32 additions & 27 deletions python/understack-workflows/understack_workflows/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,26 @@ class NIC:
@classmethod
def from_redfish(cls, data: NetworkAdapter) -> NIC:
location = cls.nic_location(data)
nic = cls(data.identity, location, [], data.model)
nic = cls(data.identity, location, [], data.model) # type: ignore
nic.interfaces = [Interface.from_redfish(i, nic) for i in cls.nic_ports(data)]
return nic

@classmethod
def from_hp_json(cls, data: dict) -> NIC:
nic = cls(data.get("name"), data.get("location"), [], data.get("name"))
ports = data.get("network_ports") or data.get("unknown_ports")
nic = cls(data["name"], data["location"], [], data["name"])
ports = data.get("network_ports") or data.get("unknown_ports", [])
nic.interfaces = [Interface.from_hp_json(i, nic, ports) for i in ports]
return nic

@classmethod
def nic_location(cls, nic: NetworkAdapter) -> str:
try:
return nic.json["Controllers"][0]["Location"]["PartLocation"][
"ServiceLabel"
]
except KeyError:
return nic.identity
json = nic.json or {}
controller = json.get("Controllers", [])[0] or {}
return (
controller.get("Location", {})
.get("PartLocation", {})
.get("ServiceLabel", nic.identity)
)

@classmethod
def nic_ports(cls, nic: NetworkAdapter) -> list[NetworkPort]:
Expand All @@ -58,17 +59,17 @@ class Interface:

@classmethod
def from_redfish(cls, data: NetworkPort, nic: NIC) -> Interface:
if data.root.json["Vendor"] == "HPE":
if data.root and data.root.json["Vendor"] == "HPE":
name = f"{nic.name}_{data.physical_port_number}"
macaddr = data.associated_network_addresses[0]
macaddr = data.associated_network_addresses[0] # type: ignore
else:
name = data.identity
macaddr = cls.fetch_macaddr_from_sys_resource(data)
return cls(
name,
name, # type: ignore
macaddr,
nic.location,
data.current_link_speed_mbps,
data.current_link_speed_mbps, # type: ignore
nic.model,
)

Expand All @@ -78,7 +79,7 @@ def from_hp_json(cls, data: dict, nic: NIC, ports: list) -> Interface:
interface_name = f"NIC.{nic.location.replace(' ', '.')}_{p_num}"
return cls(
interface_name,
data.get("mac_addr"),
data["mac_addr"],
nic.location,
data.get("speed", 0),
nic.model,
Expand All @@ -87,9 +88,9 @@ def from_hp_json(cls, data: dict, nic: NIC, ports: list) -> Interface:
@classmethod
def fetch_macaddr_from_sys_resource(cls, data: NetworkPort) -> str:
try:
path = f"{data.root.get_system().ethernet_interfaces.path}/{data.identity}"
path = f"{data.root.get_system().ethernet_interfaces.path}/{data.identity}" # type: ignore
macaddr = (
data.root.get_system().ethernet_interfaces.get_member(path).mac_address
data.root.get_system().ethernet_interfaces.get_member(path).mac_address # type: ignore
)
except ResourceNotFoundError:
macaddr = ""
Expand All @@ -116,7 +117,7 @@ class Chassis:
name: str
nics: list[NIC]
network_interfaces: list[Interface]
system_info: Systeminfo
system_info: Systeminfo | None

@classmethod
def check_manufacturer(cls, manufacturer: str) -> None:
Expand All @@ -137,28 +138,31 @@ def bmc_is_ilo4(cls, chassis_data: SushyChassis) -> bool:
@classmethod
def from_redfish(cls, oob_obj: Sushy) -> Chassis:
chassis_data = oob_obj.get_chassis(
oob_obj.get_chassis_collection().members_identities[0]
oob_obj.get_chassis_collection().members_identities[0] # type: ignore
)

cls.check_manufacturer(chassis_data.manufacturer)
cls.check_manufacturer(chassis_data.manufacturer) # type: ignore

if cls.bmc_is_ilo4(chassis_data):
return cls.from_hp_json(oob_obj, chassis_data.name)
return cls.from_hp_json(oob_obj, chassis_data.name) # type: ignore

chassis = cls(chassis_data.name, [], [], [])
chassis.nics = [
nics = [
NIC.from_redfish(i) for i in chassis_data.network_adapters.get_members()
]
chassis.network_interfaces = cls.interfaces_from_nics(chassis.nics)
chassis.system_info = Systeminfo.from_redfish(chassis_data)
return chassis

return cls(
name=chassis_data.name, # type: ignore
nics=nics,
network_interfaces=cls.interfaces_from_nics(nics),
system_info=Systeminfo.from_redfish(chassis_data),
)

@classmethod
def from_hp_json(cls, oob_obj: Sushy, chassis_name: str) -> Chassis:
data = cls.chassis_hp_json_data(oob_obj)
nics = [NIC.from_hp_json(i) for i in data]
network_interfaces = cls.interfaces_from_nics(nics)
return cls(chassis_name, nics, network_interfaces)
return cls(chassis_name, nics, network_interfaces, None)

@classmethod
def interfaces_from_nics(cls, nics: list[NIC]) -> list[Interface]:
Expand All @@ -167,7 +171,8 @@ def interfaces_from_nics(cls, nics: list[NIC]) -> list[Interface]:
@classmethod
def chassis_hp_json_data(cls, oob_obj: Sushy) -> dict:
oob_obj._conn.set_http_basic_auth(
username=oob_obj._auth._username, password=oob_obj._auth._password
username=oob_obj._auth._username, # type: ignore
password=oob_obj._auth._password, # type: ignore
)
resp = oob_obj._conn.get(path="/json/comm_controller_info")
resp.raise_for_status()
Expand Down
Loading