ominfra 0.0.0.dev190__py3-none-any.whl → 0.0.0.dev191__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ominfra/scripts/manage.py CHANGED
@@ -7089,7 +7089,7 @@ DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
7089
7089
  ##
7090
7090
 
7091
7091
 
7092
- @dc.dataclass(frozen=True)
7092
+ @dc.dataclass(frozen=True, order=True)
7093
7093
  class DeployTag(abc.ABC): # noqa
7094
7094
  s: str
7095
7095
 
@@ -9268,20 +9268,63 @@ TODO:
9268
9268
  """
9269
9269
 
9270
9270
 
9271
+ ##
9272
+
9273
+
9271
9274
  class DeployConfManager:
9272
- def _render_app_conf_content(self, ac: DeployAppConfContent) -> str:
9275
+ def _process_conf_content(
9276
+ self,
9277
+ content: T,
9278
+ *,
9279
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
9280
+ ) -> T:
9281
+ def rec(o):
9282
+ if isinstance(o, str):
9283
+ if str_processor is not None:
9284
+ return type(o)(str_processor(o))
9285
+
9286
+ elif isinstance(o, collections.abc.Mapping):
9287
+ return type(o)([ # type: ignore
9288
+ (rec(k), rec(v))
9289
+ for k, v in o.items()
9290
+ ])
9291
+
9292
+ elif isinstance(o, collections.abc.Iterable):
9293
+ return type(o)([ # type: ignore
9294
+ rec(e) for e in o
9295
+ ])
9296
+
9297
+ return o
9298
+
9299
+ return rec(content)
9300
+
9301
+ #
9302
+
9303
+ def _render_app_conf_content(
9304
+ self,
9305
+ ac: DeployAppConfContent,
9306
+ *,
9307
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
9308
+ ) -> str:
9309
+ pcc = functools.partial(
9310
+ self._process_conf_content,
9311
+ str_processor=str_processor,
9312
+ )
9313
+
9273
9314
  if isinstance(ac, RawDeployAppConfContent):
9274
- return ac.body
9315
+ return pcc(ac.body)
9275
9316
 
9276
9317
  elif isinstance(ac, JsonDeployAppConfContent):
9277
- return strip_with_newline(json_dumps_pretty(ac.obj))
9318
+ json_obj = pcc(ac.obj)
9319
+ return strip_with_newline(json_dumps_pretty(json_obj))
9278
9320
 
9279
9321
  elif isinstance(ac, IniDeployAppConfContent):
9280
- return strip_with_newline(render_ini_config(ac.sections))
9322
+ ini_sections = pcc(ac.sections)
9323
+ return strip_with_newline(render_ini_config(ini_sections))
9281
9324
 
9282
9325
  elif isinstance(ac, NginxDeployAppConfContent):
9283
- ni = NginxConfigItems.of(ac.items)
9284
- return strip_with_newline(render_nginx_config_str(ni))
9326
+ nginx_items = NginxConfigItems.of(pcc(ac.items))
9327
+ return strip_with_newline(render_nginx_config_str(nginx_items))
9285
9328
 
9286
9329
  else:
9287
9330
  raise TypeError(ac)
@@ -9290,11 +9333,16 @@ class DeployConfManager:
9290
9333
  self,
9291
9334
  acf: DeployAppConfFile,
9292
9335
  app_conf_dir: str,
9336
+ *,
9337
+ str_processor: ta.Optional[ta.Callable[[str], str]] = None,
9293
9338
  ) -> None:
9294
9339
  conf_file = os.path.join(app_conf_dir, acf.path)
9295
9340
  check.arg(is_path_in_dir(app_conf_dir, conf_file))
9296
9341
 
9297
- body = self._render_app_conf_content(acf.content)
9342
+ body = self._render_app_conf_content(
9343
+ acf.content,
9344
+ str_processor=str_processor,
9345
+ )
9298
9346
 
9299
9347
  os.makedirs(os.path.dirname(conf_file), exist_ok=True)
9300
9348
 
@@ -9306,10 +9354,14 @@ class DeployConfManager:
9306
9354
  spec: DeployAppConfSpec,
9307
9355
  app_conf_dir: str,
9308
9356
  ) -> None:
9357
+ def process_str(s: str) -> str:
9358
+ return s
9359
+
9309
9360
  for acf in spec.files or []:
9310
9361
  await self._write_app_conf_file(
9311
9362
  acf,
9312
9363
  app_conf_dir,
9364
+ str_processor=process_str,
9313
9365
  )
9314
9366
 
9315
9367
  #
@@ -9552,6 +9604,13 @@ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
9552
9604
  return DeployAppKey(self._key_str())
9553
9605
 
9554
9606
 
9607
+ @dc.dataclass(frozen=True)
9608
+ class DeployAppLinksSpec:
9609
+ apps: ta.Sequence[DeployApp] = ()
9610
+
9611
+ exclude_unspecified: bool = False
9612
+
9613
+
9555
9614
  ##
9556
9615
 
9557
9616
 
@@ -9570,6 +9629,8 @@ class DeploySpec(DeploySpecKeyed[DeployKey]):
9570
9629
 
9571
9630
  apps: ta.Sequence[DeployAppSpec] = ()
9572
9631
 
9632
+ app_links: DeployAppLinksSpec = DeployAppLinksSpec()
9633
+
9573
9634
  systemd: ta.Optional[DeploySystemdSpec] = None
9574
9635
 
9575
9636
  def __post_init__(self) -> None:
@@ -10975,31 +11036,12 @@ def bind_deploy_paths() -> InjectorBindings:
10975
11036
  ########################################
10976
11037
  # ../deploy/systemd.py
10977
11038
  """
