ominfra 0.0.0.dev145__py3-none-any.whl → 0.0.0.dev147__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/manage/commands/inject.py +5 -0
- ominfra/manage/commands/interp.py +39 -0
- ominfra/manage/commands/subprocess.py +2 -2
- ominfra/manage/deploy/command.py +4 -0
- ominfra/manage/deploy/paths.py +147 -0
- ominfra/manage/main.py +14 -8
- ominfra/manage/remote/channel.py +13 -2
- ominfra/manage/remote/execution.py +64 -8
- ominfra/scripts/manage.py +2195 -236
- {ominfra-0.0.0.dev145.dist-info → ominfra-0.0.0.dev147.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev145.dist-info → ominfra-0.0.0.dev147.dist-info}/RECORD +15 -14
- {ominfra-0.0.0.dev145.dist-info → ominfra-0.0.0.dev147.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev145.dist-info → ominfra-0.0.0.dev147.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev145.dist-info → ominfra-0.0.0.dev147.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev145.dist-info → ominfra-0.0.0.dev147.dist-info}/top_level.txt +0 -0
@@ -20,6 +20,8 @@ from .base import CommandRegistrations
|
|
20
20
|
from .base import build_command_name_map
|
21
21
|
from .execution import CommandExecutorMap
|
22
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
|
@@ -111,8 +113,11 @@ def bind_commands(
|
|
111
113
|
|
112
114
|
#
|
113
115
|
|
116
|
+
command_cls: ta.Any
|
117
|
+
executor_cls: ta.Any
|
114
118
|
for command_cls, executor_cls in [
|
115
119
|
(SubprocessCommand, SubprocessCommandExecutor),
|
120
|
+
(InterpCommand, InterpCommandExecutor),
|
116
121
|
]:
|
117
122
|
lst.append(bind_command(command_cls, executor_cls))
|
118
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
|
-
|
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):
|
ominfra/manage/deploy/command.py
CHANGED
@@ -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()
|
ominfra/manage/deploy/paths.py
CHANGED
@@ -31,3 +31,150 @@ for dn in [
|
|
31
31
|
'venv',
|
32
32
|
]:
|
33
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
|
@@ -18,9 +19,7 @@ from .bootstrap_ import main_bootstrap
|
|
18
19
|
from .commands.base import Command
|
19
20
|
from .commands.base import CommandExecutor
|
20
21
|
from .commands.execution import LocalCommandExecutor
|
21
|
-
from .commands.subprocess import SubprocessCommand
|
22
22
|
from .config import MainConfig
|
23
|
-
from .deploy.command import DeployCommand
|
24
23
|
from .remote.config import RemoteConfig
|
25
24
|
from .remote.execution import RemoteExecution
|
26
25
|
from .remote.spawning import RemoteSpawning
|
@@ -78,12 +77,15 @@ def _main() -> None:
|
|
78
77
|
|
79
78
|
#
|
80
79
|
|
80
|
+
msh = injector[ObjMarshalerManager]
|
81
|
+
|
81
82
|
cmds: ta.List[Command] = []
|
83
|
+
cmd: Command
|
82
84
|
for c in args.command:
|
83
|
-
if c
|
84
|
-
|
85
|
-
|
86
|
-
|
85
|
+
if not c.startswith('{'):
|
86
|
+
c = json.dumps({c: {}})
|
87
|
+
cmd = msh.unmarshal_obj(json.loads(c), Command)
|
88
|
+
cmds.append(cmd)
|
87
89
|
|
88
90
|
#
|
89
91
|
|
@@ -103,9 +105,13 @@ def _main() -> None:
|
|
103
105
|
ce = es.enter_context(injector[RemoteExecution].connect(tgt, bs)) # noqa
|
104
106
|
|
105
107
|
for cmd in cmds:
|
106
|
-
r = ce.try_execute(
|
108
|
+
r = ce.try_execute(
|
109
|
+
cmd,
|
110
|
+
log=log,
|
111
|
+
omit_exc_object=True,
|
112
|
+
)
|
107
113
|
|
108
|
-
print(
|
114
|
+
print(msh.marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
|
109
115
|
|
110
116
|
|
111
117
|
if __name__ == '__main__':
|
ominfra/manage/remote/channel.py
CHANGED
@@ -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
|
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
|
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
|
@@ -36,6 +37,32 @@ else:
|
|
36
37
|
##
|
37
38
|
|
38
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
|
+
|
39
66
|
def _remote_execution_main() -> None:
|
40
67
|
rt = pyremote_bootstrap_finalize() # noqa
|
41
68
|
|
@@ -53,20 +80,44 @@ def _remote_execution_main() -> None:
|
|
53
80
|
|
54
81
|
chan.set_marshaler(injector[ObjMarshalerManager])
|
55
82
|
|
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
|
+
|
56
98
|
ce = injector[LocalCommandExecutor]
|
57
99
|
|
58
100
|
while True:
|
59
|
-
|
60
|
-
if
|
101
|
+
req = chan.recv_obj(_RemoteExecutionRequest)
|
102
|
+
if req is None:
|
61
103
|
break
|
62
104
|
|
105
|
+
with log_lock:
|
106
|
+
send_logs = True
|
107
|
+
|
63
108
|
r = ce.try_execute(
|
64
|
-
|
109
|
+
req.c,
|
65
110
|
log=log,
|
66
111
|
omit_exc_object=True,
|
67
112
|
)
|
68
113
|
|
69
|
-
|
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
|
+
)))
|
70
121
|
|
71
122
|
|
72
123
|
##
|
@@ -84,12 +135,17 @@ class RemoteCommandExecutor(CommandExecutor):
|
|
84
135
|
self._chan = chan
|
85
136
|
|
86
137
|
def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
|
87
|
-
self._chan.send_obj(cmd
|
138
|
+
self._chan.send_obj(_RemoteExecutionRequest(cmd))
|
139
|
+
|
140
|
+
while True:
|
141
|
+
if (r := self._chan.recv_obj(_RemoteExecutionResponse)) is None:
|
142
|
+
raise EOFError
|
88
143
|
|
89
|
-
|
90
|
-
|
144
|
+
if r.l is not None:
|
145
|
+
log.info(r.l.s)
|
91
146
|
|
92
|
-
|
147
|
+
if r.r is not None:
|
148
|
+
return r.r
|
93
149
|
|
94
150
|
# @ta.override
|
95
151
|
def execute(self, cmd: Command) -> Command.Output:
|