ominfra 0.0.0.dev190__py3-none-any.whl → 0.0.0.dev192__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/deploy/apps.py +82 -3
- ominfra/manage/deploy/conf/manager.py +70 -7
- ominfra/manage/deploy/deploy.py +127 -32
- ominfra/manage/deploy/git.py +3 -1
- ominfra/manage/deploy/specs.py +11 -0
- ominfra/manage/deploy/systemd.py +49 -27
- ominfra/manage/deploy/tags.py +1 -1
- ominfra/manage/deploy/venvs.py +8 -4
- ominfra/scripts/manage.py +337 -74
- ominfra/scripts/supervisor.py +18 -9
- ominfra/supervisor/configs.py +6 -0
- ominfra/supervisor/http.py +1 -1
- ominfra/supervisor/inject.py +11 -8
- ominfra/systemd.py +49 -0
- {ominfra-0.0.0.dev190.dist-info → ominfra-0.0.0.dev192.dist-info}/METADATA +4 -4
- {ominfra-0.0.0.dev190.dist-info → ominfra-0.0.0.dev192.dist-info}/RECORD +20 -19
- {ominfra-0.0.0.dev190.dist-info → ominfra-0.0.0.dev192.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev190.dist-info → ominfra-0.0.0.dev192.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev190.dist-info → ominfra-0.0.0.dev192.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev190.dist-info → ominfra-0.0.0.dev192.dist-info}/top_level.txt +0 -0
ominfra/manage/deploy/apps.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import dataclasses as dc
|
3
|
+
import json
|
3
4
|
import os.path
|
4
5
|
import typing as ta
|
5
6
|
|
@@ -13,6 +14,7 @@ from .git import DeployGitManager
|
|
13
14
|
from .paths.owners import DeployPathOwner
|
14
15
|
from .paths.paths import DeployPath
|
15
16
|
from .specs import DeployAppSpec
|
17
|
+
from .tags import DeployAppRev
|
16
18
|
from .tags import DeployTagMap
|
17
19
|
from .types import DeployHome
|
18
20
|
from .venvs import DeployVenvManager
|
@@ -57,9 +59,21 @@ class DeployAppManager(DeployPathOwner):
|
|
57
59
|
|
58
60
|
#
|
59
61
|
|
62
|
+
def _make_tags(self, spec: DeployAppSpec) -> DeployTagMap:
|
63
|
+
return DeployTagMap(
|
64
|
+
spec.app,
|
65
|
+
spec.key(),
|
66
|
+
DeployAppRev(spec.git.rev),
|
67
|
+
)
|
68
|
+
|
69
|
+
#
|
70
|
+
|
60
71
|
@dc.dataclass(frozen=True)
|
61
72
|
class PreparedApp:
|
62
|
-
|
73
|
+
spec: DeployAppSpec
|
74
|
+
tags: DeployTagMap
|
75
|
+
|
76
|
+
dir: str
|
63
77
|
|
64
78
|
git_dir: ta.Optional[str] = None
|
65
79
|
venv_dir: ta.Optional[str] = None
|
@@ -70,21 +84,30 @@ class DeployAppManager(DeployPathOwner):
|
|
70
84
|
spec: DeployAppSpec,
|
71
85
|
home: DeployHome,
|
72
86
|
tags: DeployTagMap,
|
87
|
+
*,
|
88
|
+
conf_string_ns: ta.Optional[ta.Mapping[str, ta.Any]] = None,
|
73
89
|
) -> PreparedApp:
|
74
90
|
spec_json = json_dumps_pretty(self._msh.marshal_obj(spec))
|
75
91
|
|
76
92
|
#
|
77
93
|
|
94
|
+
app_tags = tags.add(*self._make_tags(spec))
|
95
|
+
|
96
|
+
#
|
97
|
+
|
78
98
|
check.non_empty_str(home)
|
79
99
|
|
80
|
-
app_dir = os.path.join(home, self.APP_DIR.render(
|
100
|
+
app_dir = os.path.join(home, self.APP_DIR.render(app_tags))
|
81
101
|
|
82
102
|
os.makedirs(app_dir, exist_ok=True)
|
83
103
|
|
84
104
|
#
|
85
105
|
|
86
106
|
rkw: ta.Dict[str, ta.Any] = dict(
|
87
|
-
|
107
|
+
spec=spec,
|
108
|
+
tags=app_tags,
|
109
|
+
|
110
|
+
dir=app_dir,
|
88
111
|
)
|
89
112
|
|
90
113
|
#
|
@@ -119,11 +142,67 @@ class DeployAppManager(DeployPathOwner):
|
|
119
142
|
if spec.conf is not None:
|
120
143
|
conf_dir = os.path.join(app_dir, 'conf')
|
121
144
|
rkw.update(conf_dir=conf_dir)
|
145
|
+
|
146
|
+
conf_ns: ta.Dict[str, ta.Any] = dict(
|
147
|
+
**(conf_string_ns or {}),
|
148
|
+
app=spec.app.s,
|
149
|
+
app_dir=app_dir.rstrip('/'),
|
150
|
+
)
|
151
|
+
|
122
152
|
await self._conf.write_app_conf(
|
123
153
|
spec.conf,
|
124
154
|
conf_dir,
|
155
|
+
string_ns=conf_ns,
|
125
156
|
)
|
126
157
|
|
127
158
|
#
|
128
159
|
|
129
160
|
return DeployAppManager.PreparedApp(**rkw)
|
161
|
+
|
162
|
+
async def prepare_app_link(
|
163
|
+
self,
|
164
|
+
tags: DeployTagMap,
|
165
|
+
app_dir: str,
|
166
|
+
) -> PreparedApp:
|
167
|
+
spec_file = os.path.join(app_dir, 'spec.json')
|
168
|
+
with open(spec_file) as f: # noqa
|
169
|
+
spec_json = f.read()
|
170
|
+
|
171
|
+
spec: DeployAppSpec = self._msh.unmarshal_obj(json.loads(spec_json), DeployAppSpec)
|
172
|
+
|
173
|
+
#
|
174
|
+
|
175
|
+
app_tags = tags.add(*self._make_tags(spec))
|
176
|
+
|
177
|
+
#
|
178
|
+
|
179
|
+
rkw: ta.Dict[str, ta.Any] = dict(
|
180
|
+
spec=spec,
|
181
|
+
tags=app_tags,
|
182
|
+
|
183
|
+
dir=app_dir,
|
184
|
+
)
|
185
|
+
|
186
|
+
#
|
187
|
+
|
188
|
+
git_dir = os.path.join(app_dir, 'git')
|
189
|
+
check.state(os.path.isdir(git_dir))
|
190
|
+
rkw.update(git_dir=git_dir)
|
191
|
+
|
192
|
+
#
|
193
|
+
|
194
|
+
if spec.venv is not None:
|
195
|
+
venv_dir = os.path.join(app_dir, 'venv')
|
196
|
+
check.state(os.path.isdir(venv_dir))
|
197
|
+
rkw.update(venv_dir=venv_dir)
|
198
|
+
|
199
|
+
#
|
200
|
+
|
201
|
+
if spec.conf is not None:
|
202
|
+
conf_dir = os.path.join(app_dir, 'conf')
|
203
|
+
check.state(os.path.isdir(conf_dir))
|
204
|
+
rkw.update(conf_dir=conf_dir)
|
205
|
+
|
206
|
+
#
|
207
|
+
|
208
|
+
return DeployAppManager.PreparedApp(**rkw)
|
@@ -16,6 +16,8 @@ TODO:
|
|
16
16
|
- some things (venvs) cannot be moved, thus the /deploy/venvs dir
|
17
17
|
- ** ensure (enforce) equivalent relpath nesting
|
18
18
|
"""
|
19
|
+
import collections.abc
|
20
|
+
import functools
|
19
21
|
import os.path
|
20
22
|
import typing as ta
|
21
23
|
|
@@ -43,20 +45,66 @@ from .specs import NginxDeployAppConfContent
|
|
43
45
|
from .specs import RawDeployAppConfContent
|
44
46
|
|
45
47
|
|
48
|
+
T = ta.TypeVar('T')
|
49
|
+
|
50
|
+
|
51
|
+
##
|
52
|
+
|
53
|
+
|
46
54
|
class DeployConfManager:
|
47
|
-
def
|
55
|
+
def _process_conf_content(
|
56
|
+
self,
|
57
|
+
content: T,
|
58
|
+
*,
|
59
|
+
str_processor: ta.Optional[ta.Callable[[str], str]] = None,
|
60
|
+
) -> T:
|
61
|
+
def rec(o):
|
62
|
+
if isinstance(o, str):
|
63
|
+
if str_processor is not None:
|
64
|
+
return type(o)(str_processor(o))
|
65
|
+
|
66
|
+
elif isinstance(o, collections.abc.Mapping):
|
67
|
+
return type(o)([ # type: ignore
|
68
|
+
(rec(k), rec(v))
|
69
|
+
for k, v in o.items()
|
70
|
+
])
|
71
|
+
|
72
|
+
elif isinstance(o, collections.abc.Iterable):
|
73
|
+
return type(o)([ # type: ignore
|
74
|
+
rec(e) for e in o
|
75
|
+
])
|
76
|
+
|
77
|
+
return o
|
78
|
+
|
79
|
+
return rec(content)
|
80
|
+
|
81
|
+
#
|
82
|
+
|
83
|
+
def _render_app_conf_content(
|
84
|
+
self,
|
85
|
+
ac: DeployAppConfContent,
|
86
|
+
*,
|
87
|
+
str_processor: ta.Optional[ta.Callable[[str], str]] = None,
|
88
|
+
) -> str:
|
89
|
+
pcc = functools.partial(
|
90
|
+
self._process_conf_content,
|
91
|
+
str_processor=str_processor,
|
92
|
+
)
|
93
|
+
|
48
94
|
if isinstance(ac, RawDeployAppConfContent):
|
49
|
-
return ac.body
|
95
|
+
return pcc(ac.body)
|
50
96
|
|
51
97
|
elif isinstance(ac, JsonDeployAppConfContent):
|
52
|
-
|
98
|
+
json_obj = pcc(ac.obj)
|
99
|
+
return strip_with_newline(json_dumps_pretty(json_obj))
|
53
100
|
|
54
101
|
elif isinstance(ac, IniDeployAppConfContent):
|
55
|
-
|
102
|
+
ini_sections = pcc(ac.sections)
|
103
|
+
return strip_with_newline(render_ini_config(ini_sections))
|
56
104
|
|
57
105
|
elif isinstance(ac, NginxDeployAppConfContent):
|
58
|
-
|
59
|
-
return strip_with_newline(render_nginx_config_str(
|
106
|
+
nginx_items = NginxConfigItems.of(pcc(ac.items))
|
107
|
+
return strip_with_newline(render_nginx_config_str(nginx_items))
|
60
108
|
|
61
109
|
else:
|
62
110
|
raise TypeError(ac)
|
@@ -65,11 +113,16 @@ class DeployConfManager:
|
|
65
113
|
self,
|
66
114
|
acf: DeployAppConfFile,
|
67
115
|
app_conf_dir: str,
|
116
|
+
*,
|
117
|
+
str_processor: ta.Optional[ta.Callable[[str], str]] = None,
|
68
118
|
) -> None:
|
69
119
|
conf_file = os.path.join(app_conf_dir, acf.path)
|
70
120
|
check.arg(is_path_in_dir(app_conf_dir, conf_file))
|
71
121
|
|
72
|
-
body = self._render_app_conf_content(
|
122
|
+
body = self._render_app_conf_content(
|
123
|
+
acf.content,
|
124
|
+
str_processor=str_processor,
|
125
|
+
)
|
73
126
|
|
74
127
|
os.makedirs(os.path.dirname(conf_file), exist_ok=True)
|
75
128
|
|
@@ -80,11 +133,21 @@ class DeployConfManager:
|
|
80
133
|
self,
|
81
134
|
spec: DeployAppConfSpec,
|
82
135
|
app_conf_dir: str,
|
136
|
+
*,
|
137
|
+
string_ns: ta.Optional[ta.Mapping[str, ta.Any]] = None,
|
83
138
|
) -> None:
|
139
|
+
process_str: ta.Any
|
140
|
+
if string_ns is not None:
|
141
|
+
def process_str(s: str) -> str:
|
142
|
+
return s.format(**string_ns)
|
143
|
+
else:
|
144
|
+
process_str = None
|
145
|
+
|
84
146
|
for acf in spec.files or []:
|
85
147
|
await self._write_app_conf_file(
|
86
148
|
acf,
|
87
149
|
app_conf_dir,
|
150
|
+
str_processor=process_str,
|
88
151
|
)
|
89
152
|
|
90
153
|
#
|
ominfra/manage/deploy/deploy.py
CHANGED
@@ -9,6 +9,7 @@ from omlish.lite.json import json_dumps_pretty
|
|
9
9
|
from omlish.lite.marshal import ObjMarshalerManager
|
10
10
|
from omlish.lite.typing import Func0
|
11
11
|
from omlish.lite.typing import Func1
|
12
|
+
from omlish.os.paths import abs_real_path
|
12
13
|
from omlish.os.paths import relative_symlink
|
13
14
|
|
14
15
|
from .apps import DeployAppManager
|
@@ -19,16 +20,17 @@ from .paths.paths import DeployPath
|
|
19
20
|
from .specs import DeployAppSpec
|
20
21
|
from .specs import DeploySpec
|
21
22
|
from .systemd import DeploySystemdManager
|
22
|
-
from .tags import
|
23
|
+
from .tags import DeployApp
|
23
24
|
from .tags import DeployTagMap
|
24
25
|
from .tags import DeployTime
|
26
|
+
from .tmp import DeployHomeAtomics
|
25
27
|
from .types import DeployHome
|
26
28
|
|
27
29
|
|
28
30
|
##
|
29
31
|
|
30
32
|
|
31
|
-
DEPLOY_TAG_DATETIME_FMT = '%Y
|
33
|
+
DEPLOY_TAG_DATETIME_FMT = '%Y-%m-%d-T-%H-%M-%S-%f-Z'
|
32
34
|
|
33
35
|
|
34
36
|
DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
|
@@ -38,16 +40,24 @@ class DeployManager(DeployPathOwner):
|
|
38
40
|
def __init__(
|
39
41
|
self,
|
40
42
|
*,
|
43
|
+
atomics: DeployHomeAtomics,
|
44
|
+
|
41
45
|
utc_clock: ta.Optional[DeployManagerUtcClock] = None,
|
42
46
|
):
|
43
47
|
super().__init__()
|
44
48
|
|
49
|
+
self._atomics = atomics
|
50
|
+
|
45
51
|
self._utc_clock = utc_clock
|
46
52
|
|
47
53
|
#
|
48
54
|
|
55
|
+
# Home current link just points to CURRENT_DEPLOY_LINK, and is intended for user convenience.
|
56
|
+
HOME_CURRENT_LINK = DeployPath.parse('current')
|
57
|
+
|
49
58
|
DEPLOYS_DIR = DeployPath.parse('deploys/')
|
50
59
|
|
60
|
+
# Authoritative current symlink is not in deploy-home, just to prevent accidental corruption.
|
51
61
|
CURRENT_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}current')
|
52
62
|
DEPLOYING_DEPLOY_LINK = DeployPath.parse(f'{DEPLOYS_DIR}deploying')
|
53
63
|
|
@@ -80,6 +90,11 @@ class DeployManager(DeployPathOwner):
|
|
80
90
|
|
81
91
|
#
|
82
92
|
|
93
|
+
def render_path(self, home: DeployHome, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
|
94
|
+
return os.path.join(check.non_empty_str(home), pth.render(tags))
|
95
|
+
|
96
|
+
#
|
97
|
+
|
83
98
|
def _utc_now(self) -> datetime.datetime:
|
84
99
|
if self._utc_clock is not None:
|
85
100
|
return self._utc_clock() # noqa
|
@@ -89,6 +104,22 @@ class DeployManager(DeployPathOwner):
|
|
89
104
|
def make_deploy_time(self) -> DeployTime:
|
90
105
|
return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
|
91
106
|
|
107
|
+
#
|
108
|
+
|
109
|
+
def make_home_current_link(self, home: DeployHome) -> None:
|
110
|
+
home_current_link = os.path.join(check.non_empty_str(home), self.HOME_CURRENT_LINK.render())
|
111
|
+
current_deploy_link = os.path.join(check.non_empty_str(home), self.CURRENT_DEPLOY_LINK.render())
|
112
|
+
with self._atomics(home).begin_atomic_path_swap( # noqa
|
113
|
+
'file',
|
114
|
+
home_current_link,
|
115
|
+
auto_commit=True,
|
116
|
+
) as dst_swap:
|
117
|
+
os.unlink(dst_swap.tmp_path)
|
118
|
+
os.symlink(
|
119
|
+
os.path.relpath(current_deploy_link, os.path.dirname(dst_swap.dst_path)),
|
120
|
+
dst_swap.tmp_path,
|
121
|
+
)
|
122
|
+
|
92
123
|
|
93
124
|
##
|
94
125
|
|
@@ -130,18 +161,18 @@ class DeployDriver:
|
|
130
161
|
#
|
131
162
|
|
132
163
|
@property
|
133
|
-
def
|
164
|
+
def tags(self) -> DeployTagMap:
|
134
165
|
return DeployTagMap(
|
135
166
|
self._time,
|
136
167
|
self._spec.key(),
|
137
168
|
)
|
138
169
|
|
139
|
-
def
|
140
|
-
return os.path.join(self._home, pth.render(tags if tags is not None else self.
|
170
|
+
def render_path(self, pth: DeployPath, tags: ta.Optional[DeployTagMap] = None) -> str:
|
171
|
+
return os.path.join(self._home, pth.render(tags if tags is not None else self.tags))
|
141
172
|
|
142
173
|
@property
|
143
|
-
def
|
144
|
-
return self.
|
174
|
+
def dir(self) -> str:
|
175
|
+
return self.render_path(self._deploys.DEPLOY_DIR)
|
145
176
|
|
146
177
|
#
|
147
178
|
|
@@ -150,25 +181,37 @@ class DeployDriver:
|
|
150
181
|
|
151
182
|
#
|
152
183
|
|
184
|
+
das: ta.Set[DeployApp] = {a.app for a in self._spec.apps}
|
185
|
+
las: ta.Set[DeployApp] = set(self._spec.app_links.apps)
|
186
|
+
ras: ta.Set[DeployApp] = set(self._spec.app_links.removed_apps)
|
187
|
+
check.empty(das & (las | ras))
|
188
|
+
check.empty(las & ras)
|
189
|
+
|
190
|
+
#
|
191
|
+
|
153
192
|
self._paths.validate_deploy_paths()
|
154
193
|
|
155
194
|
#
|
156
195
|
|
157
|
-
os.makedirs(self.
|
196
|
+
os.makedirs(self.dir)
|
158
197
|
|
159
198
|
#
|
160
199
|
|
161
|
-
spec_file = self.
|
200
|
+
spec_file = self.render_path(self._deploys.DEPLOY_SPEC_FILE)
|
162
201
|
with open(spec_file, 'w') as f: # noqa
|
163
202
|
f.write(spec_json)
|
164
203
|
|
165
204
|
#
|
166
205
|
|
167
|
-
deploying_link = self.
|
206
|
+
deploying_link = self.render_path(self._deploys.DEPLOYING_DEPLOY_LINK)
|
207
|
+
current_link = self.render_path(self._deploys.CURRENT_DEPLOY_LINK)
|
208
|
+
|
209
|
+
#
|
210
|
+
|
168
211
|
if os.path.exists(deploying_link):
|
169
212
|
os.unlink(deploying_link)
|
170
213
|
relative_symlink(
|
171
|
-
self.
|
214
|
+
self.dir,
|
172
215
|
deploying_link,
|
173
216
|
target_is_directory=True,
|
174
217
|
make_dirs=True,
|
@@ -180,16 +223,30 @@ class DeployDriver:
|
|
180
223
|
self._deploys.APPS_DEPLOY_DIR,
|
181
224
|
self._deploys.CONFS_DEPLOY_DIR,
|
182
225
|
]:
|
183
|
-
os.makedirs(self.
|
226
|
+
os.makedirs(self.render_path(md))
|
184
227
|
|
185
228
|
#
|
186
229
|
|
230
|
+
if not self._spec.app_links.exclude_unspecified:
|
231
|
+
cad = abs_real_path(os.path.join(current_link, 'apps'))
|
232
|
+
if os.path.exists(cad):
|
233
|
+
for d in os.listdir(cad):
|
234
|
+
if (da := DeployApp(d)) not in das and da not in ras:
|
235
|
+
las.add(da)
|
236
|
+
|
237
|
+
for la in las:
|
238
|
+
await self._drive_app_link(
|
239
|
+
la,
|
240
|
+
current_link,
|
241
|
+
)
|
242
|
+
|
187
243
|
for app in self._spec.apps:
|
188
|
-
await self.
|
244
|
+
await self._drive_app_deploy(
|
245
|
+
app,
|
246
|
+
)
|
189
247
|
|
190
248
|
#
|
191
249
|
|
192
|
-
current_link = self.render_deploy_path(self._deploys.CURRENT_DEPLOY_LINK)
|
193
250
|
os.replace(deploying_link, current_link)
|
194
251
|
|
195
252
|
#
|
@@ -197,31 +254,33 @@ class DeployDriver:
|
|
197
254
|
await self._systemd.sync_systemd(
|
198
255
|
self._spec.systemd,
|
199
256
|
self._home,
|
200
|
-
os.path.join(self.
|
257
|
+
os.path.join(self.dir, 'conf', 'systemd'), # FIXME
|
201
258
|
)
|
202
259
|
|
203
|
-
|
260
|
+
#
|
204
261
|
|
205
|
-
|
206
|
-
app_tags = self.deploy_tags.add(
|
207
|
-
app.app,
|
208
|
-
app.key(),
|
209
|
-
DeployAppRev(app.git.rev),
|
210
|
-
)
|
262
|
+
self._deploys.make_home_current_link(self._home)
|
211
263
|
|
212
|
-
|
264
|
+
#
|
265
|
+
|
266
|
+
async def _drive_app_deploy(self, app: DeployAppSpec) -> None:
|
267
|
+
current_deploy_link = os.path.join(self._home, self._deploys.CURRENT_DEPLOY_LINK.render())
|
213
268
|
|
214
|
-
|
269
|
+
pa = await self._apps.prepare_app(
|
215
270
|
app,
|
216
271
|
self._home,
|
217
|
-
|
272
|
+
self.tags,
|
273
|
+
conf_string_ns=dict(
|
274
|
+
deploy_home=self._home,
|
275
|
+
current_deploy_link=current_deploy_link,
|
276
|
+
),
|
218
277
|
)
|
219
278
|
|
220
279
|
#
|
221
280
|
|
222
|
-
app_link = self.
|
281
|
+
app_link = self.render_path(self._deploys.APP_DEPLOY_LINK, pa.tags)
|
223
282
|
relative_symlink(
|
224
|
-
|
283
|
+
pa.dir,
|
225
284
|
app_link,
|
226
285
|
target_is_directory=True,
|
227
286
|
make_dirs=True,
|
@@ -229,11 +288,47 @@ class DeployDriver:
|
|
229
288
|
|
230
289
|
#
|
231
290
|
|
232
|
-
|
233
|
-
|
291
|
+
await self._drive_app_configure(pa)
|
292
|
+
|
293
|
+
async def _drive_app_link(
|
294
|
+
self,
|
295
|
+
app: DeployApp,
|
296
|
+
current_link: str,
|
297
|
+
) -> None:
|
298
|
+
app_link = os.path.join(abs_real_path(current_link), 'apps', app.s)
|
299
|
+
check.state(os.path.islink(app_link))
|
300
|
+
|
301
|
+
app_dir = abs_real_path(app_link)
|
302
|
+
check.state(os.path.isdir(app_dir))
|
303
|
+
|
304
|
+
#
|
305
|
+
|
306
|
+
pa = await self._apps.prepare_app_link(
|
307
|
+
self.tags,
|
308
|
+
app_dir,
|
309
|
+
)
|
310
|
+
|
311
|
+
#
|
312
|
+
|
313
|
+
relative_symlink(
|
314
|
+
app_dir,
|
315
|
+
os.path.join(self.dir, 'apps', app.s),
|
316
|
+
target_is_directory=True,
|
317
|
+
)
|
318
|
+
|
319
|
+
#
|
320
|
+
|
321
|
+
await self._drive_app_configure(pa)
|
322
|
+
|
323
|
+
async def _drive_app_configure(
|
324
|
+
self,
|
325
|
+
pa: DeployAppManager.PreparedApp,
|
326
|
+
) -> None:
|
327
|
+
deploy_conf_dir = self.render_path(self._deploys.CONFS_DEPLOY_DIR)
|
328
|
+
if pa.spec.conf is not None:
|
234
329
|
await self._conf.link_app_conf(
|
235
|
-
|
236
|
-
|
237
|
-
check.non_empty_str(
|
330
|
+
pa.spec.conf,
|
331
|
+
pa.tags,
|
332
|
+
check.non_empty_str(pa.conf_dir),
|
238
333
|
deploy_conf_dir,
|
239
334
|
)
|
ominfra/manage/deploy/git.py
CHANGED
@@ -91,7 +91,9 @@ class DeployGitManager(SingleDirDeployPathOwner):
|
|
91
91
|
|
92
92
|
async def fetch(self, rev: DeployRev) -> None:
|
93
93
|
await self.init()
|
94
|
-
|
94
|
+
|
95
|
+
# This fetch shouldn't be depth=1 - git doesn't reuse local data with shallow fetches.
|
96
|
+
await self._call('git', 'fetch', 'origin', rev)
|
95
97
|
|
96
98
|
#
|
97
99
|
|
ominfra/manage/deploy/specs.py
CHANGED
@@ -91,6 +91,15 @@ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
|
|
91
91
|
return DeployAppKey(self._key_str())
|
92
92
|
|
93
93
|
|
94
|
+
@dc.dataclass(frozen=True)
|
95
|
+
class DeployAppLinksSpec:
|
96
|
+
apps: ta.Sequence[DeployApp] = ()
|
97
|
+
|
98
|
+
removed_apps: ta.Sequence[DeployApp] = ()
|
99
|
+
|
100
|
+
exclude_unspecified: bool = False
|
101
|
+
|
102
|
+
|
94
103
|
##
|
95
104
|
|
96
105
|
|
@@ -109,6 +118,8 @@ class DeploySpec(DeploySpecKeyed[DeployKey]):
|
|
109
118
|
|
110
119
|
apps: ta.Sequence[DeployAppSpec] = ()
|
111
120
|
|
121
|
+
app_links: DeployAppLinksSpec = DeployAppLinksSpec()
|
122
|
+
|
112
123
|
systemd: ta.Optional[DeploySystemdSpec] = None
|
113
124
|
|
114
125
|
def __post_init__(self) -> None:
|
ominfra/manage/deploy/systemd.py
CHANGED
@@ -1,34 +1,18 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
"""
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
cat ~/.config/systemd/user/sleep-infinity.service
|
10
|
-
[Unit]
|
11
|
-
Description=User-specific service to run 'sleep infinity'
|
12
|
-
After=default.target
|
13
|
-
|
14
|
-
[Service]
|
15
|
-
ExecStart=/bin/sleep infinity
|
16
|
-
Restart=always
|
17
|
-
RestartSec=5
|
18
|
-
|
19
|
-
[Install]
|
20
|
-
WantedBy=default.target
|
21
|
-
|
22
|
-
systemctl --user daemon-reload
|
23
|
-
|
24
|
-
systemctl --user enable sleep-infinity.service
|
25
|
-
systemctl --user start sleep-infinity.service
|
26
|
-
|
27
|
-
systemctl --user status sleep-infinity.service
|
3
|
+
TODO:
|
4
|
+
- verify - systemd-analyze
|
5
|
+
- sudo loginctl enable-linger "$USER"
|
6
|
+
- idemp kill services that shouldn't be running, start ones that should
|
7
|
+
- ideally only those defined by links to deploy home
|
8
|
+
- ominfra.systemd / x.sd_orphans
|
28
9
|
"""
|
29
10
|
import os.path
|
11
|
+
import shutil
|
12
|
+
import sys
|
30
13
|
import typing as ta
|
31
14
|
|
15
|
+
from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
|
32
16
|
from omlish.lite.check import check
|
33
17
|
from omlish.os.paths import abs_real_path
|
34
18
|
from omlish.os.paths import is_path_in_dir
|
@@ -73,6 +57,8 @@ class DeploySystemdManager:
|
|
73
57
|
if not spec:
|
74
58
|
return
|
75
59
|
|
60
|
+
#
|
61
|
+
|
76
62
|
if not (ud := spec.unit_dir):
|
77
63
|
return
|
78
64
|
|
@@ -80,6 +66,8 @@ class DeploySystemdManager:
|
|
80
66
|
|
81
67
|
os.makedirs(ud, exist_ok=True)
|
82
68
|
|
69
|
+
#
|
70
|
+
|
83
71
|
uld = {
|
84
72
|
n: p
|
85
73
|
for n, p in self._scan_link_dir(ud).items()
|
@@ -91,8 +79,11 @@ class DeploySystemdManager:
|
|
91
79
|
else:
|
92
80
|
cld = {}
|
93
81
|
|
94
|
-
|
95
|
-
|
82
|
+
#
|
83
|
+
|
84
|
+
ns = sorted(set(uld) | set(cld))
|
85
|
+
|
86
|
+
for n in ns:
|
96
87
|
cl = cld.get(n)
|
97
88
|
if cl is None:
|
98
89
|
os.unlink(os.path.join(ud, n))
|
@@ -108,3 +99,34 @@ class DeploySystemdManager:
|
|
108
99
|
os.path.relpath(cl, os.path.dirname(dst_swap.dst_path)),
|
109
100
|
dst_swap.tmp_path,
|
110
101
|
)
|
102
|
+
|
103
|
+
#
|
104
|
+
|
105
|
+
if sys.platform == 'linux' and shutil.which('systemctl') is not None:
|
106
|
+
async def reload() -> None:
|
107
|
+
await asyncio_subprocesses.check_call('systemctl', '--user', 'daemon-reload')
|
108
|
+
|
109
|
+
await reload()
|
110
|
+
|
111
|
+
num_deleted = 0
|
112
|
+
for n in ns:
|
113
|
+
if n.endswith('.service'):
|
114
|
+
cl = cld.get(n)
|
115
|
+
ul = uld.get(n)
|
116
|
+
if cl is not None:
|
117
|
+
if ul is None:
|
118
|
+
cs = ['enable', 'start']
|
119
|
+
else:
|
120
|
+
cs = ['restart']
|
121
|
+
else: # noqa
|
122
|
+
if ul is not None:
|
123
|
+
cs = ['stop']
|
124
|
+
num_deleted += 1
|
125
|
+
else:
|
126
|
+
cs = []
|
127
|
+
|
128
|
+
for c in cs:
|
129
|
+
await asyncio_subprocesses.check_call('systemctl', '--user', c, n)
|
130
|
+
|
131
|
+
if num_deleted:
|
132
|
+
await reload()
|