10978
- ~/.config/systemd/user/
10979
-
10980
- verify - systemd-analyze
10981
-
10982
- sudo loginctl enable-linger "$USER"
10983
-
10984
- cat ~/.config/systemd/user/sleep-infinity.service
10985
- [Unit]
10986
- Description=User-specific service to run 'sleep infinity'
10987
- After=default.target
10988
-
10989
- [Service]
10990
- ExecStart=/bin/sleep infinity
10991
- Restart=always
10992
- RestartSec=5
10993
-
10994
- [Install]
10995
- WantedBy=default.target
10996
-
10997
- systemctl --user daemon-reload
10998
-
10999
- systemctl --user enable sleep-infinity.service
11000
- systemctl --user start sleep-infinity.service
11001
-
11002
- systemctl --user status sleep-infinity.service
11039
+ TODO:
11040
+ - verify - systemd-analyze
11041
+ - sudo loginctl enable-linger "$USER"
11042
+ - idemp kill services that shouldn't be running, start ones that should
11043
+ - ideally only those defined by links to deploy home
11044
+ - ominfra.systemd / x.sd_orphans
11003
11045
  """
11004
11046
 
11005
11047
 
@@ -11038,6 +11080,8 @@ class DeploySystemdManager:
11038
11080
  if not spec:
11039
11081
  return
11040
11082
 
11083
+ #
11084
+
11041
11085
  if not (ud := spec.unit_dir):
11042
11086
  return
11043
11087
 
@@ -11045,6 +11089,8 @@ class DeploySystemdManager:
11045
11089
 
11046
11090
  os.makedirs(ud, exist_ok=True)
11047
11091
 
11092
+ #
11093
+
11048
11094
  uld = {
11049
11095
  n: p
11050
11096
  for n, p in self._scan_link_dir(ud).items()
@@ -11056,8 +11102,11 @@ class DeploySystemdManager:
11056
11102
  else:
11057
11103
  cld = {}
11058
11104
 
11059
- for n in sorted(set(uld) | set(cld)):
11060
- ul = uld.get(n) # noqa
11105
+ #
11106
+
11107
+ ns = sorted(set(uld) | set(cld))
11108
+
11109
+ for n in ns:
11061
11110
  cl = cld.get(n)
11062
11111
  if cl is None:
11063
11112
  os.unlink(os.path.join(ud, n))
@@ -11074,6 +11123,37 @@ class DeploySystemdManager:
11074
11123
  dst_swap.tmp_path,
11075
11124
  )
11076
11125
 
11126
+ #
11127
+
11128
+ if sys.platform == 'linux':
11129
+ async def reload() -> None:
11130
+ await asyncio_subprocesses.check_call('systemctl', '--user', 'daemon-reload')
11131
+
11132
+ await reload()
11133
+
11134
+ num_deleted = 0
11135
+ for n in ns:
11136
+ if n.endswith('.service'):
11137
+ cl = cld.get(n)
11138
+ ul = uld.get(n)
11139
+ if cl is not None:
11140
+ if ul is None:
11141
+ cs = ['enable', 'start']
11142
+ else:
11143
+ cs = ['restart']
11144
+ else: # noqa
11145
+ if ul is not None:
11146
+ cs = ['stop']
11147
+ num_deleted += 1
11148
+ else:
11149
+ cs = []
11150
+
11151
+ for c in cs:
11152
+ await asyncio_subprocesses.check_call('systemctl', '--user', c, n)
11153
+
11154
+ if num_deleted:
11155
+ await reload()
11156
+
11077
11157
 
11078
11158
  ########################################
11079
11159
  # ../remote/inject.py
@@ -11455,12 +11535,15 @@ class DeployVenvManager:
11455
11535
 
11456
11536
  if os.path.isfile(reqs_txt):
11457
11537
  if spec.use_uv:
11458
- await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
11459
- pip_cmd = ['-m', 'uv', 'pip']
11538
+ if shutil.which('uv') is not None:
11539
+ pip_cmd = ['uv', 'pip']
11540
+ else:
11541
+ await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
11542
+ pip_cmd = [venv_exe, '-m', 'uv', 'pip']
11460
11543
  else:
11461
- pip_cmd = ['-m', 'pip']
11544
+ pip_cmd = [venv_exe, '-m', 'pip']
11462
11545
 
11463
- await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
11546
+ await asyncio_subprocesses.check_call(*pip_cmd, 'install', '-r', reqs_txt, cwd=venv_dir)
11464
11547
 
11465
11548
 
11466
11549
  ########################################
@@ -11506,9 +11589,21 @@ class DeployAppManager(DeployPathOwner):
11506
11589
 
11507
11590
  #
11508
11591
 
11592
+ def _make_tags(self, spec: DeployAppSpec) -> DeployTagMap:
11593
+ return DeployTagMap(
11594
+ spec.app,
11595
+ spec.key(),
11596
+ DeployAppRev(spec.git.rev),
11597
+ )
11598
+
11599
+ #
11600
+
11509
11601
  @dc.dataclass(frozen=True)
11510
11602
  class PreparedApp:
11511
- app_dir: str
11603
+ spec: DeployAppSpec
11604
+ tags: DeployTagMap
11605
+
11606
+ dir: str
11512
11607
 
11513
11608
  git_dir: ta.Optional[str] = None
11514
11609
  venv_dir: ta.Optional[str] = None
@@ -11524,16 +11619,23 @@ class DeployAppManager(DeployPathOwner):
11524
11619
 
11525
11620
  #
11526
11621
 
11622
+ app_tags = tags.add(*self._make_tags(spec))
11623
+
11624
+ #
11625
+
11527
11626
  check.non_empty_str(home)
11528
11627
 
11529
- app_dir = os.path.join(home, self.APP_DIR.render(tags))
11628
+ app_dir = os.path.join(home, self.APP_DIR.render(app_tags))
11530
11629
 
11531
11630
  os.makedirs(app_dir, exist_ok=True)
11532
11631
 
11533
11632
  #
11534
11633
 
11535
11634
  rkw: ta.Dict[str, ta.Any] = dict(
11536
- app_dir=app_dir,
11635
+ spec=spec,
11636
+ tags=app_tags,
11637
+
11638
+ dir=app_dir,
11537
11639
  )
11538
11640
 
11539
11641
  #
@@ -11577,6 +11679,54 @@ class DeployAppManager(DeployPathOwner):
11577
11679
 
11578
11680
  return DeployAppManager.PreparedApp(**rkw)
11579
11681
 
11682
+ async def prepare_app_link(
11683
+ self,
11684
+ tags: DeployTagMap,
11685
+ app_dir: str,
11686
+ ) -> PreparedApp:
11687
+ spec_file = os.path.join(app_dir, 'spec.json')
11688
+ with open(spec_file) as f: # noqa
11689
+ spec_json = f.read()
11690
+
11691
+ spec: DeployAppSpec = self._msh.unmarshal_obj(json.loads(spec_json), DeployAppSpec)
11692
+
11693
+ #
11694
+
11695
+ app_tags = tags.add(*self._make_tags(spec))
11696
+
11697
+ #
11698
+
11699
+ rkw: ta.Dict[str, ta.Any] = dict(
11700
+ spec=spec,
11701
+ tags=app_tags,
11702
+
11703
+ dir=app_dir,
11704
+ )
11705
+
11706
+ #
11707
+
11708
+ git_dir = os.path.join(app_dir, 'git')
11709
+ check.state(os.path.isdir(git_dir))
11710
+ rkw.update(git_dir=git_dir)
11711
+
11712
+ #
11713
+
11714
+ if spec.venv is not None:
11715
+ venv_dir = os.path.join(app_dir, 'venv')
11716
+ check.state(os.path.isdir(venv_dir))
11717
+ rkw.update(venv_dir=venv_dir)
11718
+
11719
+ #
11720
+
11721
+ if spec.conf is not None:
11722
+ conf_dir = os.path.join(app_dir, 'conf')
11723
+ check.state(os.path.isdir(conf_dir))
11724
+ rkw.update(conf_dir=conf_dir)
11725
+
11726
+ #
11727
+
11728
+ return DeployAppManager.PreparedApp(**rkw)
11729
+
11580
11730
 
11581
11731
  ########################################
11582
11732
  # ../deploy/deploy.py
@@ -11595,16 +11745,24 @@ class DeployManager(DeployPathOwner):
11595
11745
  def __init__(
11596
11746
  self,
11597
11747
  *,
11748
+ atomics: DeployHomeAtomics,
11749
+
11598
11750
  utc_clock: ta.Optional[DeployManagerUtcClock] = None,
11599
11751
  ):
11600
11752
  super().__init__()
11601
11753
 
11754
+ self._atomics = atomics
11755
+
11602
11756
  self._utc_clock = utc_clock
11603
11757
 
11604
11758
  #
11605
11759
 
11760
+ # Home current link just points to CURRENT_DEPLOY_LINK, and is intended for user convenience.
11761
+ HOME_CURRENT_LINK = DeployPath.parse('current')
11762
+
11606
11763
  DEPLOYS_DIR = DeployPath.parse('deploys/')
11607
11764
 
11765
+ # Authoritative current symlink is not in deploy-home, just to prevent accidental corruption.
11608
11766
  CURRENT_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}current')
11609
11767
  DEPLOYING_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}deploying')
11610
11768
 
@@ -11637,6 +11795,11 @@ class DeployManager(DeployPathOwner):
11637
11795
 
11638
11796
  #
11639
11797
 
11798
+ def render_path(self, home: DeployHome, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
11799
+ return os.path.join(check.non_empty_str(home), pth.render(tags))
11800
+
11801
+ #
11802
+
11640
11803
  def _utc_now(self) -> datetime.datetime:
11641
11804
  if self._utc_clock is not None:
11642
11805
  return self._utc_clock() # noqa
@@ -11646,6 +11809,22 @@ class DeployManager(DeployPathOwner):
11646
11809
  def make_deploy_time(self) -> DeployTime:
11647
11810
  return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
11648
11811
 
11812
+ #
11813
+
11814
+ def make_home_current_link(self, home: DeployHome) -> None:
11815
+ home_current_link = os.path.join(check.non_empty_str(home), self.HOME_CURRENT_LINK.render())
11816
+ current_deploy_link = os.path.join(check.non_empty_str(home), self.CURRENT_DEPLOY_LINK.render())
11817
+ with self._atomics(home).begin_atomic_path_swap( # noqa
11818
+ 'file',
11819
+ home_current_link,
11820
+ auto_commit=True,
11821
+ ) as dst_swap:
11822
+ os.unlink(dst_swap.tmp_path)
11823
+ os.symlink(
11824
+ os.path.relpath(current_deploy_link, os.path.dirname(dst_swap.dst_path)),
11825
+ dst_swap.tmp_path,
11826
+ )
11827
+
11649
11828
 
11650
11829
  ##
11651
11830
 
@@ -11687,18 +11866,18 @@ class DeployDriver:
11687
11866
  #
11688
11867
 
11689
11868
  @property
11690
- def deploy_tags(self) -> DeployTagMap:
11869
+ def tags(self) -> DeployTagMap:
11691
11870
  return DeployTagMap(
11692
11871
  self._time,
11693
11872
  self._spec.key(),
11694
11873
  )
11695
11874
 
11696
- def render_deploy_path(self, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
11697
- return os.path.join(self._home, pth.render(tags if tags is not None else self.deploy_tags))
11875
+ def render_path(self, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
11876
+ return os.path.join(self._home, pth.render(tags if tags is not None else self.tags))
11698
11877
 
11699
11878
  @property
11700
- def deploy_dir(self) -> str:
11701
- return self.render_deploy_path(self._deploys.DEPLOY_DIR)
11879
+ def dir(self) -> str:
11880
+ return self.render_path(self._deploys.DEPLOY_DIR)
11702
11881
 
11703
11882
  #
11704
11883
 
@@ -11707,25 +11886,36 @@ class DeployDriver:
11707
11886
 
11708
11887
  #
11709
11888
 
11889
+ das: ta.Set[DeployApp] = {a.app for a in self._spec.apps}
11890
+ las: ta.Set[DeployApp] = set(self._spec.app_links.apps)
11891
+ if (ras := das & las):
11892
+ raise RuntimeError(f'Must not specify apps as both deploy and link: {sorted(a.s for a in ras)}')
11893
+
11894
+ #
11895
+
11710
11896
  self._paths.validate_deploy_paths()
11711
11897
 
11712
11898
  #
11713
11899
 
11714
- os.makedirs(self.deploy_dir)
11900
+ os.makedirs(self.dir)
11715
11901
 
11716
11902
  #
11717
11903
 
11718
- spec_file = self.render_deploy_path(self._deploys.DEPLOY_SPEC_FILE)
11904
+ spec_file = self.render_path(self._deploys.DEPLOY_SPEC_FILE)
11719
11905
  with open(spec_file, 'w') as f: # noqa
11720
11906
  f.write(spec_json)
11721
11907
 
11722
11908
  #
11723
11909
 
11724
- deploying_link = self.render_deploy_path(self._deploys.DEPLOYING_DEPLOY_LINK)
11910
+ deploying_link = self.render_path(self._deploys.DEPLOYING_DEPLOY_LINK)
11911
+ current_link = self.render_path(self._deploys.CURRENT_DEPLOY_LINK)
11912
+
11913
+ #
11914
+
11725
11915
  if os.path.exists(deploying_link):
11726
11916
  os.unlink(deploying_link)
11727
11917
  relative_symlink(
11728
- self.deploy_dir,
11918
+ self.dir,
11729
11919
  deploying_link,
11730
11920
  target_is_directory=True,
11731
11921
  make_dirs=True,
@@ -11737,16 +11927,30 @@ class DeployDriver:
11737
11927
  self._deploys.APPS_DEPLOY_DIR,
11738
11928
  self._deploys.CONFS_DEPLOY_DIR,
11739
11929
  ]:
11740
- os.makedirs(self.render_deploy_path(md))
11930
+ os.makedirs(self.render_path(md))
11741
11931
 
11742
11932
  #
11743
11933
 
11934
+ if not self._spec.app_links.exclude_unspecified:
11935
+ cad = abs_real_path(os.path.join(current_link, 'apps'))
11936
+ if os.path.exists(cad):
11937
+ for d in os.listdir(cad):
11938
+ if (da := DeployApp(d)) not in das:
11939
+ las.add(da)
11940
+
11941
+ for la in self._spec.app_links.apps:
11942
+ await self._drive_app_link(
11943
+ la,
11944
+ current_link,
11945
+ )
11946
+
11744
11947
  for app in self._spec.apps:
11745
- await self.drive_app_deploy(app)
11948
+ await self._drive_app_deploy(
11949
+ app,
11950
+ )
11746
11951
 
11747
11952
  #
11748
11953
 
11749
- current_link = self.render_deploy_path(self._deploys.CURRENT_DEPLOY_LINK)
11750
11954
  os.replace(deploying_link, current_link)
11751
11955
 
11752
11956
  #
@@ -11754,31 +11958,27 @@ class DeployDriver:
11754
11958
  await self._systemd.sync_systemd(
11755
11959
  self._spec.systemd,
11756
11960
  self._home,
11757
- os.path.join(self.deploy_dir, 'conf', 'systemd'), # FIXME
11961
+ os.path.join(self.dir, 'conf', 'systemd'), # FIXME
11758
11962
  )
11759
11963
 
11760
- #
11964
+ #
11761
11965
 
11762
- async def drive_app_deploy(self, app: DeployAppSpec) -> None:
11763
- app_tags = self.deploy_tags.add(
11764
- app.app,
11765
- app.key(),
11766
- DeployAppRev(app.git.rev),
11767
- )
11966
+ self._deploys.make_home_current_link(self._home)
11768
11967
 
11769
- #
11968
+ #
11770
11969
 
11771
- da = await self._apps.prepare_app(
11970
+ async def _drive_app_deploy(self, app: DeployAppSpec) -> None:
11971
+ pa = await self._apps.prepare_app(
11772
11972
  app,
11773
11973
  self._home,
11774
- app_tags,
11974
+ self.tags,
11775
11975
  )
11776
11976
 
11777
11977
  #
11778
11978
 
11779
- app_link = self.render_deploy_path(self._deploys.APP_DEPLOY_LINK, app_tags)
11979
+ app_link = self.render_path(self._deploys.APP_DEPLOY_LINK, pa.tags)
11780
11980
  relative_symlink(
11781
- da.app_dir,
11981
+ pa.dir,
11782
11982
  app_link,
11783
11983
  target_is_directory=True,
11784
11984
  make_dirs=True,
@@ -11786,12 +11986,48 @@ class DeployDriver:
11786
11986
 
11787
11987
  #
11788
11988
 
11789
- deploy_conf_dir = self.render_deploy_path(self._deploys.CONFS_DEPLOY_DIR)
11790
- if app.conf is not None:
11989
+ await self._drive_app_configure(pa)
11990
+
11991
+ async def _drive_app_link(
11992
+ self,
11993
+ app: DeployApp,
11994
+ current_link: str,
11995
+ ) -> None:
11996
+ app_link = os.path.join(abs_real_path(current_link), 'apps', app.s)
11997
+ check.state(os.path.islink(app_link))
11998
+
11999
+ app_dir = abs_real_path(app_link)
12000
+ check.state(os.path.isdir(app_dir))
12001
+
12002
+ #
12003
+
12004
+ pa = await self._apps.prepare_app_link(
12005
+ self.tags,
12006
+ app_dir,
12007
+ )
12008
+
12009
+ #
12010
+
12011
+ relative_symlink(
12012
+ app_dir,
12013
+ os.path.join(self.dir, 'apps', app.s),
12014
+ target_is_directory=True,
12015
+ )
12016
+
12017
+ #
12018
+
12019
+ await self._drive_app_configure(pa)
12020
+
12021
+ async def _drive_app_configure(
12022
+ self,
12023
+ pa: DeployAppManager.PreparedApp,
12024
+ ) -> None:
12025
+ deploy_conf_dir = self.render_path(self._deploys.CONFS_DEPLOY_DIR)
12026
+ if pa.spec.conf is not None:
11791
12027
  await self._conf.link_app_conf(
11792
- app.conf,
11793
- app_tags,
11794
- check.non_empty_str(da.conf_dir),
12028
+ pa.spec.conf,
12029
+ pa.tags,
12030
+ check.non_empty_str(pa.conf_dir),
11795
12031
  deploy_conf_dir,
11796
12032
  )
11797
12033
 
ominfra/systemd.py ADDED
@@ -0,0 +1,49 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import dataclasses as dc
4
+ import typing as ta
5
+
6
+
7
+ ##
8
+
9
+
10
+ @dc.dataclass(frozen=True)
11
+ class SystemdListUnit:
12
+ unit: str
13
+ load: str # loaded, not-found
14
+ active: str # active, inactive
15
+ sub: str # running, exited, dead
16
+ description: str
17
+
18
+ @classmethod
19
+ def parse(cls, s: str) -> 'SystemdListUnit':
20
+ return SystemdListUnit(*[p.strip() for p in s.strip().split(None, 4)])
21
+
22
+ @classmethod
23
+ def parse_all(cls, s: str) -> ta.List['SystemdListUnit']:
24
+ return [
25
+ cls.parse(sl)
26
+ for l in s.strip().splitlines()
27
+ if (sl := l.strip())
28
+ ]
29
+
30
+
31
+ PARSABLE_SYSTEMD_LIST_UNIT_ARGS: ta.Sequence[str] = [
32
+ '--all',
33
+ '--no-legend',
34
+ '--no-pager',
35
+ '--plain',
36
+ ]
37
+
38
+
39
+ ##
40
+
41
+
42
+ def parse_systemd_show_output(s: str) -> ta.Mapping[str, str]:
43
+ d: ta.Dict[str, str] = {}
44
+ for l in s.strip().splitlines():
45
+ if not (l := l.strip()):
46
+ continue
47
+ k, _, v = l.partition('=')
48
+ d[k] = v
49
+ return d
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ominfra
3
- Version: 0.0.0.dev190
3
+ Version: 0.0.0.dev191
4
4
  Summary: ominfra
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -12,9 +12,9 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Operating System :: POSIX
13
13
  Requires-Python: >=3.12
14
14
  License-File: LICENSE
15
- Requires-Dist: omdev==0.0.0.dev190
16
- Requires-Dist: omlish==0.0.0.dev190
17
- Requires-Dist: omserv==0.0.0.dev190
15
+ Requires-Dist: omdev==0.0.0.dev191
16
+ Requires-Dist: omlish==0.0.0.dev191
17
+ Requires-Dist: omserv==0.0.0.dev191
18
18
  Provides-Extra: all
19
19
  Requires-Dist: paramiko~=3.5; extra == "all"
20
20
  Requires-Dist: asyncssh~=2.18; extra == "all"