diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ca26115ce..866259c11 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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" diff --git a/python/understack-workflows/tests/fixture_nautobot_device.py b/python/understack-workflows/tests/fixture_nautobot_device.py index 86addd25b..38c38cc95 100644 --- a/python/understack-workflows/tests/fixture_nautobot_device.py +++ b/python/understack-workflows/tests/fixture_nautobot_device.py @@ -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", @@ -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", @@ -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", @@ -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", diff --git a/python/understack-workflows/tests/test_bmc_chassis_info.py b/python/understack-workflows/tests/test_bmc_chassis_info.py index 7160b545a..9cb7796eb 100644 --- a/python/understack-workflows/tests/test_bmc_chassis_info.py +++ b/python/understack-workflows/tests/test_bmc_chassis_info.py @@ -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 { diff --git a/python/understack-workflows/understack_workflows/bmc.py b/python/understack-workflows/understack_workflows/bmc.py index a12bb8a55..9cf703d7c 100644 --- a/python/understack-workflows/understack_workflows/bmc.py +++ b/python/understack-workflows/understack_workflows/bmc.py @@ -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 = { @@ -58,6 +58,8 @@ def redfish_request( ) if r.text: return r.json() + else: + return {} def sushy(self): return Sushy( diff --git a/python/understack-workflows/understack_workflows/bmc_credentials.py b/python/understack-workflows/understack_workflows/bmc_credentials.py index 31de58ce7..03ec0b6d3 100644 --- a/python/understack-workflows/understack_workflows/bmc_credentials.py +++ b/python/understack-workflows/understack_workflows/bmc_credentials.py @@ -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" diff --git a/python/understack-workflows/understack_workflows/bmc_network_config.py b/python/understack-workflows/understack_workflows/bmc_network_config.py index 778702e73..d3a013d22 100644 --- a/python/understack-workflows/understack_workflows/bmc_network_config.py +++ b/python/understack-workflows/understack_workflows/bmc_network_config.py @@ -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", diff --git a/python/understack-workflows/understack_workflows/ironic/client.py b/python/understack-workflows/understack_workflows/ironic/client.py index ddcc4abf4..227587378 100644 --- a/python/understack-workflows/understack_workflows/ironic/client.py +++ b/python/understack-workflows/understack_workflows/ironic/client.py @@ -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) diff --git a/python/understack-workflows/understack_workflows/ironic_node.py b/python/understack-workflows/understack_workflows/ironic_node.py index 78e48d964..47e6c8526 100644 --- a/python/understack-workflows/understack_workflows/ironic_node.py +++ b/python/understack-workflows/understack_workflows/ironic_node.py @@ -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) diff --git a/python/understack-workflows/understack_workflows/main/print_bmc_password.py b/python/understack-workflows/understack_workflows/main/print_bmc_password.py index c74f3edb8..3f189fd6c 100755 --- a/python/understack-workflows/understack_workflows/main/print_bmc_password.py +++ b/python/understack-workflows/understack_workflows/main/print_bmc_password.py @@ -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 @@ -13,12 +13,11 @@ def main(program_name, bmc_ip_address=None): if bmc_ip_address is None: print(f"Usage: {program_name} ", 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) diff --git a/python/understack-workflows/understack_workflows/main/sync_keystone.py b/python/understack-workflows/understack_workflows/main/sync_keystone.py index 8f91d455b..a18865a2f 100644 --- a/python/understack-workflows/understack_workflows/main/sync_keystone.py +++ b/python/understack-workflows/understack_workflows/main/sync_keystone.py @@ -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( @@ -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): @@ -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}") diff --git a/python/understack-workflows/understack_workflows/main/undersync_device.py b/python/understack-workflows/understack_workflows/main/undersync_device.py index e01f94cde..6c0758da7 100644 --- a/python/understack-workflows/understack_workflows/main/undersync_device.py +++ b/python/understack-workflows/understack_workflows/main/undersync_device.py @@ -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=}" diff --git a/python/understack-workflows/understack_workflows/models.py b/python/understack-workflows/understack_workflows/models.py index 298c2a586..5b91982e7 100644 --- a/python/understack-workflows/understack_workflows/models.py +++ b/python/understack-workflows/understack_workflows/models.py @@ -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]: @@ -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, ) @@ -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, @@ -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 = "" @@ -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: @@ -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]: @@ -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() diff --git a/python/understack-workflows/understack_workflows/nautobot.py b/python/understack-workflows/understack_workflows/nautobot.py index 3ab7174c1..ae48a3074 100644 --- a/python/understack-workflows/understack_workflows/nautobot.py +++ b/python/understack-workflows/understack_workflows/nautobot.py @@ -4,8 +4,7 @@ import pynautobot from pynautobot.core.api import Api as NautobotApi -from pynautobot.models.dcim import Devices as NautobotDevice -from pynautobot.models.dcim import Interfaces as NautobotInterface +from pynautobot.models import dcim class Nautobot: @@ -23,34 +22,38 @@ def exit_with_error(self, error): def api_session(self, url: str, token: str) -> NautobotApi: return pynautobot.api(url, token=token) - def device_by_id(self, device_id: UUID) -> NautobotDevice: + def device_by_id(self, device_id: UUID) -> dcim.Devices: device = self.session.dcim.devices.get(device_id) if not device: self.exit_with_error(f"Device {device_id!s} not found in Nautobot") - return device + return device # type: ignore - def interface_by_id(self, interface_id: UUID) -> NautobotInterface: + def interface_by_id(self, interface_id: UUID) -> dcim.Interfaces: interface = self.session.dcim.interfaces.get(interface_id) if not interface: self.exit_with_error(f"Interface {interface_id!s} not found in Nautobot") - return interface + return interface # type: ignore def non_lag_interface_by_mac( self, device_id: UUID, mac_address: str - ) -> list[NautobotInterface]: - interfaces = self.session.dcim.interfaces.filter( + ) -> dcim.Interfaces: + interface = self.session.dcim.interfaces.get( device_id=device_id, mac_address=mac_address, type__n="lag", ) - if not interfaces: + if not interface: self.exit_with_error( f"Interface with {device_id=} and {mac_address=} not found in Nautobot" ) - return interfaces[0] + return interface # type: ignore def update_cf(self, device_id: UUID, field_name: str, field_value: str): device = self.device_by_id(device_id) + if not device: + raise Exception(f"No such device {device_id}") + if not device.custom_fields: + raise Exception(f"Device {device_id} has no custom fields") device.custom_fields[field_name] = field_value response = device.save() self.logger.info(f"save result: {response}") @@ -58,7 +61,7 @@ def update_cf(self, device_id: UUID, field_name: str, field_value: str): def update_switch_interface_status( self, device_id: UUID, server_interface_mac: str, new_status: str - ) -> NautobotInterface: + ) -> dcim.Interfaces: """Change the Interface Status in Nautobot for interfaces. The device_id and interface MAC address parameters identify one or more @@ -82,13 +85,13 @@ def update_switch_interface_status( f"Interface {server_interface_mac=} {server_interface.type} " "is not connected in Nautobot" ) - switch_interface_id = connected_endpoint.id + switch_interface_id = connected_endpoint.id # type: ignore self.logger.debug( f"Interface {server_interface_mac=} connects to {switch_interface_id=}" ) switch_interface = self.interface_by_id(switch_interface_id) - switch_interface.status = new_status + switch_interface.status = new_status # type: ignore result = switch_interface.save() self.logger.debug( @@ -98,7 +101,7 @@ def update_switch_interface_status( def update_device_status(self, device_id: UUID, device_status: str): device = self.device_by_id(device_id) - device.status = device_status + device.status = device_status # type: ignore result = device.save() self.logger.info( f"device {device_id} updated to Status {device_status} {result=}" diff --git a/python/understack-workflows/understack_workflows/nautobot_device.py b/python/understack-workflows/understack_workflows/nautobot_device.py index f75ac73ea..1694bc857 100644 --- a/python/understack-workflows/understack_workflows/nautobot_device.py +++ b/python/understack-workflows/understack_workflows/nautobot_device.py @@ -27,14 +27,14 @@ class NautobotInterface: description: str mac_address: str status: str - ip_address: str - neighbor_device_id: str - neighbor_device_name: str - neighbor_interface_id: str - neighbor_interface_name: str - neighbor_chassis_mac: str - neighbor_location_name: str - neighbor_rack_name: str + ip_address: str | None + neighbor_device_id: str | None + neighbor_device_name: str | None + neighbor_interface_id: str | None + neighbor_interface_name: str | None + neighbor_chassis_mac: str | None + neighbor_location_name: str | None + neighbor_rack_name: str | None @dataclass @@ -86,13 +86,18 @@ def find_or_create(chassis_info: ChassisInfo, nautobot) -> NautobotDevice: # Re-run the graphql query to fetch any auto-created defaults from # nautobot (e.g. it automatically creates a BMC interface): device = nautobot_server(nautobot, serial=chassis_info.serial_number) + if not device: + raise Exception("Failed to create device in Nautobot") find_or_create_interfaces(nautobot, chassis_info, device.id, switches) # Run the graphql query yet again, to include all the data we just populated # in nautobot. Fairly innefficient for the case where we didn't change # anything, but we need the accurate data. - return nautobot_server(nautobot, serial=chassis_info.serial_number) + device = nautobot_server(nautobot, serial=chassis_info.serial_number) + if not device: + raise Exception("Failed to create device in Nautobot") + return device def location_from(switches): @@ -119,7 +124,9 @@ def switches_for(nautobot, chassis_info: ChassisInfo) -> dict: if interface.remote_switch_mac_address } base_switch_macs = { - base_mac(interface.remote_switch_mac_address, interface.remote_switch_port_name) + base_mac( + interface.remote_switch_mac_address, str(interface.remote_switch_port_name) + ) for interface in chassis_info.interfaces if interface.remote_switch_mac_address } @@ -129,7 +136,7 @@ def switches_for(nautobot, chassis_info: ChassisInfo) -> dict: return switches -def nautobot_switches(nautobot, mac_addresses: list[str]) -> dict[str, dict]: +def nautobot_switches(nautobot, mac_addresses: set[str]) -> dict[str, dict]: """Get switches by MAC address. Assumes that MAC addresses in Nautobot are normalized to upcase @@ -273,7 +280,7 @@ def parse_device(data: dict) -> NautobotDevice: def parse_interface(data: dict) -> NautobotInterface: connected = data["connected_interface"] - ip_address = data["ip_addresses"] and data["ip_addresses"][0] + ip_address = data["ip_addresses"][0] if data["ip_addresses"] else None return NautobotInterface( id=data["id"], name=data["name"], @@ -380,7 +387,7 @@ def connect_interface_to_switch( if cable is None: try: cable = nautobot.dcim.cables.create(**identity, **attrs) - except pynautobot.core.query.RequestError as e: + except pynautobot.core.query.RequestError as e: # type: ignore raise Exception( f"Failed to create nautobot cable {identity}: {e}" ) from None @@ -407,7 +414,7 @@ def assign_ip_address(nautobot, nautobot_interface, ipv4_address: IPv4Interface, }, ) logger.info(f"Created Nautobot IP {ip.id} for {ipv4_address}") - except pynautobot.core.query.RequestError as e: + except pynautobot.core.query.RequestError as e: # type: ignore raise Exception(f"Failed to assign {ipv4_address=} in Nautobot: {e}") from None return ip @@ -423,7 +430,7 @@ def associate_ip_address(nautobot, nautobot_interface, ip_id): else: nautobot.ipam.ip_address_to_interface.create(**identity, is_primary=True) logger.info(f"Associated IP address {ip_id} with {nautobot_interface.name}") - except pynautobot.core.query.RequestError as e: + except pynautobot.core.query.RequestError as e: # type: ignore raise Exception( f"Failed to associate IPAddress {ip_id} in Nautobot: {e}" ) from None diff --git a/python/understack-workflows/understack_workflows/openstack/client.py b/python/understack-workflows/understack_workflows/openstack/client.py index b755ab062..3cf7bb81e 100644 --- a/python/understack-workflows/understack_workflows/openstack/client.py +++ b/python/understack-workflows/understack_workflows/openstack/client.py @@ -11,7 +11,7 @@ from openstack.connection import Connection try: - _pkg_ver = _meta.version(__package__.split(".")[0]) + _pkg_ver = _meta.version(str(__package__).split(".")[0]) except Exception: _pkg_ver = "dev" @@ -40,7 +40,7 @@ def get_openstack_client(cloud=None, region_name="") -> Connection: return Connection(config=cloud_region) -def get_ironic_client(cloud=None, region_name="") -> IronicClient: +def get_ironic_client(cloud=None, region_name="") -> IronicClient: # type: ignore """Returns our Ironic Client wrapper configured from our clouds.yaml.""" cloud_region = _get_os_cloud_region(cloud, region_name) client = _get_ironic_client(