Skip to content

Commit 8b59b92

Browse files
committed
use lazy import inside mrack plugin.
1 parent 67bc662 commit 8b59b92

File tree

1 file changed

+154
-111
lines changed

1 file changed

+154
-111
lines changed

tmt/steps/provision/mrack.py

Lines changed: 154 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from collections.abc import Mapping
77
from contextlib import suppress
88
from functools import wraps
9-
from typing import Any, Callable, Optional, TypedDict, Union, cast
9+
from threading import Lock
10+
from typing import TYPE_CHECKING, Any, Callable, Optional, TypedDict, Union, cast
1011

1112
import tmt
1213
import tmt.hardware
@@ -15,6 +16,7 @@
1516
import tmt.steps
1617
import tmt.steps.provision
1718
import tmt.utils
19+
from tmt.plugins import ModuleImporter
1820
from tmt.utils import (
1921
Command,
2022
Path,
@@ -24,16 +26,45 @@
2426
field,
2527
)
2628

27-
mrack: Any
28-
providers: Any
29-
ProvisioningError: Any
30-
NotAuthenticatedError: Any
31-
BEAKER: Any
32-
BeakerProvider: Any
33-
BeakerTransformer: Any
34-
TmtBeakerTransformer: Any
29+
if TYPE_CHECKING:
30+
import mrack
31+
import mrack.context
32+
import mrack.errors
33+
import mrack.providers
34+
import mrack.providers.beaker
35+
import mrack.transformers.beaker
36+
37+
# lazy initialization of mrack module via ModuleImporter plugin
38+
import_mrack: ModuleImporter['mrack'] = ModuleImporter(
39+
'mrack',
40+
tmt.utils.ProvisionError,
41+
"Install 'tmt+provision-beaker' to provision using this method.")
42+
43+
import_mrack_context: ModuleImporter['mrack.context'] = ModuleImporter(
44+
'mrack.context',
45+
tmt.utils.ProvisionError,
46+
"Install 'tmt+provision-beaker' to provision using this method.")
47+
48+
import_mrack_errors: ModuleImporter['mrack.errors'] = ModuleImporter(
49+
'mrack.errors',
50+
tmt.utils.ProvisionError,
51+
"Install 'tmt+provision-beaker' to provision using this method.")
52+
53+
import_mrack_providers: ModuleImporter['mrack.providers'] = ModuleImporter(
54+
'mrack.providers',
55+
tmt.utils.ProvisionError,
56+
"Install 'tmt+provision-beaker' to provision using this method.")
57+
58+
import_mrack_providers_beaker: ModuleImporter['mrack.providers.beaker'] = ModuleImporter(
59+
'mrack.providers.beaker',
60+
tmt.utils.ProvisionError,
61+
"Install 'tmt+provision-beaker' to provision using this method.")
62+
63+
import_mrack_transformers_beaker: ModuleImporter['mrack.transformers.beaker'] = ModuleImporter(
64+
'mrack.transformers.beaker',
65+
tmt.utils.ProvisionError,
66+
"Install 'tmt+provision-beaker' to provision using this method.")
3567

36-
_MRACK_IMPORTED: bool = False
3768

3869
DEFAULT_USER = 'root'
3970
DEFAULT_ARCH = 'x86_64'
@@ -45,6 +76,101 @@
4576
#: Kerberos ticket.
4677
DEFAULT_API_SESSION_REFRESH = 3600
4778

