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

Sign up to get free protection for your applications and to get access to all the features.
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"