@@ -12,6 +12,8 @@ import sys
12
12
import shlex
13
13
import shutil
14
14
15
+ log = logging .getLogger ("nspawn-runner" )
16
+
15
17
# Import YAML for parsing only. Prefer pyyaml for loading, because it's
16
18
# significantly faster
17
19
try :
@@ -31,33 +33,16 @@ except ModuleNotFoundError:
31
33
def yaml_load (file ):
32
34
raise NotImplementedError ("this feature requires PyYaml or ruamel.yaml" )
33
35
34
- CONFIG_DIR = "/etc/nspawn-runner"
36
+ def systemd_version ():
37
+ res = subprocess .run (["systemd" , "--version" ], check = True , capture_output = True , text = True )
38
+ return int (res .stdout .splitlines ()[0 ].split ()[1 ])
35
39
36
- # Set to True to enable seccomp filtering when running CI jobs. This makes the
37
- # build slower, but makes sandboxing features available. See "Sandboxing" in
38
- # https://www.freedesktop.org/software/systemd/man/systemd.exec.html
39
- ENABLE_SECCOMP = False
40
+ CONFIG_DIR = "/etc/nspawn-runner"
41
+ DATA_DIR = "/var/lib/nspawn-runner"
40
42
43
+ SYSTEMD_VERSION = systemd_version ()
41
44
EATMYDATA = shutil .which ("eatmydata" )
42
45
43
- # See https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html
44
-
45
- # If not None, set --property=CPUAccounting=yes and
46
- # --property=CPUWeight={CPU_WEIGHT} when starting systemd-nspawn
47
- CPU_WEIGHT = 50
48
-
49
- # If not None, set --property=MemoryAccounting=yes and --property=MemoryHigh={MEMORY_HIGH}
50
- MEMORY_HIGH = "30%"
51
- # If not None, and MEMORY_HIGH is set, also set --property=MemoryMax={MEMORY_MAX}
52
- MEMORY_MAX = "40%"
53
-
54
- # Set to true to use a tempfs overlay for writable storage. This makes CIs much
55
- # faster, if the machine configuration in terms of ram and swapspace has enough
56
- # capacity to handle disk space used for builds
57
- RAMDISK = False
58
-
59
- log = logging .getLogger ("nspawn-runner" )
60
-
61
46
62
47
def run_cmd (cmd : List [str ], ** kw ) -> subprocess .CompletedProcess :
63
48
"""
@@ -98,15 +83,13 @@ class NspawnRunner:
98
83
self .root_dir = root_dir
99
84
self .gitlab_build_dir = os .path .join (self .root_dir , ".build" )
100
85
self .gitlab_cache_dir = os .path .join (self .root_dir , ".cache" )
101
- res = subprocess .run (["systemd" , "--version" ], check = True , capture_output = True , text = True )
102
- self .systemd_version = int (res .stdout .splitlines ()[0 ].split ()[1 ])
103
86
104
87
@classmethod
105
- def create (cls , root_dir : str ):
88
+ def create (cls , root_dir : str , ram_disk : bool ):
106
89
"""
107
90
Instantiate the right NspawnRunner subclass for this sytem
108
91
"""
109
- if RAMDISK :
92
+ if ram_disk :
110
93
return TmpfsRunner (root_dir )
111
94
112
95
# Detect filesystem type
@@ -181,7 +164,7 @@ class Machine:
181
164
self .run_id = run_id
182
165
self .machine_name = f"run-{ self .run_id } "
183
166
184
- def _run_nspawn (self , cmd : List [str ]):
167
+ def _run_nspawn (self , chroot : "Chroot" , cmd : List [str ]):
185
168
"""
186
169
Run the given systemd-nspawn command line, contained into its own unit
187
170
using systemd-run
@@ -200,23 +183,29 @@ class Machine:
200
183
'WatchdogSec=3min' ,
201
184
]
202
185
203
- if CPU_WEIGHT is not None :
186
+ if chroot . config . get ( 'cpu_weight' ) is not None :
204
187
unit_config .append ("CPUAccounting=yes" )
205
- unit_config .append (f"CPUWeight={ CPU_WEIGHT } " )
188
+ unit_config .append (f"CPUWeight={ chroot .config .get ('cpu_weight' )} " )
189
+
190
+ if chroot .config .get ('memory_high' ) is not None :
191
+ if not "MemoryAccounting=yes" in unit_config :
192
+ unit_config .append ("MemoryAccounting=yes" )
193
+ unit_config .append (f"MemoryHigh={ chroot .config .get ('memory_high' )} " )
206
194
207
- if MEMORY_HIGH is not None :
208
- unit_config .append ("MemoryAccounting=yes" )
209
- unit_config .append (f"MemoryHigh={ MEMORY_HIGH } " )
210
- if MEMORY_MAX is not None and self .nspawn_runner .systemd_version >= 249 :
211
- unit_config .append (f"MemoryMax={ MEMORY_MAX } " )
195
+ if chroot .config .get ('memory_max' ) is not None and SYSTEMD_VERSION >= 249 :
196
+ if not "MemoryAccounting=yes" in unit_config :
197
+ unit_config .append ("MemoryAccounting=yes" )
198
+ unit_config .append (f"MemoryMax={ chroot .config .get ('memory_max' )} " )
212
199
213
200
systemd_run_cmd = ["systemd-run" ]
214
- if not ENABLE_SECCOMP :
201
+ if not chroot . config . get ( 'seccomp' ) :
215
202
systemd_run_cmd .append ("--setenv=SYSTEMD_SECCOMP=0" )
203
+
216
204
for c in unit_config :
217
205
systemd_run_cmd .append (f"--property={ c } " )
218
206
219
207
systemd_run_cmd .extend (cmd )
208
+ systemd_run_cmd .extend (chroot .config .get ('args' , []))
220
209
221
210
log .info ("Running %s" , " " .join (shlex .quote (c ) for c in systemd_run_cmd ))
222
211
os .execvp (systemd_run_cmd [0 ], systemd_run_cmd )
@@ -231,7 +220,7 @@ class Machine:
231
220
f"--machine={ self .machine_name } " ,
232
221
"--boot" , "--notify-ready=yes" ]
233
222
234
- if self . nspawn_runner . systemd_version >= 250 :
223
+ if SYSTEMD_VERSION >= 250 :
235
224
res .append ("--suppress-sync=yes" )
236
225
return res
237
226
@@ -288,7 +277,7 @@ class OverlayMachine(Machine):
288
277
if os .path .exists (self .overlay_dir ):
289
278
raise Fail (f"overlay directory { self .overlay_dir } already exists" )
290
279
os .makedirs (self .overlay_dir , exist_ok = True )
291
- self ._run_nspawn (self ._get_nspawn_command (chroot ))
280
+ self ._run_nspawn (chroot , self ._get_nspawn_command (chroot ))
292
281
293
282
def terminate (self ):
294
283
try :
@@ -311,7 +300,7 @@ class BtrfsMachine(Machine):
311
300
312
301
def start (self , chroot : "Chroot" ):
313
302
log .info ("Starting machine using image %s" , chroot .image_name )
314
- self ._run_nspawn (self ._get_nspawn_command (chroot ))
303
+ self ._run_nspawn (chroot , self ._get_nspawn_command (chroot ))
315
304
316
305
def terminate (self ):
317
306
res = subprocess .run (["machinectl" , "terminate" , self .machine_name ])
@@ -335,7 +324,7 @@ class TmpfsMachine(Machine):
335
324
336
325
def start (self , chroot : "Chroot" ):
337
326
log .info ("Starting machine using image %s" , chroot .image_name )
338
- self ._run_nspawn (self ._get_nspawn_command (chroot ))
327
+ self ._run_nspawn (chroot , self ._get_nspawn_command (chroot ))
339
328
340
329
def terminate (self ):
341
330
res = subprocess .run (["machinectl" , "terminate" , self .machine_name ])
@@ -351,6 +340,7 @@ class Chroot:
351
340
self .nspawn_runner = nspawn_runner
352
341
self .image_name = image_name
353
342
self .chroot_dir = os .path .join (nspawn_runner .root_dir , self .image_name )
343
+ self .config = self .load_config ()
354
344
355
345
def exists (self ) -> bool :
356
346
"""
@@ -394,7 +384,7 @@ class Chroot:
394
384
Login is done with exec, so this function, when successful, never
395
385
returns and destroys the calling process
396
386
"""
397
- cmd = ["systemd-nspawn" , "--directory" , self .chroot_dir ]
387
+ cmd = ["systemd-nspawn" , "--directory" , self .chroot_dir ] + self . config . get ( 'args' , [])
398
388
log .info ("Running %s" , " " .join (shlex .quote (c ) for c in cmd ))
399
389
os .execvp (cmd [0 ], cmd )
400
390
@@ -441,11 +431,11 @@ class Chroot:
441
431
442
432
# Extract what we need from the variables
443
433
res : Dict [str , Any ] = {}
444
- for var in ( "chroot_suite" , "maint_recreate" ):
445
- key = f"nspawn_runner_ { var } "
446
- if key not in pb_vars :
434
+ prefix = 'nspawn_runner_'
435
+ for key in pb_vars . keys ():
436
+ if not key . startswith ( prefix ) :
447
437
continue
448
- res [var ] = pb_vars .get (key )
438
+ res [key [ len ( prefix ):] ] = pb_vars .get (key )
449
439
450
440
return res
451
441
@@ -505,12 +495,11 @@ class Chroot:
505
495
log .error ("%s: chroot configuration not found" , self .image_name )
506
496
return
507
497
log .info ("%s: running maintenance" , self .image_name )
508
- config = self .load_config ()
509
- if config .get ("maint_recreate" ) and self .exists ():
498
+ if self .config .get ("maint_recreate" ) and self .exists ():
510
499
log .info ("%s: removing chroot to recreate it during maintenance" , self .image_name )
511
500
self .remove ()
512
501
if not self .exists ():
513
- suite = config .get ("chroot_suite" )
502
+ suite = self . config .get ("chroot_suite" )
514
503
if suite is None :
515
504
log .error ("%s: chroot_suite not found in playbook, and chroot does not exist" , self .image_name )
516
505
return
@@ -629,7 +618,7 @@ class Command:
629
618
def __init__ (self , args ):
630
619
self .args = args
631
620
self .setup_logging ()
632
- self .nspawn_runner = NspawnRunner .create ("/var/lib/nspawn-runner" )
621
+ self .nspawn_runner = NspawnRunner .create (DATA_DIR , self . args . ram_disk )
633
622
634
623
def setup_logging (self ):
635
624
# Setup logging
@@ -742,8 +731,7 @@ class ChrootCreate(ChrootMixin, SetupMixin, Command):
742
731
self .chroot .must_not_exist ()
743
732
suite = self .args .suite
744
733
if suite is None :
745
- config = self .chroot .load_config ()
746
- suite = config .get ("chroot_suite" )
734
+ suite = self .chroot .config .get ("chroot_suite" )
747
735
if suite is None :
748
736
suite = self .FALLBACK_SUITE
749
737
self .chroot .create (suite )
@@ -885,6 +873,7 @@ def main():
885
873
parser = argparse .ArgumentParser (description = "Manage systemd-nspawn machines for CI runs." )
886
874
parser .add_argument ("-v" , "--verbose" , action = "store_true" , help = "verbose output" )
887
875
parser .add_argument ("--debug" , action = "store_true" , help = "verbose output" )
876
+ parser .add_argument ("--ram-disk" , action = "store_true" , help = "use ram disks" )
888
877
subparsers = parser .add_subparsers (help = "sub-command help" , dest = "command" )
889
878
890
879
ChrootList .make_subparser (subparsers )
0 commit comments