79+
80+
class SingletonMeta(type):
81+
_instances = {}
82+
_lock: Lock = Lock()
83+
84+
def __call__(cls, *args, **kwargs):
85+
with cls._lock:
86+
if cls not in cls._instances:
87+
instance = super().__call__(*args, **kwargs)
88+
cls._instances[cls] = instance
89+
return cls._instances[cls]
90+
91+
92+
class MrackModule(metaclass=SingletonMeta):
93+
_logger = None
94+
_is_mrack_fixed = False
95+
_lock: Lock = Lock()
96+
97+
mrack = None
98+
providers = None
99+
providers_beaker = None
100+
errors = None
101+
context = None
102+
transformers_beaker = None
103+
104+
def init(self, logger: tmt.log.Logger) -> None:
105+
self._logger = logger
106+
self.mrack: mrack = import_mrack(logger=logger)
107+
self.context: mrack.context = import_mrack_context(logger=logger)
108+
self.errors: mrack.errors = import_mrack_errors(logger=logger)
109+
self.providers: mrack.providers = import_mrack_providers(logger=logger)
110+
self.providers_beaker = import_mrack_providers_beaker(logger=logger)
111+
self.transformers_beaker = import_mrack_transformers_beaker(logger=logger)
112+
113+
def fix_handlers(self, workdir: Any, name: str) -> None:
114+
# hack: remove mrack stdout and move the logfile to /tmp
115+
with self._lock:
116+
if not self._is_mrack_fixed:
117+
self._is_mrack_fixed = True
118+
self.mrack.logger.removeHandler(self.mrack.console_handler)
119+
self.mrack.logger.removeHandler(self.mrack.file_handler)
120+
with suppress(OSError):
121+
os.remove("mrack.log")
122+
123+
logging.FileHandler(str(f"{workdir}/{name}-mrack.log"))
124+
providers = self.providers.providers
125+
providers.register(
126+
self.providers_beaker.PROVISIONER_KEY,
127+
self.providers_beaker.BeakerProvider)
128+
129+
130+
def get_bkr_transformer_cls(logger: tmt.log.Logger) -> Callable:
131+
MrackModule().transformers_beaker
132+
class TmtBeakerTransformer(MrackModule().transformers_beaker.BeakerTransformer):
133+
def _translate_tmt_hw(self, hw: tmt.hardware.Hardware) -> dict[str, Any]:
134+
""" Return hw requirements from given hw dictionary """
135+
136+
assert hw.constraint
137+
138+
transformed = MrackHWAndGroup(
139+
children=[
140+
constraint_to_beaker_filter(constraint, logger)
141+
for constraint in hw.constraint.variant()
142+
])
143+
144+
logger.debug(
145+
'Transformed hardware',
146+
tmt.utils.dict_to_yaml(
147+
transformed.to_mrack()))
148+
149+
return {
150+
'hostRequires': transformed.to_mrack()
151+
}
152+
153+
def create_host_requirement(self, host: CreateJobParameters) -> dict[str, Any]:
154+
""" Create single input for Beaker provisioner """
155+
req: dict[str, Any] = super().create_host_requirement(host.to_mrack())
156+
157+
if host.hardware and host.hardware.constraint:
158+
req.update(self._translate_tmt_hw(host.hardware))
159+
160+
if host.beaker_job_owner:
161+
req['job_owner'] = host.beaker_job_owner
162+
163+
# Whiteboard must be added *after* request preparation, to overwrite the
164+
# default one.
165+
req['whiteboard'] = host.whiteboard
166+
167+
logger.debug('mrack request', req, level=4)
168+
169+
logger.info('whiteboard', host.whiteboard, 'green')
170+
171+
return req
172+
return TmtBeakerTransformer
173+
48174
# Type annotation for "data" package describing a guest instance. Passed
49175
# between load() and save() calls
50176

