ominfra 0.0.0.dev144__py3-none-any.whl → 0.0.0.dev146__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.
@@ -8,7 +8,7 @@ from .base import CommandExecutor
8
8
  CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
9
9
 
10
10
 
11
- class CommandExecutionService(CommandExecutor):
11
+ class LocalCommandExecutor(CommandExecutor):
12
12
  def __init__(
13
13
  self,
14
14
  *,
@@ -18,8 +18,10 @@ from .base import CommandNameMap
18
18
  from .base import CommandRegistration
19
19
  from .base import CommandRegistrations
20
20
  from .base import build_command_name_map
21
- from .execution import CommandExecutionService
22
21
  from .execution import CommandExecutorMap
22
+ from .execution import LocalCommandExecutor
23
+ from .interp import InterpCommand
24
+ from .interp import InterpCommandExecutor
23
25
  from .marshal import install_command_marshaling
24
26
  from .subprocess import SubprocessCommand
25
27
  from .subprocess import SubprocessCommandExecutor
@@ -106,14 +108,16 @@ def bind_commands(
106
108
  lst.extend([
107
109
  inj.bind(provide_command_executor_map, singleton=True),
108
110
 
109
- inj.bind(CommandExecutionService, singleton=True, eager=main_config.debug),
110
- inj.bind(CommandExecutor, to_key=CommandExecutionService),
111
+ inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
111
112
  ])
112
113
 
113
114
  #
114
115
 
116
+ command_cls: ta.Any
117
+ executor_cls: ta.Any
115
118
  for command_cls, executor_cls in [
116
119
  (SubprocessCommand, SubprocessCommandExecutor),
120
+ (InterpCommand, InterpCommandExecutor),
117
121
  ]:
118
122
  lst.append(bind_command(command_cls, executor_cls))
119
123
 
@@ -0,0 +1,39 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import dataclasses as dc
3
+
4
+ from omdev.interp.resolvers import DEFAULT_INTERP_RESOLVER
5
+ from omdev.interp.types import InterpOpts
6
+ from omdev.interp.types import InterpSpecifier
7
+ from omlish.lite.check import check_not_none
8
+
9
+ from ..commands.base import Command
10
+ from ..commands.base import CommandExecutor
11
+
12
+
13
+ ##
14
+
15
+
16
+ @dc.dataclass(frozen=True)
17
+ class InterpCommand(Command['InterpCommand.Output']):
18
+ spec: str
19
+ install: bool = False
20
+
21
+ @dc.dataclass(frozen=True)
22
+ class Output(Command.Output):
23
+ exe: str
24
+ version: str
25
+ opts: InterpOpts
26
+
27
+
28
+ ##
29
+
30
+
31
+ class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
32
+ def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
33
+ i = InterpSpecifier.parse(check_not_none(cmd.spec))
34
+ o = check_not_none(DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
35
+ return InterpCommand.Output(
36
+ exe=o.exe,
37
+ version=str(o.version.version),
38
+ opts=o.version.opts,
39
+ )
@@ -5,6 +5,7 @@ import subprocess
5
5
  import time
6
6
  import typing as ta
7
7
 
8
+ from omlish.lite.check import check_not_isinstance
8
9
  from omlish.lite.subprocesses import SUBPROCESS_CHANNEL_OPTION_VALUES
9
10
  from omlish.lite.subprocesses import SubprocessChannelOption
10
11
  from omlish.lite.subprocesses import subprocess_maybe_shell_wrap_exec
@@ -31,8 +32,7 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
31
32
  timeout: ta.Optional[float] = None
32
33
 
33
34
  def __post_init__(self) -> None:
34
- if isinstance(self.cmd, str):
35
- raise TypeError(self.cmd)
35
+ check_not_isinstance(self.cmd, str)
36
36
 
37
37
  @dc.dataclass(frozen=True)
38
38
  class Output(Command.Output):
@@ -1,6 +1,8 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import dataclasses as dc
3
3
 
4
+ from omlish.lite.logs import log
5
+
4
6
  from ..commands.base import Command
5
7
  from ..commands.base import CommandExecutor
6
8
 
@@ -20,4 +22,6 @@ class DeployCommand(Command['DeployCommand.Output']):
20
22
 
21
23
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
22
24
  def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
25
+ log.info('Deploying!')
26
+
23
27
  return DeployCommand.Output()
@@ -0,0 +1,180 @@
1
+ # ruff: noqa: UP006 UP007
2
+ """
3
+ ~deploy
4
+ deploy.pid (flock)
5
+ /app
6
+ /<appspec> - shallow clone
7
+ /conf
8
+ /env
9
+ <appspec>.env
10
+ /nginx
11
+ <appspec>.conf
12
+ /supervisor
13
+ <appspec>.conf
14
+ /venv
15
+ /<appspec>
16
+
17
+ ?
18
+ /logs
19
+ /wrmsr--omlish--<spec>
20
+
21
+ spec = <name>--<rev>--<when>
22
+
23
+ ==
24
+
25
+ for dn in [
26
+ 'app',
27
+ 'conf',
28
+ 'conf/env',
29
+ 'conf/nginx',
30
+ 'conf/supervisor',
31
+ 'venv',
32
+ ]:
33
+ """
34
+ import abc
35
+ import dataclasses as dc
36
+ import os.path
37
+ import typing as ta
38
+
39
+ from omlish.lite.check import check_equal
40
+ from omlish.lite.check import check_non_empty
41
+ from omlish.lite.check import check_non_empty_str
42
+ from omlish.lite.check import check_not_in
43
+
44
+
45
+ DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
46
+
47
+
48
+ ##
49
+
50
+
51
+ DEPLOY_PATH_SPEC_PLACEHOLDER = '@'
52
+
53
+
54
+ @dc.dataclass(frozen=True)
55
+ class DeployPathPart(abc.ABC): # noqa
56
+ @property
57
+ @abc.abstractmethod
58
+ def kind(self) -> DeployPathKind:
59
+ raise NotImplementedError
60
+
61
+ @abc.abstractmethod
62
+ def render(self) -> str:
63
+ raise NotImplementedError
64
+
65
+
66
+ #
67
+
68
+
69
+ class DeployPathDir(DeployPathPart, abc.ABC):
70
+ @property
71
+ def kind(self) -> DeployPathKind:
72
+ return 'dir'
73
+
74
+ @classmethod
75
+ def parse(cls, s: str) -> 'DeployPathDir':
76
+ if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
77
+ check_equal(s, DEPLOY_PATH_SPEC_PLACEHOLDER)
78
+ return SpecDeployPathDir()
79
+ else:
80
+ return ConstDeployPathDir(s)
81
+
82
+
83
+ class DeployPathFile(DeployPathPart, abc.ABC):
84
+ @property
85
+ def kind(self) -> DeployPathKind:
86
+ return 'file'
87
+
88
+ @classmethod
89
+ def parse(cls, s: str) -> 'DeployPathFile':
90
+ if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
91
+ check_equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
92
+ return SpecDeployPathFile(s[1:])
93
+ else:
94
+ return ConstDeployPathFile(s)
95
+
96
+
97
+ #
98
+
99
+
100
+ @dc.dataclass(frozen=True)
101
+ class ConstDeployPathPart(DeployPathPart, abc.ABC):
102
+ name: str
103
+
104
+ def __post_init__(self) -> None:
105
+ check_non_empty_str(self.name)
106
+ check_not_in('/', self.name)
107
+ check_not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.name)
108
+
109
+ def render(self) -> str:
110
+ return self.name
111
+
112
+
113
+ class ConstDeployPathDir(ConstDeployPathPart, DeployPathDir):
114
+ pass
115
+
116
+
117
+ class ConstDeployPathFile(ConstDeployPathPart, DeployPathFile):
118
+ pass
119
+
120
+
121
+ #
122
+
123
+
124
+ class SpecDeployPathPart(DeployPathPart, abc.ABC):
125
+ pass
126
+
127
+
128
+ class SpecDeployPathDir(SpecDeployPathPart, DeployPathDir):
129
+ def render(self) -> str:
130
+ return DEPLOY_PATH_SPEC_PLACEHOLDER
131
+
132
+
133
+ @dc.dataclass(frozen=True)
134
+ class SpecDeployPathFile(SpecDeployPathPart, DeployPathFile):
135
+ suffix: str
136
+
137
+ def __post_init__(self) -> None:
138
+ check_non_empty_str(self.suffix)
139
+ check_not_in('/', self.suffix)
140
+ check_not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.suffix)
141
+
142
+ def render(self) -> str:
143
+ return DEPLOY_PATH_SPEC_PLACEHOLDER + self.suffix
144
+
145
+
146
+ ##
147
+
148
+
149
+ @dc.dataclass(frozen=True)
150
+ class DeployPath:
151
+ parts: ta.Sequence[DeployPathPart]
152
+
153
+ def __post_init__(self) -> None:
154
+ check_non_empty(self.parts)
155
+ for p in self.parts[:-1]:
156
+ check_equal(p.kind, 'dir')
157
+
158
+ @property
159
+ def kind(self) -> ta.Literal['file', 'dir']:
160
+ return self.parts[-1].kind
161
+
162
+ def render(self) -> str:
163
+ return os.path.join( # noqa
164
+ *[p.render() for p in self.parts],
165
+ *([''] if self.kind == 'dir' else []),
166
+ )
167
+
168
+ @classmethod
169
+ def parse(cls, s: str) -> 'DeployPath':
170
+ tail_parse: ta.Callable[[str], DeployPathPart]
171
+ if s.endswith('/'):
172
+ tail_parse = DeployPathDir.parse
173
+ s = s[:-1]
174
+ else:
175
+ tail_parse = DeployPathFile.parse
176
+ ps = check_non_empty_str(s).split('/')
177
+ return cls([
178
+ *([DeployPathDir.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
179
+ tail_parse(ps[-1]),
180
+ ])
ominfra/manage/main.py CHANGED
@@ -6,6 +6,7 @@ manage.py -s 'docker run -i python:3.12'
6
6
  manage.py -s 'ssh -i /foo/bar.pem foo@bar.baz' -q --python=python3.8
7
7
  """
8
8
  import contextlib
9
+ import json
9
10
  import typing as ta
10
11
 
11
12
  from omlish.lite.logs import log # noqa
@@ -17,9 +18,8 @@ from .bootstrap import MainBootstrap
17
18
  from .bootstrap_ import main_bootstrap
18
19
  from .commands.base import Command
19
20
  from .commands.base import CommandExecutor
20
- from .commands.subprocess import SubprocessCommand
21
+ from .commands.execution import LocalCommandExecutor
21
22
  from .config import MainConfig
22
- from .deploy.command import DeployCommand
23
23
  from .remote.config import RemoteConfig
24
24
  from .remote.execution import RemoteExecution
25
25
  from .remote.spawning import RemoteSpawning
@@ -77,12 +77,15 @@ def _main() -> None:
77
77
 
78
78
  #
79
79
 
80
+ msh = injector[ObjMarshalerManager]
81
+
80
82
  cmds: ta.List[Command] = []
83
+ cmd: Command
81
84
  for c in args.command:
82
- if c == 'deploy':
83
- cmds.append(DeployCommand())
84
- else:
85
- cmds.append(SubprocessCommand([c]))
85
+ if not c.startswith('{'):
86
+ c = json.dumps({c: {}})
87
+ cmd = msh.unmarshal_obj(json.loads(c), Command)
88
+ cmds.append(cmd)
86
89
 
87
90
  #
88
91
 
@@ -90,7 +93,7 @@ def _main() -> None:
90
93
  ce: CommandExecutor
91
94
 
92
95
  if args.local:
93
- ce = injector[CommandExecutor]
96
+ ce = injector[LocalCommandExecutor]
94
97
 
95
98
  else:
96
99
  tgt = RemoteSpawning.Target(
@@ -102,9 +105,13 @@ def _main() -> None:
102
105
  ce = es.enter_context(injector[RemoteExecution].connect(tgt, bs)) # noqa
103
106
 
104
107
  for cmd in cmds:
105
- r = ce.try_execute(cmd)
108
+ r = ce.try_execute(
109
+ cmd,
110
+ log=log,
111
+ omit_exc_object=True,
112
+ )
106
113
 
107
- print(injector[ObjMarshalerManager].marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
114
+ print(msh.marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
108
115
 
109
116
 
110
117
  if __name__ == '__main__':
@@ -1,6 +1,7 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import json
3
3
  import struct
4
+ import threading
4
5
  import typing as ta
5
6
 
6
7
  from omlish.lite.json import json_dumps_compact
@@ -25,10 +26,12 @@ class RemoteChannel:
25
26
  self._output = output
26
27
  self._msh = msh
27
28
 
29
+ self._lock = threading.RLock()
30
+
28
31
  def set_marshaler(self, msh: ObjMarshalerManager) -> None:
29
32
  self._msh = msh
30
33
 
31
- def send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
34
+ def _send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
32
35
  j = json_dumps_compact(self._msh.marshal_obj(o, ty))
33
36
  d = j.encode('utf-8')
34
37
 
@@ -36,7 +39,11 @@ class RemoteChannel:
36
39
  self._output.write(d)
37
40
  self._output.flush()
38
41
 
39
- def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
42
+ def send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
43
+ with self._lock:
44
+ return self._send_obj(o, ty)
45
+
46
+ def _recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
40
47
  d = self._input.read(4)
41
48
  if not d:
42
49
  return None
@@ -50,3 +57,7 @@ class RemoteChannel:
50
57
 
51
58
  j = json.loads(d.decode('utf-8'))
52
59
  return self._msh.unmarshal_obj(j, ty)
60
+
61
+ def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
62
+ with self._lock:
63
+ return self._recv_obj(ty)
@@ -2,6 +2,7 @@
2
2
  import contextlib
3
3
  import dataclasses as dc
4
4
  import logging
5
+ import threading
5
6
  import typing as ta
6
7
 
7
8
  from omlish.lite.cached import cached_nullary
@@ -20,6 +21,7 @@ from ..commands.base import CommandException
20
21
  from ..commands.base import CommandExecutor
21
22
  from ..commands.base import CommandOutputOrException
22
23
  from ..commands.base import CommandOutputOrExceptionData
24
+ from ..commands.execution import LocalCommandExecutor
23
25
  from .channel import RemoteChannel
24
26
  from .payload import RemoteExecutionPayloadFile
25
27
  from .payload import get_remote_payload_src
@@ -35,6 +37,32 @@ else:
35
37
  ##
36
38
 
37
39
 
40
+ class _RemoteExecutionLogHandler(logging.Handler):
41
+ def __init__(self, fn: ta.Callable[[str], None]) -> None:
42
+ super().__init__()
43
+ self._fn = fn
44
+
45
+ def emit(self, record):
46
+ msg = self.format(record)
47
+ self._fn(msg)
48
+
49
+
50
+ @dc.dataclass(frozen=True)
51
+ class _RemoteExecutionRequest:
52
+ c: Command
53
+
54
+
55
+ @dc.dataclass(frozen=True)
56
+ class _RemoteExecutionLog:
57
+ s: str
58
+
59
+
60
+ @dc.dataclass(frozen=True)
61
+ class _RemoteExecutionResponse:
62
+ r: ta.Optional[CommandOutputOrExceptionData] = None
63
+ l: ta.Optional[_RemoteExecutionLog] = None
64
+
65
+
38
66
  def _remote_execution_main() -> None:
39
67
  rt = pyremote_bootstrap_finalize() # noqa
40
68
 
@@ -52,20 +80,44 @@ def _remote_execution_main() -> None:
52
80
 
53
81
  chan.set_marshaler(injector[ObjMarshalerManager])
54
82
 
55
- ce = injector[CommandExecutor]
83
+ #
84
+
85
+ log_lock = threading.RLock()
86
+ send_logs = False
87
+
88
+ def log_fn(s: str) -> None:
89
+ with log_lock:
90
+ if send_logs:
91
+ chan.send_obj(_RemoteExecutionResponse(l=_RemoteExecutionLog(s)))
92
+
93
+ log_handler = _RemoteExecutionLogHandler(log_fn)
94
+ logging.root.addHandler(log_handler)
95
+
96
+ #
97
+
98
+ ce = injector[LocalCommandExecutor]
56
99
 
57
100
  while True:
58
- i = chan.recv_obj(Command)
59
- if i is None:
101
+ req = chan.recv_obj(_RemoteExecutionRequest)
102
+ if req is None:
60
103
  break
61
104
 
105
+ with log_lock:
106
+ send_logs = True
107
+
62
108
  r = ce.try_execute(
63
- i,
109
+ req.c,
64
110
  log=log,
65
111
  omit_exc_object=True,
66
112
  )
67
113
 
68
- chan.send_obj(r)
114
+ with log_lock:
115
+ send_logs = False
116
+
117
+ chan.send_obj(_RemoteExecutionResponse(r=CommandOutputOrExceptionData(
118
+ output=r.output,
119
+ exception=r.exception,
120
+ )))
69
121
 
70
122
 
71
123
  ##
@@ -83,12 +135,17 @@ class RemoteCommandExecutor(CommandExecutor):
83
135
  self._chan = chan
84
136
 
85
137
  def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
86
- self._chan.send_obj(cmd, Command)
138
+ self._chan.send_obj(_RemoteExecutionRequest(cmd))
139
+
140
+ while True:
141
+ if (r := self._chan.recv_obj(_RemoteExecutionResponse)) is None:
142
+ raise EOFError
87
143
 
88
- if (r := self._chan.recv_obj(CommandOutputOrExceptionData)) is None:
89
- raise EOFError
144
+ if r.l is not None:
145
+ log.info(r.l.s)
90
146
 
91
- return r
147
+ if r.r is not None:
148
+ return r.r
92
149
 
93
150
  # @ta.override
94
151
  def execute(self, cmd: Command) -> Command.Output: