1c342db35SBrad Bishop#!/usr/bin/env python3 2c342db35SBrad Bishop"""systemctl: subset of systemctl used for image construction 3eb8dc403SDave Cobbley 4c342db35SBrad BishopMask/preset systemd units 5c342db35SBrad Bishop""" 6eb8dc403SDave Cobbley 7c342db35SBrad Bishopimport argparse 8c342db35SBrad Bishopimport fnmatch 9c342db35SBrad Bishopimport os 10c342db35SBrad Bishopimport re 11c342db35SBrad Bishopimport sys 12eb8dc403SDave Cobbley 13c342db35SBrad Bishopfrom collections import namedtuple 14615f2f11SAndrew Geisslerfrom itertools import chain 15c342db35SBrad Bishopfrom pathlib import Path 16eb8dc403SDave Cobbley 17c342db35SBrad Bishopversion = 1.0 18eb8dc403SDave Cobbley 19c342db35SBrad BishopROOT = Path("/") 20c342db35SBrad BishopSYSCONFDIR = Path("etc") 21c342db35SBrad BishopBASE_LIBDIR = Path("lib") 22c342db35SBrad BishopLIBDIR = Path("usr", "lib") 23eb8dc403SDave Cobbley 24c342db35SBrad Bishoplocations = list() 25eb8dc403SDave Cobbley 26eb8dc403SDave Cobbley 27c342db35SBrad Bishopclass SystemdFile(): 28c342db35SBrad Bishop """Class representing a single systemd configuration file""" 2987f5cff0SAndrew Geissler 3087f5cff0SAndrew Geissler _clearable_keys = ['WantedBy'] 3187f5cff0SAndrew Geissler 32615f2f11SAndrew Geissler def __init__(self, root, path, instance_unit_name): 33c342db35SBrad Bishop self.sections = dict() 34c342db35SBrad Bishop self._parse(root, path) 3508902b01SBrad Bishop dirname = os.path.basename(path.name) + ".d" 3608902b01SBrad Bishop for location in locations: 37615f2f11SAndrew Geissler files = (root / location / "system" / dirname).glob("*.conf") 38615f2f11SAndrew Geissler if instance_unit_name: 39615f2f11SAndrew Geissler inst_dirname = instance_unit_name + ".d" 40615f2f11SAndrew Geissler files = chain(files, (root / location / "system" / inst_dirname).glob("*.conf")) 41615f2f11SAndrew Geissler for path2 in sorted(files): 4208902b01SBrad Bishop self._parse(root, path2) 43eb8dc403SDave Cobbley 44c342db35SBrad Bishop def _parse(self, root, path): 45c342db35SBrad Bishop """Parse a systemd syntax configuration file 46eb8dc403SDave Cobbley 47c342db35SBrad Bishop Args: 48c342db35SBrad Bishop path: A pathlib.Path object pointing to the file 49eb8dc403SDave Cobbley 50c342db35SBrad Bishop """ 51c342db35SBrad Bishop skip_re = re.compile(r"^\s*([#;]|$)") 52c342db35SBrad Bishop section_re = re.compile(r"^\s*\[(?P<section>.*)\]") 53c342db35SBrad Bishop kv_re = re.compile(r"^\s*(?P<key>[^\s]+)\s*=\s*(?P<value>.*)") 54c342db35SBrad Bishop section = None 55eb8dc403SDave Cobbley 56c342db35SBrad Bishop if path.is_symlink(): 57c342db35SBrad Bishop try: 58c342db35SBrad Bishop path.resolve() 59c342db35SBrad Bishop except FileNotFoundError: 60c342db35SBrad Bishop # broken symlink, try relative to root 61c342db35SBrad Bishop path = root / Path(os.readlink(str(path))).relative_to(ROOT) 62eb8dc403SDave Cobbley 63c342db35SBrad Bishop with path.open() as f: 64c342db35SBrad Bishop for line in f: 65c342db35SBrad Bishop if skip_re.match(line): 66eb8dc403SDave Cobbley continue 67eb8dc403SDave Cobbley 68a34c030eSBrad Bishop line = line.strip() 69c342db35SBrad Bishop m = section_re.match(line) 70c342db35SBrad Bishop if m: 7108902b01SBrad Bishop if m.group('section') not in self.sections: 72c342db35SBrad Bishop section = dict() 73c342db35SBrad Bishop self.sections[m.group('section')] = section 7408902b01SBrad Bishop else: 7508902b01SBrad Bishop section = self.sections[m.group('section')] 76c342db35SBrad Bishop continue 77eb8dc403SDave Cobbley 78c342db35SBrad Bishop while line.endswith("\\"): 79c342db35SBrad Bishop line += f.readline().rstrip("\n") 80eb8dc403SDave Cobbley 81c342db35SBrad Bishop m = kv_re.match(line) 82c342db35SBrad Bishop k = m.group('key') 83c342db35SBrad Bishop v = m.group('value') 84c342db35SBrad Bishop if k not in section: 85c342db35SBrad Bishop section[k] = list() 8687f5cff0SAndrew Geissler 8787f5cff0SAndrew Geissler # If we come across a "key=" line for a "clearable key", then 8887f5cff0SAndrew Geissler # forget all preceding assignments. This works because we are 8987f5cff0SAndrew Geissler # processing files in correct parse order. 9087f5cff0SAndrew Geissler if k in self._clearable_keys and not v: 9187f5cff0SAndrew Geissler del section[k] 9287f5cff0SAndrew Geissler continue 9387f5cff0SAndrew Geissler 94c342db35SBrad Bishop section[k].extend(v.split()) 95c342db35SBrad Bishop 96c342db35SBrad Bishop def get(self, section, prop): 97c342db35SBrad Bishop """Get a property from section 98c342db35SBrad Bishop 99c342db35SBrad Bishop Args: 100c342db35SBrad Bishop section: Section to retrieve property from 101c342db35SBrad Bishop prop: Property to retrieve 102c342db35SBrad Bishop 103c342db35SBrad Bishop Returns: 104c342db35SBrad Bishop List representing all properties of type prop in section. 105c342db35SBrad Bishop 106c342db35SBrad Bishop Raises: 107c342db35SBrad Bishop KeyError: if ``section`` or ``prop`` not found 108c342db35SBrad Bishop """ 109c342db35SBrad Bishop return self.sections[section][prop] 110c342db35SBrad Bishop 111c342db35SBrad Bishop 112c342db35SBrad Bishopclass Presets(): 113c342db35SBrad Bishop """Class representing all systemd presets""" 114c342db35SBrad Bishop def __init__(self, scope, root): 115c342db35SBrad Bishop self.directives = list() 116c342db35SBrad Bishop self._collect_presets(scope, root) 117c342db35SBrad Bishop 118c342db35SBrad Bishop def _parse_presets(self, presets): 119c342db35SBrad Bishop """Parse presets out of a set of preset files""" 120c342db35SBrad Bishop skip_re = re.compile(r"^\s*([#;]|$)") 121c342db35SBrad Bishop directive_re = re.compile(r"^\s*(?P<action>enable|disable)\s+(?P<unit_name>(.+))") 122c342db35SBrad Bishop 123c342db35SBrad Bishop Directive = namedtuple("Directive", "action unit_name") 124c342db35SBrad Bishop for preset in presets: 125c342db35SBrad Bishop with preset.open() as f: 126c342db35SBrad Bishop for line in f: 127c342db35SBrad Bishop m = directive_re.match(line) 128c342db35SBrad Bishop if m: 129c342db35SBrad Bishop directive = Directive(action=m.group('action'), 130c342db35SBrad Bishop unit_name=m.group('unit_name')) 131c342db35SBrad Bishop self.directives.append(directive) 132c342db35SBrad Bishop elif skip_re.match(line): 133c342db35SBrad Bishop pass 134c342db35SBrad Bishop else: 135c342db35SBrad Bishop sys.exit("Unparsed preset line in {}".format(preset)) 136c342db35SBrad Bishop 137c342db35SBrad Bishop def _collect_presets(self, scope, root): 138c342db35SBrad Bishop """Collect list of preset files""" 139c342db35SBrad Bishop presets = dict() 140c342db35SBrad Bishop for location in locations: 141c342db35SBrad Bishop paths = (root / location / scope).glob("*.preset") 142c342db35SBrad Bishop for path in paths: 143c342db35SBrad Bishop # earlier names override later ones 144c342db35SBrad Bishop if path.name not in presets: 145c342db35SBrad Bishop presets[path.name] = path 146c342db35SBrad Bishop 147c342db35SBrad Bishop self._parse_presets([v for k, v in sorted(presets.items())]) 148c342db35SBrad Bishop 149c342db35SBrad Bishop def state(self, unit_name): 150c342db35SBrad Bishop """Return state of preset for unit_name 151c342db35SBrad Bishop 152c342db35SBrad Bishop Args: 153c342db35SBrad Bishop presets: set of presets 154c342db35SBrad Bishop unit_name: name of the unit 155c342db35SBrad Bishop 156c342db35SBrad Bishop Returns: 157c342db35SBrad Bishop None: no matching preset 158c342db35SBrad Bishop `enable`: unit_name is enabled 159c342db35SBrad Bishop `disable`: unit_name is disabled 160c342db35SBrad Bishop """ 161c342db35SBrad Bishop for directive in self.directives: 162c342db35SBrad Bishop if fnmatch.fnmatch(unit_name, directive.unit_name): 163c342db35SBrad Bishop return directive.action 164c342db35SBrad Bishop 165c342db35SBrad Bishop return None 166c342db35SBrad Bishop 167c342db35SBrad Bishop 168c342db35SBrad Bishopdef add_link(path, target): 169c342db35SBrad Bishop try: 170c342db35SBrad Bishop path.parent.mkdir(parents=True) 171c342db35SBrad Bishop except FileExistsError: 172c342db35SBrad Bishop pass 173c342db35SBrad Bishop if not path.is_symlink(): 174c342db35SBrad Bishop print("ln -s {} {}".format(target, path)) 175c342db35SBrad Bishop path.symlink_to(target) 176c342db35SBrad Bishop 177c342db35SBrad Bishop 178c342db35SBrad Bishopclass SystemdUnitNotFoundError(Exception): 179ac69b488SWilliam A. Kennington III def __init__(self, path, unit): 180ac69b488SWilliam A. Kennington III self.path = path 181ac69b488SWilliam A. Kennington III self.unit = unit 182c342db35SBrad Bishop 183c342db35SBrad Bishop 184c342db35SBrad Bishopclass SystemdUnit(): 185c342db35SBrad Bishop def __init__(self, root, unit): 186c342db35SBrad Bishop self.root = root 187c342db35SBrad Bishop self.unit = unit 188c342db35SBrad Bishop self.config = None 189c342db35SBrad Bishop 190c342db35SBrad Bishop def _path_for_unit(self, unit): 191c342db35SBrad Bishop for location in locations: 192c342db35SBrad Bishop path = self.root / location / "system" / unit 19382c905dcSAndrew Geissler if path.exists() or path.is_symlink(): 194c342db35SBrad Bishop return path 195c342db35SBrad Bishop 196c342db35SBrad Bishop raise SystemdUnitNotFoundError(self.root, unit) 197c342db35SBrad Bishop 198*028142bdSAndrew Geissler def _process_deps(self, config, service, location, prop, dirstem, instance): 199c342db35SBrad Bishop systemdir = self.root / SYSCONFDIR / "systemd" / "system" 200c342db35SBrad Bishop 201c342db35SBrad Bishop target = ROOT / location.relative_to(self.root) 202c342db35SBrad Bishop try: 203c342db35SBrad Bishop for dependent in config.get('Install', prop): 204*028142bdSAndrew Geissler # determine whether or not dependent is a template with an actual 205*028142bdSAndrew Geissler # instance (i.e. a '@%i') 206*028142bdSAndrew Geissler dependent_is_template = re.match(r"[^@]+@(?P<instance>[^\.]*)\.", dependent) 207*028142bdSAndrew Geissler if dependent_is_template: 208*028142bdSAndrew Geissler # if so, replace with the actual instance to achieve 209*028142bdSAndrew Geissler # svc-wants@a.service.wants/svc-wanted-by@a.service 210*028142bdSAndrew Geissler dependent = re.sub(dependent_is_template.group('instance'), instance, dependent, 1) 211c342db35SBrad Bishop wants = systemdir / "{}.{}".format(dependent, dirstem) / service 212c342db35SBrad Bishop add_link(wants, target) 213c342db35SBrad Bishop 214c342db35SBrad Bishop except KeyError: 215c342db35SBrad Bishop pass 216c342db35SBrad Bishop 2175199d831SAndrew Geissler def enable(self, caller_unit=None): 218c342db35SBrad Bishop # if we're enabling an instance, first extract the actual instance 219c342db35SBrad Bishop # then figure out what the template unit is 220c342db35SBrad Bishop template = re.match(r"[^@]+@(?P<instance>[^\.]*)\.", self.unit) 221615f2f11SAndrew Geissler instance_unit_name = None 222c342db35SBrad Bishop if template: 223c342db35SBrad Bishop instance = template.group('instance') 224615f2f11SAndrew Geissler if instance != "": 225615f2f11SAndrew Geissler instance_unit_name = self.unit 226c342db35SBrad Bishop unit = re.sub(r"@[^\.]*\.", "@.", self.unit, 1) 227c342db35SBrad Bishop else: 228c342db35SBrad Bishop instance = None 229c342db35SBrad Bishop unit = self.unit 230c342db35SBrad Bishop 231c342db35SBrad Bishop path = self._path_for_unit(unit) 232c342db35SBrad Bishop 233c342db35SBrad Bishop if path.is_symlink(): 234c342db35SBrad Bishop # ignore aliases 235c342db35SBrad Bishop return 236c342db35SBrad Bishop 237615f2f11SAndrew Geissler config = SystemdFile(self.root, path, instance_unit_name) 238c342db35SBrad Bishop if instance == "": 239c342db35SBrad Bishop try: 240c342db35SBrad Bishop default_instance = config.get('Install', 'DefaultInstance')[0] 241c342db35SBrad Bishop except KeyError: 242c342db35SBrad Bishop # no default instance, so nothing to enable 243c342db35SBrad Bishop return 244c342db35SBrad Bishop 245c342db35SBrad Bishop service = self.unit.replace("@.", 246c342db35SBrad Bishop "@{}.".format(default_instance)) 247c342db35SBrad Bishop else: 248c342db35SBrad Bishop service = self.unit 249c342db35SBrad Bishop 250*028142bdSAndrew Geissler self._process_deps(config, service, path, 'WantedBy', 'wants', instance) 251*028142bdSAndrew Geissler self._process_deps(config, service, path, 'RequiredBy', 'requires', instance) 252c342db35SBrad Bishop 253c342db35SBrad Bishop try: 254c342db35SBrad Bishop for also in config.get('Install', 'Also'): 255ac69b488SWilliam A. Kennington III try: 2565199d831SAndrew Geissler if caller_unit != also: 2575199d831SAndrew Geissler SystemdUnit(self.root, also).enable(unit) 258ac69b488SWilliam A. Kennington III except SystemdUnitNotFoundError as e: 259ac69b488SWilliam A. Kennington III sys.exit("Error: Systemctl also enable issue with %s (%s)" % (service, e.unit)) 260c342db35SBrad Bishop 261c342db35SBrad Bishop except KeyError: 262c342db35SBrad Bishop pass 263c342db35SBrad Bishop 264c342db35SBrad Bishop systemdir = self.root / SYSCONFDIR / "systemd" / "system" 265c342db35SBrad Bishop target = ROOT / path.relative_to(self.root) 266c342db35SBrad Bishop try: 267c342db35SBrad Bishop for dest in config.get('Install', 'Alias'): 268c342db35SBrad Bishop alias = systemdir / dest 269c342db35SBrad Bishop add_link(alias, target) 270c342db35SBrad Bishop 271c342db35SBrad Bishop except KeyError: 272c342db35SBrad Bishop pass 273c342db35SBrad Bishop 274c342db35SBrad Bishop def mask(self): 275c342db35SBrad Bishop systemdir = self.root / SYSCONFDIR / "systemd" / "system" 276c342db35SBrad Bishop add_link(systemdir / self.unit, "/dev/null") 277c342db35SBrad Bishop 278c342db35SBrad Bishop 279c342db35SBrad Bishopdef collect_services(root): 280c342db35SBrad Bishop """Collect list of service files""" 281c342db35SBrad Bishop services = set() 282c342db35SBrad Bishop for location in locations: 283c342db35SBrad Bishop paths = (root / location / "system").glob("*") 284c342db35SBrad Bishop for path in paths: 285c342db35SBrad Bishop if path.is_dir(): 286c342db35SBrad Bishop continue 287c342db35SBrad Bishop services.add(path.name) 288c342db35SBrad Bishop 289c342db35SBrad Bishop return services 290c342db35SBrad Bishop 291c342db35SBrad Bishop 292c342db35SBrad Bishopdef preset_all(root): 293c342db35SBrad Bishop presets = Presets('system-preset', root) 294c342db35SBrad Bishop services = collect_services(root) 295c342db35SBrad Bishop 296c342db35SBrad Bishop for service in services: 297c342db35SBrad Bishop state = presets.state(service) 298c342db35SBrad Bishop 299c342db35SBrad Bishop if state == "enable" or state is None: 300ac69b488SWilliam A. Kennington III try: 301c342db35SBrad Bishop SystemdUnit(root, service).enable() 302ac69b488SWilliam A. Kennington III except SystemdUnitNotFoundError: 303ac69b488SWilliam A. Kennington III sys.exit("Error: Systemctl preset_all issue in %s" % service) 304c342db35SBrad Bishop 305c342db35SBrad Bishop # If we populate the systemd links we also create /etc/machine-id, which 306c342db35SBrad Bishop # allows systemd to boot with the filesystem read-only before generating 307c342db35SBrad Bishop # a real value and then committing it back. 308c342db35SBrad Bishop # 309c342db35SBrad Bishop # For the stateless configuration, where /etc is generated at runtime 310c342db35SBrad Bishop # (for example on a tmpfs), this script shouldn't run at all and we 311c342db35SBrad Bishop # allow systemd to completely populate /etc. 312fc113eadSAndrew Geissler (root / SYSCONFDIR / "machine-id").touch() 313c342db35SBrad Bishop 314c342db35SBrad Bishop 315c342db35SBrad Bishopdef main(): 316c342db35SBrad Bishop if sys.version_info < (3, 4, 0): 317c342db35SBrad Bishop sys.exit("Python 3.4 or greater is required") 318c342db35SBrad Bishop 319c342db35SBrad Bishop parser = argparse.ArgumentParser() 32009209eecSAndrew Geissler parser.add_argument('command', nargs='?', choices=['enable', 'mask', 321c342db35SBrad Bishop 'preset-all']) 322c342db35SBrad Bishop parser.add_argument('service', nargs=argparse.REMAINDER) 323c342db35SBrad Bishop parser.add_argument('--root') 324c342db35SBrad Bishop parser.add_argument('--preset-mode', 325c342db35SBrad Bishop choices=['full', 'enable-only', 'disable-only'], 326c342db35SBrad Bishop default='full') 327c342db35SBrad Bishop 328c342db35SBrad Bishop args = parser.parse_args() 329c342db35SBrad Bishop 330c342db35SBrad Bishop root = Path(args.root) if args.root else ROOT 331c342db35SBrad Bishop 332c342db35SBrad Bishop locations.append(SYSCONFDIR / "systemd") 333c342db35SBrad Bishop # Handle the usrmerge case by ignoring /lib when it's a symlink 334c342db35SBrad Bishop if not (root / BASE_LIBDIR).is_symlink(): 335c342db35SBrad Bishop locations.append(BASE_LIBDIR / "systemd") 336c342db35SBrad Bishop locations.append(LIBDIR / "systemd") 337c342db35SBrad Bishop 33809209eecSAndrew Geissler command = args.command 33909209eecSAndrew Geissler if not command: 34009209eecSAndrew Geissler parser.print_help() 34109209eecSAndrew Geissler return 0 34209209eecSAndrew Geissler 343c342db35SBrad Bishop if command == "mask": 344c342db35SBrad Bishop for service in args.service: 345ac69b488SWilliam A. Kennington III try: 346c342db35SBrad Bishop SystemdUnit(root, service).mask() 347ac69b488SWilliam A. Kennington III except SystemdUnitNotFoundError as e: 348ac69b488SWilliam A. Kennington III sys.exit("Error: Systemctl main mask issue in %s (%s)" % (service, e.unit)) 349c342db35SBrad Bishop elif command == "enable": 350c342db35SBrad Bishop for service in args.service: 351ac69b488SWilliam A. Kennington III try: 352c342db35SBrad Bishop SystemdUnit(root, service).enable() 353ac69b488SWilliam A. Kennington III except SystemdUnitNotFoundError as e: 354ac69b488SWilliam A. Kennington III sys.exit("Error: Systemctl main enable issue in %s (%s)" % (service, e.unit)) 355c342db35SBrad Bishop elif command == "preset-all": 356c342db35SBrad Bishop if len(args.service) != 0: 357c342db35SBrad Bishop sys.exit("Too many arguments.") 358c342db35SBrad Bishop if args.preset_mode != "enable-only": 359c342db35SBrad Bishop sys.exit("Only enable-only is supported as preset-mode.") 360c342db35SBrad Bishop preset_all(root) 361c342db35SBrad Bishop else: 362c342db35SBrad Bishop raise RuntimeError() 363c342db35SBrad Bishop 364c342db35SBrad Bishop 365c342db35SBrad Bishopif __name__ == '__main__': 366c342db35SBrad Bishop main() 367