@@ -698,88 +824,6 @@ def constraint_to_beaker_filter(
698824
return _transform_unsupported(constraint, logger)
699825

700826

701-
def import_and_load_mrack_deps(workdir: Any, name: str, logger: tmt.log.Logger) -> None:
702-
""" Import mrack module only when needed """
703-
global _MRACK_IMPORTED
704-
705-
if _MRACK_IMPORTED:
706-
return
707-
708-
global mrack
709-
global providers
710-
global ProvisioningError
711-
global NotAuthenticatedError
712-
global BEAKER
713-
global BeakerProvider
714-
global BeakerTransformer
715-
global TmtBeakerTransformer
716-
717-
try:
718-
import mrack
719-
from mrack.errors import NotAuthenticatedError, ProvisioningError
720-
from mrack.providers import providers
721-
from mrack.providers.beaker import PROVISIONER_KEY as BEAKER
722-
from mrack.providers.beaker import BeakerProvider
723-
from mrack.transformers.beaker import BeakerTransformer
724-
725-
# hack: remove mrack stdout and move the logfile to /tmp
726-
mrack.logger.removeHandler(mrack.console_handler)
727-
mrack.logger.removeHandler(mrack.file_handler)
728-
729-
with suppress(OSError):
730-
os.remove("mrack.log")
731-
732-
logging.FileHandler(str(f"{workdir}/{name}-mrack.log"))
733-
734-
providers.register(BEAKER, BeakerProvider)
735-
736-
except ImportError:
737-
raise ProvisionError(
738-
"Install 'tmt+provision-beaker' to provision using this method.")
739-
740-
# ignore the misc because mrack sources are not typed and result into
741-
# error: Class cannot subclass "BeakerTransformer" (has type "Any")
742-
# as mypy does not have type information for the BeakerTransformer class
743-
class TmtBeakerTransformer(BeakerTransformer): # type: ignore[misc]
744-
def _translate_tmt_hw(self, hw: tmt.hardware.Hardware) -> dict[str, Any]:
745-
""" Return hw requirements from given hw dictionary """
746-
747-
assert hw.constraint
748-
749-
transformed = MrackHWAndGroup(
750-
children=[
751-
constraint_to_beaker_filter(constraint, logger)
752-
for constraint in hw.constraint.variant()
753-
])
754-
755-
logger.debug('Transformed hardware', tmt.utils.dict_to_yaml(transformed.to_mrack()))
756-
757-
return {
758-
'hostRequires': transformed.to_mrack()
759-
}
760-
761-
def create_host_requirement(self, host: CreateJobParameters) -> dict[str, Any]:
762-
""" Create single input for Beaker provisioner """
763-
req: dict[str, Any] = super().create_host_requirement(host.to_mrack())
764-
765-
if host.hardware and host.hardware.constraint:
766-
req.update(self._translate_tmt_hw(host.hardware))
767-
768-
if host.beaker_job_owner:
769-
req['job_owner'] = host.beaker_job_owner
770-
771-
# Whiteboard must be added *after* request preparation, to overwrite the default one.
772-
req['whiteboard'] = host.whiteboard
773-
774-
logger.debug('mrack request', req, level=4)
775-
776-
logger.info('whiteboard', host.whiteboard, 'green')
777-
778-
return req
779-
780-
_MRACK_IMPORTED = True
781-
782-
783827
def async_run(func: Any) -> Any:
784828
""" Decorate click actions to run as async """
785829
@wraps(func)
@@ -919,24 +963,24 @@ class BeakerAPI:
919963
# req is a requirement passed to Beaker mrack provisioner
920964
mrack_requirement: dict[str, Any] = {}
921965
dsp_name: str = "Beaker"
922-
966+
mrack_module = MrackModule()
923967
# wrapping around the __init__ with async wrapper does mangle the method
924968
# and mypy complains as it no longer returns None but the coroutine
969+
925970
@async_run
926-
async def __init__(self, guest: 'GuestBeaker') -> None: # type: ignore[misc]
971+
# type: ignore[misc]
972+
async def __init__(self, guest: 'GuestBeaker', logger: tmt.log.Logger) -> None:
927973
""" Initialize the API class with defaults and load the config """
928974
self._guest = guest
929-
930975
# use global context class
931-
global_context = mrack.context.global_context
932-
976+
global_context = self.mrack_module.context.global_context
977+
erorrs = self.mrack_module.errors
933978
mrack_config_locations = [
934979
Path(__file__).parent / "mrack/mrack.conf",
935980
Path("/etc/tmt/mrack.conf"),
936981
Path("~/.mrack/mrack.conf").expanduser(),
937982
Path.cwd() / "mrack.conf"
938983
]
939-
940984
mrack_config: Optional[Path] = None
941985

942986
for potential_location in mrack_config_locations:
@@ -948,13 +992,13 @@ async def __init__(self, guest: 'GuestBeaker') -> None: # type: ignore[misc]
948992

949993
try:
950994
global_context.init(str(mrack_config))
951-
except mrack.errors.ConfigError as mrack_conf_err:
995+
except erorrs.ConfigError as mrack_conf_err:
952996
raise ProvisionError(mrack_conf_err)
953997

954-
self._mrack_transformer = TmtBeakerTransformer()
998+
self._mrack_transformer = get_bkr_transformer_cls(logger)()
955999
try:
9561000
await self._mrack_transformer.init(global_context.PROV_CONFIG, {})
957-
except NotAuthenticatedError as kinit_err:
1001+
except erorrs.NotAuthenticatedError as kinit_err:
9581002
raise ProvisionError(kinit_err) from kinit_err
9591003
except AttributeError as hub_err:
9601004
raise ProvisionError(
@@ -1026,17 +1070,16 @@ class GuestBeaker(tmt.steps.provision.GuestSsh):
10261070

10271071
_api: Optional[BeakerAPI] = None
10281072
_api_timestamp: Optional[datetime.datetime] = None
1073+
is_mrack_handlers_fixed = False
10291074

10301075
@property
10311076
def api(self) -> BeakerAPI:
10321077
""" Create BeakerAPI leveraging mrack """
10331078

10341079
def _construct_api() -> tuple[BeakerAPI, datetime.datetime]:
10351080
assert self.parent is not None
1036-
1037-
import_and_load_mrack_deps(self.parent.workdir, self.parent.name, self._logger)
1038-
1039-
return BeakerAPI(self), datetime.datetime.now(datetime.timezone.utc)
1081+
MrackModule().fix_handlers(self.parent.workdir, self.parent.name)
1082+
return BeakerAPI(self, self._logger), datetime.datetime.now(datetime.timezone.utc)
10401083

10411084
if self._api is None:
10421085
self._api, self._api_timestamp = _construct_api()
@@ -1059,8 +1102,6 @@ def is_ready(self) -> bool:
10591102
if self.job_id is None:
10601103
return False
10611104

1062-
assert mrack is not None
1063-
10641105
try:
10651106
response = self.api.inspect()
10661107

@@ -1076,7 +1117,7 @@ def is_ready(self) -> bool:
10761117
return True
10771118
return False
10781119

1079-
except mrack.errors.MrackError:
1120+
except MrackModule().errors.MrackError:
10801121
return False
10811122

10821123
def _create(self, tmt_name: str) -> None:
@@ -1091,11 +1132,13 @@ def _create(self, tmt_name: str) -> None:
10911132
name=f'{self.image}-{self.arch}',
10921133
whiteboard=self.whiteboard or tmt_name,
10931134
beaker_job_owner=self.beaker_job_owner)
1094-
1135+
mrack_module = MrackModule()
1136+
# initialize module and logger inside mrack module as this is fist usage
1137+
mrack_module.init(self._logger)
1138+
provisioning_error = mrack_module.errors.ProvisioningError
10951139
try:
10961140
response = self.api.create(data)
1097-
1098-
except ProvisioningError as exc:
1141+
except provisioning_error as exc:
10991142
import xmlrpc.client
11001143

11011144
cause = exc.__cause__

0 commit comments

Comments
 (0)