6
6
from collections .abc import Mapping
7
7
from contextlib import suppress
8
8
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
10
11
11
12
import tmt
12
13
import tmt .hardware
15
16
import tmt .steps
16
17
import tmt .steps .provision
17
18
import tmt .utils
19
+ from tmt .plugins import ModuleImporter
18
20
from tmt .utils import (
19
21
Command ,
20
22
Path ,
24
26
field ,
25
27
)
26
28
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." )
35
67
36
- _MRACK_IMPORTED : bool = False
37
68
38
69
DEFAULT_USER = 'root'
39
70
DEFAULT_ARCH = 'x86_64'
45
76
#: Kerberos ticket.
46
77
DEFAULT_API_SESSION_REFRESH = 3600
47
78
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
+
48
174
# Type annotation for "data" package describing a guest instance. Passed
49
175
# between load() and save() calls
50
176
@@ -698,88 +824,6 @@ def constraint_to_beaker_filter(
698
824
return _transform_unsupported (constraint , logger )
699
825
700
826
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
-
783
827
def async_run (func : Any ) -> Any :
784
828
""" Decorate click actions to run as async """
785
829
@wraps (func )
@@ -919,24 +963,24 @@ class BeakerAPI:
919
963
# req is a requirement passed to Beaker mrack provisioner
920
964
mrack_requirement : dict [str , Any ] = {}
921
965
dsp_name : str = "Beaker"
922
-
966
+ mrack_module = MrackModule ()
923
967
# wrapping around the __init__ with async wrapper does mangle the method
924
968
# and mypy complains as it no longer returns None but the coroutine
969
+
925
970
@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 :
927
973
""" Initialize the API class with defaults and load the config """
928
974
self ._guest = guest
929
-
930
975
# 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
933
978
mrack_config_locations = [
934
979
Path (__file__ ).parent / "mrack/mrack.conf" ,
935
980
Path ("/etc/tmt/mrack.conf" ),
936
981
Path ("~/.mrack/mrack.conf" ).expanduser (),
937
982
Path .cwd () / "mrack.conf"
938
983
]
939
-
940
984
mrack_config : Optional [Path ] = None
941
985
942
986
for potential_location in mrack_config_locations :
@@ -948,13 +992,13 @@ async def __init__(self, guest: 'GuestBeaker') -> None: # type: ignore[misc]
948
992
949
993
try :
950
994
global_context .init (str (mrack_config ))
951
- except mrack . errors .ConfigError as mrack_conf_err :
995
+ except erorrs .ConfigError as mrack_conf_err :
952
996
raise ProvisionError (mrack_conf_err )
953
997
954
- self ._mrack_transformer = TmtBeakerTransformer ()
998
+ self ._mrack_transformer = get_bkr_transformer_cls ( logger ) ()
955
999
try :
956
1000
await self ._mrack_transformer .init (global_context .PROV_CONFIG , {})
957
- except NotAuthenticatedError as kinit_err :
1001
+ except erorrs . NotAuthenticatedError as kinit_err :
958
1002
raise ProvisionError (kinit_err ) from kinit_err
959
1003
except AttributeError as hub_err :
960
1004
raise ProvisionError (
@@ -1026,17 +1070,16 @@ class GuestBeaker(tmt.steps.provision.GuestSsh):
1026
1070
1027
1071
_api : Optional [BeakerAPI ] = None
1028
1072
_api_timestamp : Optional [datetime .datetime ] = None
1073
+ is_mrack_handlers_fixed = False
1029
1074
1030
1075
@property
1031
1076
def api (self ) -> BeakerAPI :
1032
1077
""" Create BeakerAPI leveraging mrack """
1033
1078
1034
1079
def _construct_api () -> tuple [BeakerAPI , datetime .datetime ]:
1035
1080
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 )
1040
1083
1041
1084
if self ._api is None :
1042
1085
self ._api , self ._api_timestamp = _construct_api ()
@@ -1059,8 +1102,6 @@ def is_ready(self) -> bool:
1059
1102
if self .job_id is None :
1060
1103
return False
1061
1104
1062
- assert mrack is not None
1063
-
1064
1105
try :
1065
1106
response = self .api .inspect ()
1066
1107
@@ -1076,7 +1117,7 @@ def is_ready(self) -> bool:
1076
1117
return True
1077
1118
return False
1078
1119
1079
- except mrack .errors .MrackError :
1120
+ except MrackModule () .errors .MrackError :
1080
1121
return False
1081
1122
1082
1123
def _create (self , tmt_name : str ) -> None :
@@ -1091,11 +1132,13 @@ def _create(self, tmt_name: str) -> None:
1091
1132
name = f'{ self .image } -{ self .arch } ' ,
1092
1133
whiteboard = self .whiteboard or tmt_name ,
1093
1134
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
1095
1139
try :
1096
1140
response = self .api .create (data )
1097
-
1098
- except ProvisioningError as exc :
1141
+ except provisioning_error as exc :
1099
1142
import xmlrpc .client
1100
1143
1101
1144
cause = exc .__cause__
0 commit comments