From 7d61d5d5ff50f9a923f4412e6153e490549cecdb Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Tue, 10 Feb 2026 11:23:29 -0500 Subject: [PATCH] Defer import of system modules and Azure SDKs Fixes: - Resolve an issue where importing `VmState` in Robottelo could trigger errors due to the eager loading of all system provider modules and their heavy dependencies. This change allows `VmState` to be imported without pulling in unused provider SDKs. Refactoring: - Implement `__getattr__` in `wrapanapi/__init__.py` and `wrapanapi/systems/__init__.py` to lazily import system classes (e.g., `EC2System`, `VMWareSystem`). This defers the loading of module dependencies until a specific system class is accessed. - Introduce lazy importing for Azure SDK components within `wrapanapi/systems/msazure.py`. Azure dependencies are now loaded only when an Azure system or entity is instantiated, avoiding unnecessary module loading and potential import errors when the SDK is not present or needed. - Adjust a multi-line expression in `wrapanapi/systems/nuage.py` for improved readability. Configuration: - Update `ruff-pre-commit` to version `v0.15.0`. - Update `pyupgrade` to version `v3.21.2`. --- .pre-commit-config.yaml | 4 +- wrapanapi/__init__.py | 80 ++++++++++++++++++++++++++++------- wrapanapi/systems/__init__.py | 71 +++++++++++++++++++++++++------ wrapanapi/systems/msazure.py | 65 ++++++++++++++++++++++------ wrapanapi/systems/nuage.py | 8 ++-- 5 files changed, 181 insertions(+), 47 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c5f9c774..47890f9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.2 + rev: v0.15.0 hooks: # Run the linter. - id: ruff @@ -24,7 +24,7 @@ repos: - id: end-of-file-fixer - id: debug-statements - repo: https://github.com/asottile/pyupgrade - rev: v3.21.0 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py3-plus, --py311-plus] diff --git a/wrapanapi/__init__.py b/wrapanapi/__init__.py index c6c1db54..25c66b2b 100644 --- a/wrapanapi/__init__.py +++ b/wrapanapi/__init__.py @@ -1,20 +1,5 @@ # Imports for convenience from .entities.vm import VmState -from .systems.container.podman import Podman -from .systems.container.rhopenshift import Openshift -from .systems.ec2 import EC2System -from .systems.google import GoogleCloudSystem -from .systems.hawkular import HawkularSystem -from .systems.lenovo import LenovoSystem -from .systems.msazure import AzureSystem -from .systems.nuage import NuageSystem -from .systems.openstack import OpenstackSystem -from .systems.openstack_infra import OpenstackInfraSystem -from .systems.redfish import RedfishSystem -from .systems.rhevm import RHEVMSystem -from .systems.scvmm import SCVMMSystem -from .systems.vcloud import VmwareCloudSystem -from .systems.virtualcenter import VMWareSystem __all__ = [ "EC2System", @@ -34,3 +19,68 @@ "Podman", "VmState", ] + + +def __getattr__(name): + """Lazy import system classes to avoid loading dependencies for unused providers.""" + if name == "EC2System": + from .systems.ec2 import EC2System + + return EC2System + elif name == "GoogleCloudSystem": + from .systems.google import GoogleCloudSystem + + return GoogleCloudSystem + elif name == "HawkularSystem": + from .systems.hawkular import HawkularSystem + + return HawkularSystem + elif name == "LenovoSystem": + from .systems.lenovo import LenovoSystem + + return LenovoSystem + elif name == "AzureSystem": + from .systems.msazure import AzureSystem + + return AzureSystem + elif name == "NuageSystem": + from .systems.nuage import NuageSystem + + return NuageSystem + elif name == "OpenstackSystem": + from .systems.openstack import OpenstackSystem + + return OpenstackSystem + elif name == "OpenstackInfraSystem": + from .systems.openstack_infra import OpenstackInfraSystem + + return OpenstackInfraSystem + elif name == "RedfishSystem": + from .systems.redfish import RedfishSystem + + return RedfishSystem + elif name == "RHEVMSystem": + from .systems.rhevm import RHEVMSystem + + return RHEVMSystem + elif name == "SCVMMSystem": + from .systems.scvmm import SCVMMSystem + + return SCVMMSystem + elif name == "VmwareCloudSystem": + from .systems.vcloud import VmwareCloudSystem + + return VmwareCloudSystem + elif name == "VMWareSystem": + from .systems.virtualcenter import VMWareSystem + + return VMWareSystem + elif name == "Openshift": + from .systems.container.rhopenshift import Openshift + + return Openshift + elif name == "Podman": + from .systems.container.podman import Podman + + return Podman + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/wrapanapi/systems/__init__.py b/wrapanapi/systems/__init__.py index fa0adf4f..f6010866 100644 --- a/wrapanapi/systems/__init__.py +++ b/wrapanapi/systems/__init__.py @@ -1,17 +1,3 @@ -from .ec2 import EC2System -from .google import GoogleCloudSystem -from .hawkular import HawkularSystem -from .lenovo import LenovoSystem -from .msazure import AzureSystem -from .nuage import NuageSystem -from .openstack import OpenstackSystem -from .openstack_infra import OpenstackInfraSystem -from .redfish import RedfishSystem -from .rhevm import RHEVMSystem -from .scvmm import SCVMMSystem -from .vcloud import VmwareCloudSystem -from .virtualcenter import VMWareSystem - __all__ = [ "EC2System", "GoogleCloudSystem", @@ -27,3 +13,60 @@ "VmwareCloudSystem", "VMWareSystem", ] + + +def __getattr__(name): + """Lazy import system classes to avoid loading dependencies for unused providers.""" + if name == "EC2System": + from .ec2 import EC2System + + return EC2System + elif name == "GoogleCloudSystem": + from .google import GoogleCloudSystem + + return GoogleCloudSystem + elif name == "HawkularSystem": + from .hawkular import HawkularSystem + + return HawkularSystem + elif name == "LenovoSystem": + from .lenovo import LenovoSystem + + return LenovoSystem + elif name == "AzureSystem": + from .msazure import AzureSystem + + return AzureSystem + elif name == "NuageSystem": + from .nuage import NuageSystem + + return NuageSystem + elif name == "OpenstackSystem": + from .openstack import OpenstackSystem + + return OpenstackSystem + elif name == "OpenstackInfraSystem": + from .openstack_infra import OpenstackInfraSystem + + return OpenstackInfraSystem + elif name == "RedfishSystem": + from .redfish import RedfishSystem + + return RedfishSystem + elif name == "RHEVMSystem": + from .rhevm import RHEVMSystem + + return RHEVMSystem + elif name == "SCVMMSystem": + from .scvmm import SCVMMSystem + + return SCVMMSystem + elif name == "VmwareCloudSystem": + from .vcloud import VmwareCloudSystem + + return VmwareCloudSystem + elif name == "VMWareSystem": + from .virtualcenter import VMWareSystem + + return VMWareSystem + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/wrapanapi/systems/msazure.py b/wrapanapi/systems/msazure.py index a7c32397..d544b9a4 100644 --- a/wrapanapi/systems/msazure.py +++ b/wrapanapi/systems/msazure.py @@ -8,25 +8,63 @@ from functools import cached_property import pytz -from azure.core.exceptions import HttpResponseError -from azure.identity import ClientSecretCredential -from azure.mgmt.compute import ComputeManagementClient -from azure.mgmt.iothub import IotHubClient -from azure.mgmt.network import NetworkManagementClient -from azure.mgmt.network.models import NetworkSecurityGroup, SecurityRule -from azure.mgmt.resource.resources import ResourceManagementClient -from azure.mgmt.storage import StorageManagementClient -from azure.mgmt.subscription import SubscriptionClient -from azure.mgmt.subscription.models import SubscriptionState -from azure.storage.blob import BlobServiceClient from dateutil import parser -from msrestazure.azure_exceptions import CloudError from wait_for import wait_for from wrapanapi.entities import Instance, Template, TemplateMixin, VmMixin, VmState from wrapanapi.exceptions import ImageNotFoundError, MultipleImagesError, VMInstanceNotFound from wrapanapi.systems.base import System +# Lazy imports for Azure dependencies to avoid import errors when Azure packages +# are not properly installed or when users don't need Azure functionality +_azure_imports_loaded = False + + +def _ensure_azure_imports(): + """Lazily import Azure dependencies only when needed.""" + global _azure_imports_loaded + global HttpResponseError, ClientSecretCredential, ComputeManagementClient + global IotHubClient, NetworkManagementClient, NetworkSecurityGroup, SecurityRule + global ResourceManagementClient, StorageManagementClient, SubscriptionClient + global SubscriptionState, BlobServiceClient, CloudError + + if not _azure_imports_loaded: + from azure.core.exceptions import HttpResponseError as _HttpResponseError + from azure.identity import ClientSecretCredential as _ClientSecretCredential + from azure.mgmt.compute import ComputeManagementClient as _ComputeManagementClient + from azure.mgmt.iothub import IotHubClient as _IotHubClient + from azure.mgmt.network import NetworkManagementClient as _NetworkManagementClient + from azure.mgmt.network.models import ( + NetworkSecurityGroup as _NetworkSecurityGroup, + ) + from azure.mgmt.network.models import ( + SecurityRule as _SecurityRule, + ) + from azure.mgmt.resource.resources import ( + ResourceManagementClient as _ResourceManagementClient, + ) + from azure.mgmt.storage import StorageManagementClient as _StorageManagementClient + from azure.mgmt.subscription import SubscriptionClient as _SubscriptionClient + from azure.mgmt.subscription.models import SubscriptionState as _SubscriptionState + from azure.storage.blob import BlobServiceClient as _BlobServiceClient + from msrestazure.azure_exceptions import CloudError as _CloudError + + HttpResponseError = _HttpResponseError + ClientSecretCredential = _ClientSecretCredential + ComputeManagementClient = _ComputeManagementClient + IotHubClient = _IotHubClient + NetworkManagementClient = _NetworkManagementClient + NetworkSecurityGroup = _NetworkSecurityGroup + SecurityRule = _SecurityRule + ResourceManagementClient = _ResourceManagementClient + StorageManagementClient = _StorageManagementClient + SubscriptionClient = _SubscriptionClient + SubscriptionState = _SubscriptionState + BlobServiceClient = _BlobServiceClient + CloudError = _CloudError + + _azure_imports_loaded = True + class AzureInstance(Instance): state_map = { @@ -47,6 +85,7 @@ def __init__(self, system, raw=None, **kwargs): name: name of instance resource_group: name of resource group this instance is in """ + _ensure_azure_imports() self._resource_group = kwargs.get("resource_group") self._name = kwargs.get("name") if not self._name or not self._resource_group: @@ -304,6 +343,7 @@ def __init__(self, system, raw=None, **kwargs): name: name of template container: container the template is stored in """ + _ensure_azure_imports() self._name = kwargs.get("name") self._container = kwargs.get("container") if not self._name or not self._container: @@ -488,6 +528,7 @@ class AzureSystem(System, VmMixin, TemplateMixin): } def __init__(self, **kwargs): + _ensure_azure_imports() super().__init__(**kwargs) self.client_id = kwargs.get("username") self.client_secret = kwargs.get("password") diff --git a/wrapanapi/systems/nuage.py b/wrapanapi/systems/nuage.py index 820c098b..f1dfbbf4 100644 --- a/wrapanapi/systems/nuage.py +++ b/wrapanapi/systems/nuage.py @@ -22,10 +22,10 @@ class NuageSystem(System): # entities.count() == (fetcher, served object, count of fetched objects) "num_security_group": lambda self: self.api.policy_groups.count()[2], # Filter out 'BackHaulSubnet' and combine it with l2_domains the same way CloudForms does - "num_cloud_subnet": lambda self: self.api.subnets.count(filter="name != 'BackHaulSubnet'")[ - 2 - ] - + self.api.l2_domains.count()[2], + "num_cloud_subnet": lambda self: ( + self.api.subnets.count(filter="name != 'BackHaulSubnet'")[2] + + self.api.l2_domains.count()[2] + ), "num_cloud_tenant": lambda self: self.api.enterprises.count()[2], "num_network_router": lambda self: self.api.domains.count()[2], "num_cloud_network": lambda self: len(self.list_floating_network_resources()),