@@ -12,6 +12,8 @@ import sys
1212import shlex
1313import shutil
1414
15+ log = logging .getLogger ("nspawn-runner" )
16+
1517# Import YAML for parsing only. Prefer pyyaml for loading, because it's
1618# significantly faster
1719try :
@@ -31,33 +33,16 @@ except ModuleNotFoundError:
3133 def yaml_load (file ):
3234 raise NotImplementedError ("this feature requires PyYaml or ruamel.yaml" )
3335
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 ])
3539
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"
4042
43+ SYSTEMD_VERSION = systemd_version ()
4144EATMYDATA = shutil .which ("eatmydata" )
4245
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-
6146
6247def run_cmd (cmd : List [str ], ** kw ) -> subprocess .CompletedProcess :
6348 """
@@ -98,15 +83,13 @@ class NspawnRunner:
9883 self .root_dir = root_dir
9984 self .gitlab_build_dir = os .path .join (self .root_dir , ".build" )
10085 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 ])
10386
10487 @classmethod
105- def create (cls , root_dir : str ):
88+ def create (cls , root_dir : str , ram_disk : bool ):
10689 """
10790 Instantiate the right NspawnRunner subclass for this sytem
10891 """
109- if RAMDISK :
92+ if ram_disk :
11093 return TmpfsRunner (root_dir )
11194
11295 # Detect filesystem type
@@ -181,7 +164,7 @@ class Machine:
181164 self .run_id = run_id
182165 self .machine_name = f"run-{ self .run_id } "
183166
184- def _run_nspawn (self , cmd : List [str ]):
167+ def _run_nspawn (self , chroot : "Chroot" , cmd : List [str ]):
185168 """
186169 Run the given systemd-nspawn command line, contained into its own unit
187170 using systemd-run
@@ -200,23 +183,29 @@ class Machine:
200183 'WatchdogSec=3min' ,
201184 ]
202185
203- if CPU_WEIGHT is not None :
186+ if chroot . config . get ( 'cpu_weight' ) is not None :
204187 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' )} " )
206194
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' )} " )
212199
213200 systemd_run_cmd = ["systemd-run" ]
214- if not ENABLE_SECCOMP :
201+ if not chroot . config . get ( 'seccomp' ) :
215202 systemd_run_cmd .append ("--setenv=SYSTEMD_SECCOMP=0" )
203+
216204 for c in unit_config :
217205 systemd_run_cmd .append (f"--property={ c } " )
218206
219207 systemd_run_cmd .extend (cmd )
208+ systemd_run_cmd .extend (chroot .config .get ('args' , []))
220209
221210 log .info ("Running %s" , " " .join (shlex .quote (c ) for c in systemd_run_cmd ))
222211 os .execvp (systemd_run_cmd [0 ], systemd_run_cmd )
@@ -231,7 +220,7 @@ class Machine:
231220 f"--machine={ self .machine_name } " ,
232221 "--boot" , "--notify-ready=yes" ]
233222
234- if self . nspawn_runner . systemd_version >= 250 :
223+ if SYSTEMD_VERSION >= 250 :
235224 res .append ("--suppress-sync=yes" )
236225 return res
237226
@@ -288,7 +277,7 @@ class OverlayMachine(Machine):
288277 if os .path .exists (self .overlay_dir ):
289278 raise Fail (f"overlay directory { self .overlay_dir } already exists" )
290279 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 ))
292281
293282 def terminate (self ):
294283 try :
@@ -311,7 +300,7 @@ class BtrfsMachine(Machine):
311300
312301 def start (self , chroot : "Chroot" ):
313302 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 ))
315304
316305 def terminate (self ):
317306 res = subprocess .run (["machinectl" , "terminate" , self .machine_name ])
@@ -335,7 +324,7 @@ class TmpfsMachine(Machine):
335324
336325 def start (self , chroot : "Chroot" ):
337326 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 ))
339328
340329 def terminate (self ):
341330 res = subprocess .run (["machinectl" , "terminate" , self .machine_name ])
@@ -351,6 +340,7 @@ class Chroot:
351340 self .nspawn_runner = nspawn_runner
352341 self .image_name = image_name
353342 self .chroot_dir = os .path .join (nspawn_runner .root_dir , self .image_name )
343+ self .config = self .load_config ()
354344
355345 def exists (self ) -> bool :
356346 """
@@ -394,7 +384,7 @@ class Chroot:
394384 Login is done with exec, so this function, when successful, never
395385 returns and destroys the calling process
396386 """
397- cmd = ["systemd-nspawn" , "--directory" , self .chroot_dir ]
387+ cmd = ["systemd-nspawn" , "--directory" , self .chroot_dir ] + self . config . get ( 'args' , [])
398388 log .info ("Running %s" , " " .join (shlex .quote (c ) for c in cmd ))
399389 os .execvp (cmd [0 ], cmd )
400390
@@ -441,11 +431,11 @@ class Chroot:
441431
442432 # Extract what we need from the variables
443433 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 ) :
447437 continue
448- res [var ] = pb_vars .get (key )
438+ res [key [ len ( prefix ):] ] = pb_vars .get (key )
449439
450440 return res
451441
@@ -505,12 +495,11 @@ class Chroot:
505495 log .error ("%s: chroot configuration not found" , self .image_name )
506496 return
507497 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 ():
510499 log .info ("%s: removing chroot to recreate it during maintenance" , self .image_name )
511500 self .remove ()
512501 if not self .exists ():
513- suite = config .get ("chroot_suite" )
502+ suite = self . config .get ("chroot_suite" )
514503 if suite is None :
515504 log .error ("%s: chroot_suite not found in playbook, and chroot does not exist" , self .image_name )
516505 return
@@ -629,7 +618,7 @@ class Command:
629618 def __init__ (self , args ):
630619 self .args = args
631620 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 )
633622
634623 def setup_logging (self ):
635624 # Setup logging
@@ -742,8 +731,7 @@ class ChrootCreate(ChrootMixin, SetupMixin, Command):
742731 self .chroot .must_not_exist ()
743732 suite = self .args .suite
744733 if suite is None :
745- config = self .chroot .load_config ()
746- suite = config .get ("chroot_suite" )
734+ suite = self .chroot .config .get ("chroot_suite" )
747735 if suite is None :
748736 suite = self .FALLBACK_SUITE
749737 self .chroot .create (suite )
@@ -885,6 +873,7 @@ def main():
885873 parser = argparse .ArgumentParser (description = "Manage systemd-nspawn machines for CI runs." )
886874 parser .add_argument ("-v" , "--verbose" , action = "store_true" , help = "verbose output" )
887875 parser .add_argument ("--debug" , action = "store_true" , help = "verbose output" )
876+ parser .add_argument ("--ram-disk" , action = "store_true" , help = "use ram disks" )
888877 subparsers = parser .add_subparsers (help = "sub-command help" , dest = "command" )
889878
890879 ChrootList .make_subparser (subparsers )
0 commit comments