ominfra 0.0.0.dev152__py3-none-any.whl → 0.0.0.dev154__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- ominfra/manage/bootstrap.py +3 -0
- ominfra/manage/bootstrap_.py +1 -0
- ominfra/manage/commands/interp.py +0 -3
- ominfra/manage/commands/subprocess.py +0 -3
- ominfra/manage/deploy/{command.py → commands.py} +0 -3
- ominfra/manage/deploy/inject.py +2 -2
- ominfra/manage/inject.py +8 -1
- ominfra/manage/main.py +2 -0
- ominfra/manage/remote/_main.py +2 -2
- ominfra/manage/remote/config.py +2 -0
- ominfra/manage/remote/connection.py +48 -0
- ominfra/manage/remote/execution.py +90 -12
- ominfra/manage/remote/inject.py +19 -4
- ominfra/manage/system/__init__.py +0 -0
- ominfra/manage/system/commands.py +24 -0
- ominfra/manage/system/config.py +8 -0
- ominfra/manage/system/inject.py +54 -0
- ominfra/manage/system/packages.py +106 -0
- ominfra/manage/system/types.py +5 -0
- ominfra/scripts/manage.py +400 -38
- {ominfra-0.0.0.dev152.dist-info → ominfra-0.0.0.dev154.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev152.dist-info → ominfra-0.0.0.dev154.dist-info}/RECORD +26 -20
- {ominfra-0.0.0.dev152.dist-info → ominfra-0.0.0.dev154.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev152.dist-info → ominfra-0.0.0.dev154.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev152.dist-info → ominfra-0.0.0.dev154.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev152.dist-info → ominfra-0.0.0.dev154.dist-info}/top_level.txt +0 -0
ominfra/manage/bootstrap.py
CHANGED
@@ -2,6 +2,7 @@ import dataclasses as dc
|
|
2
2
|
|
3
3
|
from .config import MainConfig
|
4
4
|
from .remote.config import RemoteConfig
|
5
|
+
from .system.config import SystemConfig
|
5
6
|
|
6
7
|
|
7
8
|
@dc.dataclass(frozen=True)
|
@@ -9,3 +10,5 @@ class MainBootstrap:
|
|
9
10
|
main_config: MainConfig = MainConfig()
|
10
11
|
|
11
12
|
remote_config: RemoteConfig = RemoteConfig()
|
13
|
+
|
14
|
+
system_config: SystemConfig = SystemConfig()
|
ominfra/manage/bootstrap_.py
CHANGED
@@ -25,9 +25,6 @@ class InterpCommand(Command['InterpCommand.Output']):
|
|
25
25
|
opts: InterpOpts
|
26
26
|
|
27
27
|
|
28
|
-
##
|
29
|
-
|
30
|
-
|
31
28
|
class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
|
32
29
|
async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
|
33
30
|
i = InterpSpecifier.parse(check.not_none(cmd.spec))
|
@@ -48,9 +48,6 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
|
|
48
48
|
stderr: ta.Optional[bytes] = None
|
49
49
|
|
50
50
|
|
51
|
-
##
|
52
|
-
|
53
|
-
|
54
51
|
class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCommand.Output]):
|
55
52
|
async def execute(self, cmd: SubprocessCommand) -> SubprocessCommand.Output:
|
56
53
|
proc: asyncio.subprocess.Process
|
@@ -17,9 +17,6 @@ class DeployCommand(Command['DeployCommand.Output']):
|
|
17
17
|
pass
|
18
18
|
|
19
19
|
|
20
|
-
##
|
21
|
-
|
22
|
-
|
23
20
|
class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
|
24
21
|
async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
|
25
22
|
log.info('Deploying!')
|
ominfra/manage/deploy/inject.py
CHANGED
@@ -6,8 +6,8 @@ from omlish.lite.inject import InjectorBindings
|
|
6
6
|
from omlish.lite.inject import inj
|
7
7
|
|
8
8
|
from ..commands.inject import bind_command
|
9
|
-
from .
|
10
|
-
from .
|
9
|
+
from .commands import DeployCommand
|
10
|
+
from .commands import DeployCommandExecutor
|
11
11
|
|
12
12
|
|
13
13
|
def bind_deploy(
|
ominfra/manage/inject.py
CHANGED
@@ -13,6 +13,8 @@ from .marshal import ObjMarshalerInstaller
|
|
13
13
|
from .marshal import ObjMarshalerInstallers
|
14
14
|
from .remote.config import RemoteConfig
|
15
15
|
from .remote.inject import bind_remote
|
16
|
+
from .system.config import SystemConfig
|
17
|
+
from .system.inject import bind_system
|
16
18
|
|
17
19
|
|
18
20
|
##
|
@@ -22,6 +24,7 @@ def bind_main(
|
|
22
24
|
*,
|
23
25
|
main_config: MainConfig,
|
24
26
|
remote_config: RemoteConfig,
|
27
|
+
system_config: SystemConfig,
|
25
28
|
) -> InjectorBindings:
|
26
29
|
lst: ta.List[InjectorBindingOrBindings] = [
|
27
30
|
inj.bind(main_config),
|
@@ -30,11 +33,15 @@ def bind_main(
|
|
30
33
|
main_config=main_config,
|
31
34
|
),
|
32
35
|
|
36
|
+
bind_deploy(),
|
37
|
+
|
33
38
|
bind_remote(
|
34
39
|
remote_config=remote_config,
|
35
40
|
),
|
36
41
|
|
37
|
-
|
42
|
+
bind_system(
|
43
|
+
system_config=system_config,
|
44
|
+
),
|
38
45
|
]
|
39
46
|
|
40
47
|
#
|
ominfra/manage/main.py
CHANGED
ominfra/manage/remote/_main.py
CHANGED
@@ -8,8 +8,8 @@ import threading
|
|
8
8
|
import time
|
9
9
|
import typing as ta
|
10
10
|
|
11
|
-
from omlish.
|
12
|
-
from omlish.
|
11
|
+
from omlish.asyncs.asyncio.streams import asyncio_open_stream_reader
|
12
|
+
from omlish.asyncs.asyncio.streams import asyncio_open_stream_writer
|
13
13
|
from omlish.lite.cached import cached_nullary
|
14
14
|
from omlish.lite.check import check
|
15
15
|
from omlish.lite.inject import Injector
|
ominfra/manage/remote/config.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import abc
|
3
|
+
import asyncio
|
3
4
|
import contextlib
|
4
5
|
import typing as ta
|
5
6
|
|
7
|
+
from omlish.asyncs.asyncio.channels import asyncio_create_bytes_channel
|
6
8
|
from omlish.lite.cached import cached_nullary
|
7
9
|
from omlish.lite.marshal import ObjMarshalerManager
|
8
10
|
|
@@ -10,9 +12,11 @@ from ...pyremote import PyremoteBootstrapDriver
|
|
10
12
|
from ...pyremote import PyremoteBootstrapOptions
|
11
13
|
from ...pyremote import pyremote_build_bootstrap_cmd
|
12
14
|
from ..bootstrap import MainBootstrap
|
15
|
+
from ..commands.execution import LocalCommandExecutor
|
13
16
|
from ._main import _remote_execution_main # noqa
|
14
17
|
from .channel import RemoteChannelImpl
|
15
18
|
from .execution import RemoteCommandExecutor
|
19
|
+
from .execution import _RemoteCommandHandler
|
16
20
|
from .payload import RemoteExecutionPayloadFile
|
17
21
|
from .payload import get_remote_payload_src
|
18
22
|
from .spawning import RemoteSpawning
|
@@ -104,3 +108,47 @@ class PyremoteRemoteExecutionConnector(RemoteExecutionConnector):
|
|
104
108
|
await rce.start()
|
105
109
|
|
106
110
|
yield rce
|
111
|
+
|
112
|
+
|
113
|
+
##
|
114
|
+
|
115
|
+
|
116
|
+
class InProcessRemoteExecutionConnector(RemoteExecutionConnector):
|
117
|
+
def __init__(
|
118
|
+
self,
|
119
|
+
*,
|
120
|
+
msh: ObjMarshalerManager,
|
121
|
+
local_executor: LocalCommandExecutor,
|
122
|
+
) -> None:
|
123
|
+
super().__init__()
|
124
|
+
|
125
|
+
self._msh = msh
|
126
|
+
self._local_executor = local_executor
|
127
|
+
|
128
|
+
@contextlib.asynccontextmanager
|
129
|
+
async def connect(
|
130
|
+
self,
|
131
|
+
tgt: RemoteSpawning.Target,
|
132
|
+
bs: MainBootstrap,
|
133
|
+
) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
|
134
|
+
r0, w0 = asyncio_create_bytes_channel()
|
135
|
+
r1, w1 = asyncio_create_bytes_channel()
|
136
|
+
|
137
|
+
remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
|
138
|
+
local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
|
139
|
+
|
140
|
+
rch = _RemoteCommandHandler(
|
141
|
+
remote_chan,
|
142
|
+
self._local_executor,
|
143
|
+
)
|
144
|
+
rch_task = asyncio.create_task(rch.run()) # noqa
|
145
|
+
try:
|
146
|
+
rce: RemoteCommandExecutor
|
147
|
+
async with contextlib.aclosing(RemoteCommandExecutor(local_chan)) as rce:
|
148
|
+
await rce.start()
|
149
|
+
|
150
|
+
yield rce
|
151
|
+
|
152
|
+
finally:
|
153
|
+
rch.stop()
|
154
|
+
await rch_task
|
@@ -1,9 +1,14 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
|
+
"""
|
3
|
+
TODO:
|
4
|
+
- sequence all messages
|
5
|
+
"""
|
2
6
|
import abc
|
3
7
|
import asyncio
|
4
8
|
import dataclasses as dc
|
5
9
|
import itertools
|
6
10
|
import logging
|
11
|
+
import time
|
7
12
|
import typing as ta
|
8
13
|
|
9
14
|
from omlish.lite.check import check
|
@@ -96,38 +101,80 @@ class _RemoteLogHandler(logging.Handler):
|
|
96
101
|
|
97
102
|
|
98
103
|
class _RemoteCommandHandler:
|
104
|
+
DEFAULT_PING_INTERVAL_S: float = 3.
|
105
|
+
|
99
106
|
def __init__(
|
100
107
|
self,
|
101
108
|
chan: RemoteChannel,
|
102
109
|
executor: CommandExecutor,
|
103
110
|
*,
|
104
111
|
stop: ta.Optional[asyncio.Event] = None,
|
112
|
+
ping_interval_s: float = DEFAULT_PING_INTERVAL_S,
|
105
113
|
) -> None:
|
106
114
|
super().__init__()
|
107
115
|
|
108
116
|
self._chan = chan
|
109
117
|
self._executor = executor
|
110
118
|
self._stop = stop if stop is not None else asyncio.Event()
|
119
|
+
self._ping_interval_s = ping_interval_s
|
111
120
|
|
112
121
|
self._cmds_by_seq: ta.Dict[int, _RemoteCommandHandler._Command] = {}
|
113
122
|
|
123
|
+
self._last_ping_send: float = 0.
|
124
|
+
self._ping_in_flight: bool = False
|
125
|
+
self._last_ping_recv: ta.Optional[float] = None
|
126
|
+
|
127
|
+
def stop(self) -> None:
|
128
|
+
self._stop.set()
|
129
|
+
|
114
130
|
@dc.dataclass(frozen=True)
|
115
131
|
class _Command:
|
116
132
|
req: _RemoteProtocol.CommandRequest
|
117
133
|
fut: asyncio.Future
|
118
134
|
|
119
135
|
async def run(self) -> None:
|
136
|
+
log.debug('_RemoteCommandHandler loop start: %r', self)
|
137
|
+
|
120
138
|
stop_task = asyncio.create_task(self._stop.wait())
|
121
139
|
recv_task: ta.Optional[asyncio.Task] = None
|
122
140
|
|
123
141
|
while not self._stop.is_set():
|
124
142
|
if recv_task is None:
|
125
|
-
recv_task = asyncio.create_task(_RemoteProtocol.
|
143
|
+
recv_task = asyncio.create_task(_RemoteProtocol.Message.recv(self._chan))
|
126
144
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
145
|
+
if not self._ping_in_flight:
|
146
|
+
if not self._last_ping_recv:
|
147
|
+
ping_wait_time = 0.
|
148
|
+
else:
|
149
|
+
ping_wait_time = self._ping_interval_s - (time.time() - self._last_ping_recv)
|
150
|
+
else:
|
151
|
+
ping_wait_time = float('inf')
|
152
|
+
wait_time = min(self._ping_interval_s, ping_wait_time)
|
153
|
+
log.debug('_RemoteCommandHandler loop wait: %f', wait_time)
|
154
|
+
|
155
|
+
done, pending = await asyncio.wait(
|
156
|
+
[
|
157
|
+
stop_task,
|
158
|
+
recv_task,
|
159
|
+
],
|
160
|
+
return_when=asyncio.FIRST_COMPLETED,
|
161
|
+
timeout=wait_time,
|
162
|
+
)
|
163
|
+
|
164
|
+
#
|
165
|
+
|
166
|
+
if (
|
167
|
+
(time.time() - self._last_ping_send >= self._ping_interval_s) and
|
168
|
+
not self._ping_in_flight
|
169
|
+
):
|
170
|
+
now = time.time()
|
171
|
+
self._last_ping_send = now
|
172
|
+
self._ping_in_flight = True
|
173
|
+
await _RemoteProtocol.PingRequest(
|
174
|
+
time=now,
|
175
|
+
).send(self._chan)
|
176
|
+
|
177
|
+
#
|
131
178
|
|
132
179
|
if recv_task in done:
|
133
180
|
msg: ta.Optional[_RemoteProtocol.Message] = check.isinstance(
|
@@ -141,6 +188,20 @@ class _RemoteCommandHandler:
|
|
141
188
|
|
142
189
|
await self._handle_message(msg)
|
143
190
|
|
191
|
+
log.debug('_RemoteCommandHandler loop stopping: %r', self)
|
192
|
+
|
193
|
+
for task in [
|
194
|
+
stop_task,
|
195
|
+
recv_task,
|
196
|
+
]:
|
197
|
+
if task is not None and not task.done():
|
198
|
+
task.cancel()
|
199
|
+
|
200
|
+
for cmd in self._cmds_by_seq.values():
|
201
|
+
cmd.fut.cancel()
|
202
|
+
|
203
|
+
log.debug('_RemoteCommandHandler loop exited: %r', self)
|
204
|
+
|
144
205
|
async def _handle_message(self, msg: _RemoteProtocol.Message) -> None:
|
145
206
|
if isinstance(msg, _RemoteProtocol.PingRequest):
|
146
207
|
log.debug('Ping: %r', msg)
|
@@ -148,6 +209,12 @@ class _RemoteCommandHandler:
|
|
148
209
|
time=msg.time,
|
149
210
|
).send(self._chan)
|
150
211
|
|
212
|
+
elif isinstance(msg, _RemoteProtocol.PingResponse):
|
213
|
+
latency_s = time.time() - msg.time
|
214
|
+
log.debug('Pong: %0.2f ms %r', latency_s * 1000., msg)
|
215
|
+
self._last_ping_recv = time.time()
|
216
|
+
self._ping_in_flight = False
|
217
|
+
|
151
218
|
elif isinstance(msg, _RemoteProtocol.CommandRequest):
|
152
219
|
fut = asyncio.create_task(self._handle_command_request(msg))
|
153
220
|
self._cmds_by_seq[msg.seq] = _RemoteCommandHandler._Command(
|
@@ -229,16 +296,23 @@ class RemoteCommandExecutor(CommandExecutor):
|
|
229
296
|
if recv_task is None:
|
230
297
|
recv_task = asyncio.create_task(_RemoteProtocol.Message.recv(self._chan))
|
231
298
|
|
232
|
-
done, pending = await asyncio.wait(
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
299
|
+
done, pending = await asyncio.wait(
|
300
|
+
[
|
301
|
+
stop_task,
|
302
|
+
queue_task,
|
303
|
+
recv_task,
|
304
|
+
],
|
305
|
+
return_when=asyncio.FIRST_COMPLETED,
|
306
|
+
)
|
307
|
+
|
308
|
+
#
|
237
309
|
|
238
310
|
if queue_task in done:
|
239
311
|
req = check.isinstance(queue_task.result(), RemoteCommandExecutor._Request)
|
240
312
|
queue_task = None
|
241
|
-
await self.
|
313
|
+
await self._handle_queued_request(req)
|
314
|
+
|
315
|
+
#
|
242
316
|
|
243
317
|
if recv_task in done:
|
244
318
|
msg: ta.Optional[_RemoteProtocol.Message] = check.isinstance(
|
@@ -268,7 +342,7 @@ class RemoteCommandExecutor(CommandExecutor):
|
|
268
342
|
|
269
343
|
log.debug('RemoteCommandExecutor loop exited: %r', self)
|
270
344
|
|
271
|
-
async def
|
345
|
+
async def _handle_queued_request(self, req: _Request) -> None:
|
272
346
|
self._reqs_by_seq[req.seq] = req
|
273
347
|
await _RemoteProtocol.CommandRequest(
|
274
348
|
seq=req.seq,
|
@@ -282,6 +356,10 @@ class RemoteCommandExecutor(CommandExecutor):
|
|
282
356
|
time=msg.time,
|
283
357
|
).send(self._chan)
|
284
358
|
|
359
|
+
elif isinstance(msg, _RemoteProtocol.PingResponse):
|
360
|
+
latency_s = time.time() - msg.time
|
361
|
+
log.debug('Pong: %0.2f ms %r', latency_s * 1000., msg)
|
362
|
+
|
285
363
|
elif isinstance(msg, _RemoteProtocol.LogResponse):
|
286
364
|
log.info(msg.s)
|
287
365
|
|
ominfra/manage/remote/inject.py
CHANGED
@@ -6,6 +6,7 @@ from omlish.lite.inject import InjectorBindings
|
|
6
6
|
from omlish.lite.inject import inj
|
7
7
|
|
8
8
|
from .config import RemoteConfig
|
9
|
+
from .connection import InProcessRemoteExecutionConnector
|
9
10
|
from .connection import PyremoteRemoteExecutionConnector
|
10
11
|
from .connection import RemoteExecutionConnector
|
11
12
|
from .payload import RemoteExecutionPayloadFile
|
@@ -22,12 +23,26 @@ def bind_remote(
|
|
22
23
|
|
23
24
|
inj.bind(SubprocessRemoteSpawning, singleton=True),
|
24
25
|
inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
|
25
|
-
|
26
|
-
inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
|
27
|
-
inj.bind(RemoteExecutionConnector, to_key=PyremoteRemoteExecutionConnector),
|
28
26
|
]
|
29
27
|
|
28
|
+
#
|
29
|
+
|
30
|
+
if remote_config.use_in_process_remote_executor:
|
31
|
+
lst.extend([
|
32
|
+
inj.bind(InProcessRemoteExecutionConnector, singleton=True),
|
33
|
+
inj.bind(RemoteExecutionConnector, to_key=InProcessRemoteExecutionConnector),
|
34
|
+
])
|
35
|
+
else:
|
36
|
+
lst.extend([
|
37
|
+
inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
|
38
|
+
inj.bind(RemoteExecutionConnector, to_key=PyremoteRemoteExecutionConnector),
|
39
|
+
])
|
40
|
+
|
41
|
+
#
|
42
|
+
|
30
43
|
if (pf := remote_config.payload_file) is not None:
|
31
|
-
lst.append(inj.bind(pf,
|
44
|
+
lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
|
45
|
+
|
46
|
+
#
|
32
47
|
|
33
48
|
return inj.as_bindings(*lst)
|
File without changes
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import dataclasses as dc
|
3
|
+
|
4
|
+
from omlish.lite.logs import log
|
5
|
+
|
6
|
+
from ..commands.base import Command
|
7
|
+
from ..commands.base import CommandExecutor
|
8
|
+
|
9
|
+
|
10
|
+
##
|
11
|
+
|
12
|
+
|
13
|
+
@dc.dataclass(frozen=True)
|
14
|
+
class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
|
15
|
+
@dc.dataclass(frozen=True)
|
16
|
+
class Output(Command.Output):
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
|
21
|
+
async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
|
22
|
+
log.info('Checking system package!')
|
23
|
+
|
24
|
+
return CheckSystemPackageCommand.Output()
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import sys
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
from omlish.lite.inject import InjectorBindingOrBindings
|
6
|
+
from omlish.lite.inject import InjectorBindings
|
7
|
+
from omlish.lite.inject import inj
|
8
|
+
|
9
|
+
from ..commands.inject import bind_command
|
10
|
+
from .commands import CheckSystemPackageCommand
|
11
|
+
from .commands import CheckSystemPackageCommandExecutor
|
12
|
+
from .config import SystemConfig
|
13
|
+
from .packages import AptSystemPackageManager
|
14
|
+
from .packages import BrewSystemPackageManager
|
15
|
+
from .packages import SystemPackageManager
|
16
|
+
from .types import SystemPlatform
|
17
|
+
|
18
|
+
|
19
|
+
def bind_system(
|
20
|
+
*,
|
21
|
+
system_config: SystemConfig,
|
22
|
+
) -> InjectorBindings:
|
23
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
24
|
+
inj.bind(system_config),
|
25
|
+
]
|
26
|
+
|
27
|
+
#
|
28
|
+
|
29
|
+
platform = system_config.platform or sys.platform
|
30
|
+
lst.append(inj.bind(platform, key=SystemPlatform))
|
31
|
+
|
32
|
+
#
|
33
|
+
|
34
|
+
if platform == 'linux':
|
35
|
+
lst.extend([
|
36
|
+
inj.bind(AptSystemPackageManager, singleton=True),
|
37
|
+
inj.bind(SystemPackageManager, to_key=AptSystemPackageManager),
|
38
|
+
])
|
39
|
+
|
40
|
+
elif platform == 'darwin':
|
41
|
+
lst.extend([
|
42
|
+
inj.bind(BrewSystemPackageManager, singleton=True),
|
43
|
+
inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
|
44
|
+
])
|
45
|
+
|
46
|
+
#
|
47
|
+
|
48
|
+
lst.extend([
|
49
|
+
bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
|
50
|
+
])
|
51
|
+
|
52
|
+
#
|
53
|
+
|
54
|
+
return inj.as_bindings(*lst)
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
"""
|
3
|
+
TODO:
|
4
|
+
- yum/rpm
|
5
|
+
"""
|
6
|
+
import abc
|
7
|
+
import dataclasses as dc
|
8
|
+
import json
|
9
|
+
import os
|
10
|
+
import typing as ta
|
11
|
+
|
12
|
+
from omlish.lite.asyncio.subprocesses import asyncio_subprocess_check_call
|
13
|
+
from omlish.lite.asyncio.subprocesses import asyncio_subprocess_check_output
|
14
|
+
from omlish.lite.asyncio.subprocesses import asyncio_subprocess_run
|
15
|
+
from omlish.lite.check import check
|
16
|
+
|
17
|
+
|
18
|
+
SystemPackageOrStr = ta.Union['SystemPackage', str]
|
19
|
+
|
20
|
+
|
21
|
+
@dc.dataclass(frozen=True)
|
22
|
+
class SystemPackage:
|
23
|
+
name: str
|
24
|
+
version: ta.Optional[str] = None
|
25
|
+
|
26
|
+
|
27
|
+
class SystemPackageManager(abc.ABC):
|
28
|
+
@abc.abstractmethod
|
29
|
+
def update(self) -> ta.Awaitable[None]:
|
30
|
+
raise NotImplementedError
|
31
|
+
|
32
|
+
@abc.abstractmethod
|
33
|
+
def upgrade(self) -> ta.Awaitable[None]:
|
34
|
+
raise NotImplementedError
|
35
|
+
|
36
|
+
@abc.abstractmethod
|
37
|
+
def install(self, *packages: SystemPackageOrStr) -> ta.Awaitable[None]:
|
38
|
+
raise NotImplementedError
|
39
|
+
|
40
|
+
@abc.abstractmethod
|
41
|
+
def query(self, *packages: SystemPackageOrStr) -> ta.Awaitable[ta.Mapping[str, SystemPackage]]:
|
42
|
+
raise NotImplementedError
|
43
|
+
|
44
|
+
|
45
|
+
class BrewSystemPackageManager(SystemPackageManager):
|
46
|
+
async def update(self) -> None:
|
47
|
+
await asyncio_subprocess_check_call('brew', 'update')
|
48
|
+
|
49
|
+
async def upgrade(self) -> None:
|
50
|
+
await asyncio_subprocess_check_call('brew', 'upgrade')
|
51
|
+
|
52
|
+
async def install(self, *packages: SystemPackageOrStr) -> None:
|
53
|
+
es: ta.List[str] = []
|
54
|
+
for p in packages:
|
55
|
+
if isinstance(p, SystemPackage):
|
56
|
+
es.append(p.name + (f'@{p.version}' if p.version is not None else ''))
|
57
|
+
else:
|
58
|
+
es.append(p)
|
59
|
+
await asyncio_subprocess_check_call('brew', 'install', *es)
|
60
|
+
|
61
|
+
async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
|
62
|
+
pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
|
63
|
+
o = await asyncio_subprocess_check_output('brew', 'info', '--json', *pns)
|
64
|
+
j = json.loads(o.decode())
|
65
|
+
d: ta.Dict[str, SystemPackage] = {}
|
66
|
+
for e in j:
|
67
|
+
if not e['installed']:
|
68
|
+
continue
|
69
|
+
d[e['name']] = SystemPackage(
|
70
|
+
name=e['name'],
|
71
|
+
version=e['installed'][0]['version'],
|
72
|
+
)
|
73
|
+
return d
|
74
|
+
|
75
|
+
|
76
|
+
class AptSystemPackageManager(SystemPackageManager):
|
77
|
+
_APT_ENV: ta.ClassVar[ta.Mapping[str, str]] = {
|
78
|
+
'DEBIAN_FRONTEND': 'noninteractive',
|
79
|
+
}
|
80
|
+
|
81
|
+
async def update(self) -> None:
|
82
|
+
await asyncio_subprocess_check_call('apt', 'update', env={**os.environ, **self._APT_ENV})
|
83
|
+
|
84
|
+
async def upgrade(self) -> None:
|
85
|
+
await asyncio_subprocess_check_call('apt', 'upgrade', '-y', env={**os.environ, **self._APT_ENV})
|
86
|
+
|
87
|
+
async def install(self, *packages: SystemPackageOrStr) -> None:
|
88
|
+
pns = [p.name if isinstance(p, SystemPackage) else p for p in packages] # FIXME: versions
|
89
|
+
await asyncio_subprocess_check_call('apt', 'install', '-y', *pns, env={**os.environ, **self._APT_ENV})
|
90
|
+
|
91
|
+
async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
|
92
|
+
pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
|
93
|
+
cmd = ['dpkg-query', '-W', '-f=${Package}=${Version}\n', *pns]
|
94
|
+
stdout, stderr = await asyncio_subprocess_run(
|
95
|
+
*cmd,
|
96
|
+
capture_output=True,
|
97
|
+
check=False,
|
98
|
+
)
|
99
|
+
d: ta.Dict[str, SystemPackage] = {}
|
100
|
+
for l in check.not_none(stdout).decode('utf-8').strip().splitlines():
|
101
|
+
n, v = l.split('=', 1)
|
102
|
+
d[n] = SystemPackage(
|
103
|
+
name=n,
|
104
|
+
version=v,
|
105
|
+
)
|
106
|
+
return d
|