ominfra 0.0.0.dev7__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.
Files changed (44) hide show
  1. ominfra/__about__.py +27 -0
  2. ominfra/__init__.py +0 -0
  3. ominfra/bootstrap/__init__.py +0 -0
  4. ominfra/bootstrap/bootstrap.py +8 -0
  5. ominfra/cmds.py +83 -0
  6. ominfra/deploy/__init__.py +0 -0
  7. ominfra/deploy/_executor.py +1036 -0
  8. ominfra/deploy/configs.py +19 -0
  9. ominfra/deploy/executor/__init__.py +1 -0
  10. ominfra/deploy/executor/base.py +115 -0
  11. ominfra/deploy/executor/concerns/__init__.py +0 -0
  12. ominfra/deploy/executor/concerns/dirs.py +28 -0
  13. ominfra/deploy/executor/concerns/nginx.py +47 -0
  14. ominfra/deploy/executor/concerns/repo.py +17 -0
  15. ominfra/deploy/executor/concerns/supervisor.py +46 -0
  16. ominfra/deploy/executor/concerns/systemd.py +88 -0
  17. ominfra/deploy/executor/concerns/user.py +25 -0
  18. ominfra/deploy/executor/concerns/venv.py +22 -0
  19. ominfra/deploy/executor/main.py +119 -0
  20. ominfra/deploy/poly/__init__.py +1 -0
  21. ominfra/deploy/poly/_main.py +725 -0
  22. ominfra/deploy/poly/base.py +179 -0
  23. ominfra/deploy/poly/configs.py +38 -0
  24. ominfra/deploy/poly/deploy.py +25 -0
  25. ominfra/deploy/poly/main.py +18 -0
  26. ominfra/deploy/poly/nginx.py +60 -0
  27. ominfra/deploy/poly/repo.py +41 -0
  28. ominfra/deploy/poly/runtime.py +39 -0
  29. ominfra/deploy/poly/site.py +11 -0
  30. ominfra/deploy/poly/supervisor.py +64 -0
  31. ominfra/deploy/poly/venv.py +52 -0
  32. ominfra/deploy/remote.py +91 -0
  33. ominfra/pyremote/__init__.py +0 -0
  34. ominfra/pyremote/_runcommands.py +824 -0
  35. ominfra/pyremote/bootstrap.py +149 -0
  36. ominfra/pyremote/runcommands.py +56 -0
  37. ominfra/ssh.py +191 -0
  38. ominfra/tools/__init__.py +0 -0
  39. ominfra/tools/listresources.py +256 -0
  40. ominfra-0.0.0.dev7.dist-info/LICENSE +21 -0
  41. ominfra-0.0.0.dev7.dist-info/METADATA +19 -0
  42. ominfra-0.0.0.dev7.dist-info/RECORD +44 -0
  43. ominfra-0.0.0.dev7.dist-info/WHEEL +5 -0
  44. ominfra-0.0.0.dev7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1036 @@
1
+ #!/usr/bin/env python3
2
+ # noinspection DuplicatedCode
3
+ # @omdev-amalg-output executor/main.py
4
+ r"""
5
+ TODO:
6
+ - flock
7
+ - interp.py
8
+ - systemd
9
+
10
+ deployment matrix
11
+ - os: ubuntu / amzn / generic
12
+ - arch: amd64 / arm64
13
+ - host: bare / docker
14
+ - init: supervisor-provided / supervisor-must-configure / systemd (/ self?)
15
+ - interp: system / pyenv / interp.py
16
+ - venv: none / yes
17
+ - nginx: no / provided / must-configure
18
+
19
+ ==
20
+
21
+ ~deploy
22
+ deploy.pid (flock)
23
+ /app
24
+ /<appspec> - shallow clone
25
+ /conf
26
+ /env
27
+ <appspec>.env
28
+ /nginx
29
+ <appspec>.conf
30
+ /supervisor
31
+ <appspec>.conf
32
+ /venv
33
+ /<appspec>
34
+
35
+ ?
36
+ /logs
37
+ /wrmsr--omlish--<spec>
38
+
39
+ spec = <name>--<rev>--<when>
40
+
41
+ https://docs.docker.com/config/containers/multi-service_container/#use-a-process-manager
42
+ https://serverfault.com/questions/211525/supervisor-not-loading-new-configuration-files
43
+ """ # noqa
44
+ # ruff: noqa: UP007
45
+ import abc
46
+ import argparse
47
+ import base64
48
+ import collections.abc
49
+ import dataclasses as dc
50
+ import datetime
51
+ import decimal
52
+ import enum
53
+ import fractions
54
+ import functools
55
+ import inspect
56
+ import json
57
+ import logging
58
+ import os
59
+ import os.path
60
+ import pwd
61
+ import shlex
62
+ import subprocess
63
+ import sys
64
+ import textwrap
65
+ import typing as ta
66
+ import uuid
67
+ import weakref # noqa
68
+
69
+
70
+ T = ta.TypeVar('T')
71
+
72
+
73
+ ########################################
74
+ # ../../configs.py
75
+
76
+
77
+ @dc.dataclass(frozen=True)
78
+ class DeployConfig:
79
+ python_bin: str
80
+ app_name: str
81
+ repo_url: str
82
+ revision: str
83
+ requirements_txt: str
84
+ entrypoint: str
85
+
86
+
87
+ @dc.dataclass(frozen=True)
88
+ class HostConfig:
89
+ username: str = 'deploy'
90
+
91
+ global_supervisor_conf_file_path: str = '/etc/supervisor/conf.d/supervisord.conf'
92
+ global_nginx_conf_file_path: str = '/etc/nginx/sites-enabled/deploy.conf'
93
+
94
+
95
+ ########################################
96
+ # ../../../../omlish/lite/cached.py
97
+
98
+
99
+ class cached_nullary: # noqa
100
+ def __init__(self, fn):
101
+ super().__init__()
102
+ self._fn = fn
103
+ self._value = self._missing = object()
104
+ functools.update_wrapper(self, fn)
105
+
106
+ def __call__(self, *args, **kwargs): # noqa
107
+ if self._value is self._missing:
108
+ self._value = self._fn()
109
+ return self._value
110
+
111
+ def __get__(self, instance, owner): # noqa
112
+ bound = instance.__dict__[self._fn.__name__] = self.__class__(self._fn.__get__(instance, owner))
113
+ return bound
114
+
115
+
116
+ ########################################
117
+ # ../../../../omlish/lite/check.py
118
+ # ruff: noqa: UP006 UP007
119
+
120
+
121
+ def check_isinstance(v: T, spec: ta.Union[ta.Type[T], tuple]) -> T:
122
+ if not isinstance(v, spec):
123
+ raise TypeError(v)
124
+ return v
125
+
126
+
127
+ def check_not_isinstance(v: T, spec: ta.Union[type, tuple]) -> T:
128
+ if isinstance(v, spec):
129
+ raise TypeError(v)
130
+ return v
131
+
132
+
133
+ def check_not_none(v: ta.Optional[T]) -> T:
134
+ if v is None:
135
+ raise ValueError
136
+ return v
137
+
138
+
139
+ def check_not(v: ta.Any) -> None:
140
+ if v:
141
+ raise ValueError(v)
142
+ return v
143
+
144
+
145
+ ########################################
146
+ # ../../../../omlish/lite/json.py
147
+
148
+
149
+ ##
150
+
151
+
152
+ JSON_PRETTY_INDENT = 2
153
+
154
+ JSON_PRETTY_KWARGS: ta.Mapping[str, ta.Any] = dict(
155
+ indent=JSON_PRETTY_INDENT,
156
+ )
157
+
158
+ json_dump_pretty: ta.Callable[..., bytes] = functools.partial(json.dump, **JSON_PRETTY_KWARGS) # type: ignore
159
+ json_dumps_pretty: ta.Callable[..., str] = functools.partial(json.dumps, **JSON_PRETTY_KWARGS)
160
+
161
+
162
+ ##
163
+
164
+
165
+ JSON_COMPACT_SEPARATORS = (',', ':')
166
+
167
+ JSON_COMPACT_KWARGS: ta.Mapping[str, ta.Any] = dict(
168
+ indent=None,
169
+ separators=JSON_COMPACT_SEPARATORS,
170
+ )
171
+
172
+ json_dump_compact: ta.Callable[..., bytes] = functools.partial(json.dump, **JSON_COMPACT_KWARGS) # type: ignore
173
+ json_dumps_compact: ta.Callable[..., str] = functools.partial(json.dumps, **JSON_COMPACT_KWARGS)
174
+
175
+
176
+ ########################################
177
+ # ../../../../omlish/lite/reflect.py
178
+ # ruff: noqa: UP006
179
+
180
+
181
+ _GENERIC_ALIAS_TYPES = (
182
+ ta._GenericAlias, # type: ignore # noqa
183
+ *([ta._SpecialGenericAlias] if hasattr(ta, '_SpecialGenericAlias') else []), # noqa
184
+ )
185
+
186
+
187
+ def is_generic_alias(obj, *, origin: ta.Any = None) -> bool:
188
+ return (
189
+ isinstance(obj, _GENERIC_ALIAS_TYPES) and
190
+ (origin is None or ta.get_origin(obj) is origin)
191
+ )
192
+
193
+
194
+ is_union_alias = functools.partial(is_generic_alias, origin=ta.Union)
195
+ is_callable_alias = functools.partial(is_generic_alias, origin=ta.Callable)
196
+
197
+
198
+ def is_optional_alias(spec: ta.Any) -> bool:
199
+ return (
200
+ isinstance(spec, _GENERIC_ALIAS_TYPES) and # noqa
201
+ ta.get_origin(spec) is ta.Union and
202
+ len(ta.get_args(spec)) == 2 and
203
+ any(a in (None, type(None)) for a in ta.get_args(spec))
204
+ )
205
+
206
+
207
+ def get_optional_alias_arg(spec: ta.Any) -> ta.Any:
208
+ [it] = [it for it in ta.get_args(spec) if it not in (None, type(None))]
209
+ return it
210
+
211
+
212
+ def deep_subclasses(cls: ta.Type[T]) -> ta.Iterator[ta.Type[T]]:
213
+ seen = set()
214
+ todo = list(reversed(cls.__subclasses__()))
215
+ while todo:
216
+ cur = todo.pop()
217
+ if cur in seen:
218
+ continue
219
+ seen.add(cur)
220
+ yield cur
221
+ todo.extend(reversed(cur.__subclasses__()))
222
+
223
+
224
+ ########################################
225
+ # ../../../../omlish/lite/logs.py
226
+ """
227
+ TODO:
228
+ - debug
229
+ """
230
+ # ruff: noqa: UP007
231
+
232
+
233
+ log = logging.getLogger(__name__)
234
+
235
+
236
+ class JsonLogFormatter(logging.Formatter):
237
+
238
+ KEYS: ta.Mapping[str, bool] = {
239
+ 'name': False,
240
+ 'msg': False,
241
+ 'args': False,
242
+ 'levelname': False,
243
+ 'levelno': False,
244
+ 'pathname': False,
245
+ 'filename': False,
246
+ 'module': False,
247
+ 'exc_info': True,
248
+ 'exc_text': True,
249
+ 'stack_info': True,
250
+ 'lineno': False,
251
+ 'funcName': False,
252
+ 'created': False,
253
+ 'msecs': False,
254
+ 'relativeCreated': False,
255
+ 'thread': False,
256
+ 'threadName': False,
257
+ 'processName': False,
258
+ 'process': False,
259
+ }
260
+
261
+ def format(self, record: logging.LogRecord) -> str:
262
+ dct = {
263
+ k: v
264
+ for k, o in self.KEYS.items()
265
+ for v in [getattr(record, k)]
266
+ if not (o and v is None)
267
+ }
268
+ return json_dumps_compact(dct)
269
+
270
+
271
+ def configure_standard_logging(level: ta.Union[int, str] = logging.INFO) -> None:
272
+ logging.root.addHandler(logging.StreamHandler())
273
+ logging.root.setLevel(level)
274
+
275
+
276
+ ########################################
277
+ # ../../../../omlish/lite/marshal.py
278
+ """
279
+ TODO:
280
+ - pickle stdlib objs? have to pin to 3.8 pickle protocol, will be cross-version
281
+ """
282
+ # ruff: noqa: UP006 UP007
283
+
284
+
285
+ ##
286
+
287
+
288
+ class ObjMarshaler(abc.ABC):
289
+ @abc.abstractmethod
290
+ def marshal(self, o: ta.Any) -> ta.Any:
291
+ raise NotImplementedError
292
+
293
+ @abc.abstractmethod
294
+ def unmarshal(self, o: ta.Any) -> ta.Any:
295
+ raise NotImplementedError
296
+
297
+
298
+ class NopObjMarshaler(ObjMarshaler):
299
+ def marshal(self, o: ta.Any) -> ta.Any:
300
+ return o
301
+
302
+ def unmarshal(self, o: ta.Any) -> ta.Any:
303
+ return o
304
+
305
+
306
+ @dc.dataclass()
307
+ class ProxyObjMarshaler(ObjMarshaler):
308
+ m: ta.Optional[ObjMarshaler] = None
309
+
310
+ def marshal(self, o: ta.Any) -> ta.Any:
311
+ return check_not_none(self.m).marshal(o)
312
+
313
+ def unmarshal(self, o: ta.Any) -> ta.Any:
314
+ return check_not_none(self.m).unmarshal(o)
315
+
316
+
317
+ @dc.dataclass(frozen=True)
318
+ class CastObjMarshaler(ObjMarshaler):
319
+ ty: type
320
+
321
+ def marshal(self, o: ta.Any) -> ta.Any:
322
+ return o
323
+
324
+ def unmarshal(self, o: ta.Any) -> ta.Any:
325
+ return self.ty(o)
326
+
327
+
328
+ class DynamicObjMarshaler(ObjMarshaler):
329
+ def marshal(self, o: ta.Any) -> ta.Any:
330
+ return marshal_obj(o)
331
+
332
+ def unmarshal(self, o: ta.Any) -> ta.Any:
333
+ return o
334
+
335
+
336
+ @dc.dataclass(frozen=True)
337
+ class Base64ObjMarshaler(ObjMarshaler):
338
+ ty: type
339
+
340
+ def marshal(self, o: ta.Any) -> ta.Any:
341
+ return base64.b64encode(o).decode('ascii')
342
+
343
+ def unmarshal(self, o: ta.Any) -> ta.Any:
344
+ return self.ty(base64.b64decode(o))
345
+
346
+
347
+ @dc.dataclass(frozen=True)
348
+ class EnumObjMarshaler(ObjMarshaler):
349
+ ty: type
350
+
351
+ def marshal(self, o: ta.Any) -> ta.Any:
352
+ return o.name
353
+
354
+ def unmarshal(self, o: ta.Any) -> ta.Any:
355
+ return self.ty.__members__[o] # type: ignore
356
+
357
+
358
+ @dc.dataclass(frozen=True)
359
+ class OptionalObjMarshaler(ObjMarshaler):
360
+ item: ObjMarshaler
361
+
362
+ def marshal(self, o: ta.Any) -> ta.Any:
363
+ if o is None:
364
+ return None
365
+ return self.item.marshal(o)
366
+
367
+ def unmarshal(self, o: ta.Any) -> ta.Any:
368
+ if o is None:
369
+ return None
370
+ return self.item.unmarshal(o)
371
+
372
+
373
+ @dc.dataclass(frozen=True)
374
+ class MappingObjMarshaler(ObjMarshaler):
375
+ ty: type
376
+ km: ObjMarshaler
377
+ vm: ObjMarshaler
378
+
379
+ def marshal(self, o: ta.Any) -> ta.Any:
380
+ return {self.km.marshal(k): self.vm.marshal(v) for k, v in o.items()}
381
+
382
+ def unmarshal(self, o: ta.Any) -> ta.Any:
383
+ return self.ty((self.km.unmarshal(k), self.vm.unmarshal(v)) for k, v in o.items())
384
+
385
+
386
+ @dc.dataclass(frozen=True)
387
+ class IterableObjMarshaler(ObjMarshaler):
388
+ ty: type
389
+ item: ObjMarshaler
390
+
391
+ def marshal(self, o: ta.Any) -> ta.Any:
392
+ return [self.item.marshal(e) for e in o]
393
+
394
+ def unmarshal(self, o: ta.Any) -> ta.Any:
395
+ return self.ty(self.item.unmarshal(e) for e in o)
396
+
397
+
398
+ @dc.dataclass(frozen=True)
399
+ class DataclassObjMarshaler(ObjMarshaler):
400
+ ty: type
401
+ fs: ta.Mapping[str, ObjMarshaler]
402
+
403
+ def marshal(self, o: ta.Any) -> ta.Any:
404
+ return {k: m.marshal(getattr(o, k)) for k, m in self.fs.items()}
405
+
406
+ def unmarshal(self, o: ta.Any) -> ta.Any:
407
+ return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items()})
408
+
409
+
410
+ @dc.dataclass(frozen=True)
411
+ class PolymorphicObjMarshaler(ObjMarshaler):
412
+ class Impl(ta.NamedTuple):
413
+ ty: type
414
+ tag: str
415
+ m: ObjMarshaler
416
+
417
+ impls_by_ty: ta.Mapping[type, Impl]
418
+ impls_by_tag: ta.Mapping[str, Impl]
419
+
420
+ def marshal(self, o: ta.Any) -> ta.Any:
421
+ impl = self.impls_by_ty[type(o)]
422
+ return {impl.tag: impl.m.marshal(o)}
423
+
424
+ def unmarshal(self, o: ta.Any) -> ta.Any:
425
+ [(t, v)] = o.items()
426
+ impl = self.impls_by_tag[t]
427
+ return impl.m.unmarshal(v)
428
+
429
+
430
+ @dc.dataclass(frozen=True)
431
+ class DatetimeObjMarshaler(ObjMarshaler):
432
+ ty: type
433
+
434
+ def marshal(self, o: ta.Any) -> ta.Any:
435
+ return o.isoformat()
436
+
437
+ def unmarshal(self, o: ta.Any) -> ta.Any:
438
+ return self.ty.fromisoformat(o) # type: ignore
439
+
440
+
441
+ class DecimalObjMarshaler(ObjMarshaler):
442
+ def marshal(self, o: ta.Any) -> ta.Any:
443
+ return str(check_isinstance(o, decimal.Decimal))
444
+
445
+ def unmarshal(self, v: ta.Any) -> ta.Any:
446
+ return decimal.Decimal(check_isinstance(v, str))
447
+
448
+
449
+ class FractionObjMarshaler(ObjMarshaler):
450
+ def marshal(self, o: ta.Any) -> ta.Any:
451
+ fr = check_isinstance(o, fractions.Fraction)
452
+ return [fr.numerator, fr.denominator]
453
+
454
+ def unmarshal(self, v: ta.Any) -> ta.Any:
455
+ num, denom = check_isinstance(v, list)
456
+ return fractions.Fraction(num, denom)
457
+
458
+
459
+ class UuidObjMarshaler(ObjMarshaler):
460
+ def marshal(self, o: ta.Any) -> ta.Any:
461
+ return str(o)
462
+
463
+ def unmarshal(self, o: ta.Any) -> ta.Any:
464
+ return uuid.UUID(o)
465
+
466
+
467
+ _OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = {
468
+ **{t: NopObjMarshaler() for t in (type(None),)},
469
+ **{t: CastObjMarshaler(t) for t in (int, float, str, bool)},
470
+ **{t: Base64ObjMarshaler(t) for t in (bytes, bytearray)},
471
+ **{t: IterableObjMarshaler(t, DynamicObjMarshaler()) for t in (list, tuple, set, frozenset)},
472
+ **{t: MappingObjMarshaler(t, DynamicObjMarshaler(), DynamicObjMarshaler()) for t in (dict,)},
473
+
474
+ ta.Any: DynamicObjMarshaler(),
475
+
476
+ **{t: DatetimeObjMarshaler(t) for t in (datetime.date, datetime.time, datetime.datetime)},
477
+ decimal.Decimal: DecimalObjMarshaler(),
478
+ fractions.Fraction: FractionObjMarshaler(),
479
+ uuid.UUID: UuidObjMarshaler(),
480
+ }
481
+
482
+ _OBJ_MARSHALER_GENERIC_MAPPING_TYPES: ta.Dict[ta.Any, type] = {
483
+ **{t: t for t in (dict,)},
484
+ **{t: dict for t in (collections.abc.Mapping, collections.abc.MutableMapping)},
485
+ }
486
+
487
+ _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES: ta.Dict[ta.Any, type] = {
488
+ **{t: t for t in (list, tuple, set, frozenset)},
489
+ **{t: frozenset for t in (collections.abc.Set, collections.abc.MutableSet)},
490
+ **{t: tuple for t in (collections.abc.Sequence, collections.abc.MutableSequence)},
491
+ }
492
+
493
+
494
+ def register_opj_marshaler(ty: ta.Any, m: ObjMarshaler) -> None:
495
+ if ty in _OBJ_MARSHALERS:
496
+ raise KeyError(ty)
497
+ _OBJ_MARSHALERS[ty] = m
498
+
499
+
500
+ def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
501
+ if isinstance(ty, type) and abc.ABC in ty.__bases__:
502
+ impls = [ # type: ignore
503
+ PolymorphicObjMarshaler.Impl(
504
+ ity,
505
+ ity.__qualname__,
506
+ get_obj_marshaler(ity),
507
+ )
508
+ for ity in deep_subclasses(ty)
509
+ if abc.ABC not in ity.__bases__
510
+ ]
511
+ return PolymorphicObjMarshaler(
512
+ {i.ty: i for i in impls},
513
+ {i.tag: i for i in impls},
514
+ )
515
+
516
+ if isinstance(ty, type) and issubclass(ty, enum.Enum):
517
+ return EnumObjMarshaler(ty)
518
+
519
+ if dc.is_dataclass(ty):
520
+ return DataclassObjMarshaler(
521
+ ty,
522
+ {f.name: get_obj_marshaler(f.type) for f in dc.fields(ty)},
523
+ )
524
+
525
+ if is_generic_alias(ty):
526
+ try:
527
+ mt = _OBJ_MARSHALER_GENERIC_MAPPING_TYPES[ta.get_origin(ty)]
528
+ except KeyError:
529
+ pass
530
+ else:
531
+ k, v = ta.get_args(ty)
532
+ return MappingObjMarshaler(mt, get_obj_marshaler(k), get_obj_marshaler(v))
533
+
534
+ try:
535
+ st = _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES[ta.get_origin(ty)]
536
+ except KeyError:
537
+ pass
538
+ else:
539
+ [e] = ta.get_args(ty)
540
+ return IterableObjMarshaler(st, get_obj_marshaler(e))
541
+
542
+ if is_union_alias(ty):
543
+ return OptionalObjMarshaler(get_obj_marshaler(get_optional_alias_arg(ty)))
544
+
545
+ raise TypeError(ty)
546
+
547
+
548
+ def get_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
549
+ try:
550
+ return _OBJ_MARSHALERS[ty]
551
+ except KeyError:
552
+ pass
553
+
554
+ p = ProxyObjMarshaler()
555
+ _OBJ_MARSHALERS[ty] = p
556
+ try:
557
+ m = _make_obj_marshaler(ty)
558
+ except Exception:
559
+ del _OBJ_MARSHALERS[ty]
560
+ raise
561
+ else:
562
+ p.m = m
563
+ _OBJ_MARSHALERS[ty] = m
564
+ return m
565
+
566
+
567
+ def marshal_obj(o: ta.Any, ty: ta.Any = None) -> ta.Any:
568
+ return get_obj_marshaler(ty if ty is not None else type(o)).marshal(o)
569
+
570
+
571
+ def unmarshal_obj(o: ta.Any, ty: ta.Union[ta.Type[T], ta.Any]) -> T:
572
+ return get_obj_marshaler(ty).unmarshal(o)
573
+
574
+
575
+ ########################################
576
+ # ../../../../omlish/lite/runtime.py
577
+
578
+
579
+ @cached_nullary
580
+ def is_debugger_attached() -> bool:
581
+ return any(frame[1].endswith('pydevd.py') for frame in inspect.stack())
582
+
583
+
584
+ REQUIRED_PYTHON_VERSION = (3, 8)
585
+
586
+
587
+ def check_runtime_version() -> None:
588
+ if sys.version_info < REQUIRED_PYTHON_VERSION:
589
+ raise OSError(
590
+ f'Requires python {REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
591
+
592
+
593
+ ########################################
594
+ # ../../../../omlish/lite/subprocesses.py
595
+ # ruff: noqa: UP006 UP007
596
+
597
+
598
+ ##
599
+
600
+
601
+ _SUBPROCESS_SHELL_WRAP_EXECS = False
602
+
603
+
604
+ def subprocess_shell_wrap_exec(*args: str) -> ta.Tuple[str, ...]:
605
+ return ('sh', '-c', ' '.join(map(shlex.quote, args)))
606
+
607
+
608
+ def subprocess_maybe_shell_wrap_exec(*args: str) -> ta.Tuple[str, ...]:
609
+ if _SUBPROCESS_SHELL_WRAP_EXECS or is_debugger_attached():
610
+ return subprocess_shell_wrap_exec(*args)
611
+ else:
612
+ return args
613
+
614
+
615
+ def _prepare_subprocess_invocation(
616
+ *args: str,
617
+ env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
618
+ extra_env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
619
+ quiet: bool = False,
620
+ shell: bool = False,
621
+ **kwargs: ta.Any,
622
+ ) -> ta.Tuple[ta.Tuple[ta.Any, ...], ta.Dict[str, ta.Any]]:
623
+ log.debug(args)
624
+ if extra_env:
625
+ log.debug(extra_env)
626
+
627
+ if extra_env:
628
+ env = {**(env if env is not None else os.environ), **extra_env}
629
+
630
+ if quiet and 'stderr' not in kwargs:
631
+ if not log.isEnabledFor(logging.DEBUG):
632
+ kwargs['stderr'] = subprocess.DEVNULL
633
+
634
+ if not shell:
635
+ args = subprocess_maybe_shell_wrap_exec(*args)
636
+
637
+ return args, dict(
638
+ env=env,
639
+ shell=shell,
640
+ **kwargs,
641
+ )
642
+
643
+
644
+ def subprocess_check_call(*args: str, stdout=sys.stderr, **kwargs: ta.Any) -> None:
645
+ args, kwargs = _prepare_subprocess_invocation(*args, stdout=stdout, **kwargs)
646
+ return subprocess.check_call(args, **kwargs) # type: ignore
647
+
648
+
649
+ def subprocess_check_output(*args: str, **kwargs: ta.Any) -> bytes:
650
+ args, kwargs = _prepare_subprocess_invocation(*args, **kwargs)
651
+ return subprocess.check_output(args, **kwargs)
652
+
653
+
654
+ def subprocess_check_output_str(*args: str, **kwargs: ta.Any) -> str:
655
+ return subprocess_check_output(*args, **kwargs).decode().strip()
656
+
657
+
658
+ ##
659
+
660
+
661
+ DEFAULT_SUBPROCESS_TRY_EXCEPTIONS: ta.Tuple[ta.Type[Exception], ...] = (
662
+ FileNotFoundError,
663
+ subprocess.CalledProcessError,
664
+ )
665
+
666
+
667
+ def subprocess_try_call(
668
+ *args: str,
669
+ try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
670
+ **kwargs: ta.Any,
671
+ ) -> bool:
672
+ try:
673
+ subprocess_check_call(*args, **kwargs)
674
+ except try_exceptions as e: # noqa
675
+ if log.isEnabledFor(logging.DEBUG):
676
+ log.exception('command failed')
677
+ return False
678
+ else:
679
+ return True
680
+
681
+
682
+ def subprocess_try_output(
683
+ *args: str,
684
+ try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
685
+ **kwargs: ta.Any,
686
+ ) -> ta.Optional[bytes]:
687
+ try:
688
+ return subprocess_check_output(*args, **kwargs)
689
+ except try_exceptions as e: # noqa
690
+ if log.isEnabledFor(logging.DEBUG):
691
+ log.exception('command failed')
692
+ return None
693
+
694
+
695
+ def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
696
+ out = subprocess_try_output(*args, **kwargs)
697
+ return out.decode().strip() if out is not None else None
698
+
699
+
700
+ ########################################
701
+ # ../base.py
702
+ # ruff: noqa: UP006
703
+
704
+
705
+ ##
706
+
707
+
708
+ class Phase(enum.Enum):
709
+ HOST = enum.auto()
710
+ ENV = enum.auto()
711
+ BACKEND = enum.auto()
712
+ FRONTEND = enum.auto()
713
+ START_BACKEND = enum.auto()
714
+ START_FRONTEND = enum.auto()
715
+
716
+
717
+ def run_in_phase(*ps: Phase):
718
+ def inner(fn):
719
+ fn.__deployment_phases__ = ps
720
+ return fn
721
+ return inner
722
+
723
+
724
+ class Concern(abc.ABC):
725
+ def __init__(self, d: 'Deployment') -> None:
726
+ super().__init__()
727
+ self._d = d
728
+
729
+ _phase_fns: ta.ClassVar[ta.Mapping[Phase, ta.Sequence[ta.Callable]]]
730
+
731
+ def __init_subclass__(cls, **kwargs):
732
+ super().__init_subclass__(**kwargs)
733
+ dct: ta.Dict[Phase, ta.List[ta.Callable]] = {}
734
+ for fn, ps in [
735
+ (v, ps)
736
+ for a in dir(cls)
737
+ if not (a.startswith('__') and a.endswith('__'))
738
+ for v in [getattr(cls, a, None)]
739
+ for ps in [getattr(v, '__deployment_phases__', None)]
740
+ if ps
741
+ ]:
742
+ dct.update({p: [*dct.get(p, []), fn] for p in ps})
743
+ cls._phase_fns = dct
744
+
745
+ @dc.dataclass(frozen=True)
746
+ class Output(abc.ABC):
747
+ path: str
748
+ is_file: bool
749
+
750
+ def outputs(self) -> ta.Sequence[Output]:
751
+ return ()
752
+
753
+ def run_phase(self, p: Phase) -> None:
754
+ for fn in self._phase_fns.get(p, ()):
755
+ fn.__get__(self, type(self))()
756
+
757
+
758
+ ##
759
+
760
+
761
+ class Deployment:
762
+
763
+ def __init__(
764
+ self,
765
+ cfg: DeployConfig,
766
+ concern_cls_list: ta.List[ta.Type[Concern]],
767
+ host_cfg: HostConfig = HostConfig(),
768
+ ) -> None:
769
+ super().__init__()
770
+ self._cfg = cfg
771
+ self._host_cfg = host_cfg
772
+
773
+ self._concerns: ta.List[Concern] = [cls(self) for cls in concern_cls_list]
774
+
775
+ @property
776
+ def cfg(self) -> DeployConfig:
777
+ return self._cfg
778
+
779
+ @property
780
+ def host_cfg(self) -> HostConfig:
781
+ return self._host_cfg
782
+
783
+ def sh(self, *ss: str) -> None:
784
+ s = ' && '.join(ss)
785
+ log.info('Executing: %s', s)
786
+ subprocess_check_call(s, shell=True)
787
+
788
+ def ush(self, *ss: str) -> None:
789
+ s = ' && '.join(ss)
790
+ self.sh(f'su - {self._host_cfg.username} -c {shlex.quote(s)}')
791
+
792
+ @cached_nullary
793
+ def home_dir(self) -> str:
794
+ return os.path.expanduser(f'~{self._host_cfg.username}')
795
+
796
+ @cached_nullary
797
+ def deploy(self) -> None:
798
+ for p in Phase:
799
+ log.info('Phase %s', p.name)
800
+ for c in self._concerns:
801
+ c.run_phase(p)
802
+
803
+ log.info('Shitty deploy complete!')
804
+
805
+
806
+ ########################################
807
+ # ../concerns/dirs.py
808
+
809
+
810
+ class DirsConcern(Concern):
811
+ @run_in_phase(Phase.HOST)
812
+ def create_dirs(self) -> None:
813
+ pwn = pwd.getpwnam(self._d.host_cfg.username)
814
+
815
+ for dn in [
816
+ 'app',
817
+ 'conf',
818
+ 'conf/env',
819
+ 'conf/nginx',
820
+ 'conf/supervisor',
821
+ 'venv',
822
+ ]:
823
+ fp = os.path.join(self._d.home_dir(), dn)
824
+ if not os.path.exists(fp):
825
+ log.info('Creating directory: %s', fp)
826
+ os.mkdir(fp)
827
+ os.chown(fp, pwn.pw_uid, pwn.pw_gid)
828
+
829
+
830
+ ########################################
831
+ # ../concerns/nginx.py
832
+ """
833
+ TODO:
834
+ - https://stackoverflow.com/questions/3011067/restart-nginx-without-sudo
835
+ """
836
+
837
+
838
+ class GlobalNginxConcern(Concern):
839
+ @run_in_phase(Phase.HOST)
840
+ def create_global_nginx_conf(self) -> None:
841
+ nginx_conf_dir = os.path.join(self._d.home_dir(), 'conf/nginx')
842
+ if not os.path.isfile(self._d.host_cfg.global_nginx_conf_file_path):
843
+ log.info('Writing global nginx conf at %s', self._d.host_cfg.global_nginx_conf_file_path)
844
+ with open(self._d.host_cfg.global_nginx_conf_file_path, 'w') as f:
845
+ f.write(f'include {nginx_conf_dir}/*.conf;\n')
846
+
847
+
848
+ class NginxConcern(Concern):
849
+ @run_in_phase(Phase.FRONTEND)
850
+ def create_nginx_conf(self) -> None:
851
+ nginx_conf = textwrap.dedent(f"""
852
+ server {{
853
+ listen 80;
854
+ location / {{
855
+ proxy_pass http://127.0.0.1:8000/;
856
+ }}
857
+ }}
858
+ """)
859
+ nginx_conf_file = os.path.join(self._d.home_dir(), f'conf/nginx/{self._d.cfg.app_name}.conf')
860
+ log.info('Writing nginx conf to %s', nginx_conf_file)
861
+ with open(nginx_conf_file, 'w') as f:
862
+ f.write(nginx_conf)
863
+
864
+ @run_in_phase(Phase.START_FRONTEND)
865
+ def poke_nginx(self) -> None:
866
+ log.info('Starting nginx')
867
+ self._d.sh('service nginx start')
868
+
869
+ log.info('Poking nginx')
870
+ self._d.sh('nginx -s reload')
871
+
872
+
873
+ ########################################
874
+ # ../concerns/repo.py
875
+
876
+
877
+ class RepoConcern(Concern):
878
+ @run_in_phase(Phase.ENV)
879
+ def clone_repo(self) -> None:
880
+ clone_submodules = False
881
+ self._d.ush(
882
+ 'cd ~/app',
883
+ f'git clone --depth 1 {self._d.cfg.repo_url} {self._d.cfg.app_name}',
884
+ *([
885
+ f'cd {self._d.cfg.app_name}',
886
+ 'git submodule update --init',
887
+ ] if clone_submodules else []),
888
+ )
889
+
890
+
891
+ ########################################
892
+ # ../concerns/supervisor.py
893
+
894
+
895
+ class GlobalSupervisorConcern(Concern):
896
+ @run_in_phase(Phase.HOST)
897
+ def create_global_supervisor_conf(self) -> None:
898
+ sup_conf_dir = os.path.join(self._d.home_dir(), 'conf/supervisor')
899
+ with open(self._d.host_cfg.global_supervisor_conf_file_path) as f:
900
+ glo_sup_conf = f.read()
901
+ if sup_conf_dir not in glo_sup_conf:
902
+ log.info('Updating global supervisor conf at %s', self._d.host_cfg.global_supervisor_conf_file_path) # noqa
903
+ glo_sup_conf += textwrap.dedent(f"""
904
+ [include]
905
+ files = {self._d.home_dir()}/conf/supervisor/*.conf
906
+ """)
907
+ with open(self._d.host_cfg.global_supervisor_conf_file_path, 'w') as f:
908
+ f.write(glo_sup_conf)
909
+
910
+
911
+ class SupervisorConcern(Concern):
912
+ @run_in_phase(Phase.BACKEND)
913
+ def create_supervisor_conf(self) -> None:
914
+ sup_conf = textwrap.dedent(f"""
915
+ [program:{self._d.cfg.app_name}]
916
+ command={self._d.home_dir()}/venv/{self._d.cfg.app_name}/bin/python -m {self._d.cfg.entrypoint}
917
+ directory={self._d.home_dir()}/app/{self._d.cfg.app_name}
918
+ user={self._d.host_cfg.username}
919
+ autostart=true
920
+ autorestart=true
921
+ """)
922
+ sup_conf_file = os.path.join(self._d.home_dir(), f'conf/supervisor/{self._d.cfg.app_name}.conf')
923
+ log.info('Writing supervisor conf to %s', sup_conf_file)
924
+ with open(sup_conf_file, 'w') as f:
925
+ f.write(sup_conf)
926
+
927
+ @run_in_phase(Phase.START_BACKEND)
928
+ def poke_supervisor(self) -> None:
929
+ log.info('Poking supervisor')
930
+ self._d.sh('kill -HUP 1')
931
+
932
+
933
+ ########################################
934
+ # ../concerns/user.py
935
+
936
+
937
+ class UserConcern(Concern):
938
+ @run_in_phase(Phase.HOST)
939
+ def create_user(self) -> None:
940
+ try:
941
+ pwd.getpwnam(self._d.host_cfg.username)
942
+ except KeyError:
943
+ log.info('Creating user %s', self._d.host_cfg.username)
944
+ self._d.sh(' '.join([
945
+ 'adduser',
946
+ '--system',
947
+ '--disabled-password',
948
+ '--group',
949
+ '--shell /bin/bash',
950
+ self._d.host_cfg.username,
951
+ ]))
952
+ pwd.getpwnam(self._d.host_cfg.username)
953
+
954
+
955
+ ########################################
956
+ # ../concerns/venv.py
957
+ """
958
+ TODO:
959
+ - use LinuxInterpResolver lol
960
+ """
961
+
962
+
963
+ class VenvConcern(Concern):
964
+ @run_in_phase(Phase.ENV)
965
+ def setup_venv(self) -> None:
966
+ self._d.ush(
967
+ 'cd ~/venv',
968
+ f'{self._d.cfg.python_bin} -mvenv {self._d.cfg.app_name}',
969
+
970
+ # https://stackoverflow.com/questions/77364550/attributeerror-module-pkgutil-has-no-attribute-impimporter-did-you-mean
971
+ f'{self._d.cfg.app_name}/bin/python -m ensurepip',
972
+ f'{self._d.cfg.app_name}/bin/python -mpip install --upgrade setuptools pip',
973
+
974
+ f'{self._d.cfg.app_name}/bin/python -mpip install -r ~deploy/app/{self._d.cfg.app_name}/{self._d.cfg.requirements_txt}', # noqa
975
+ )
976
+
977
+
978
+ ########################################
979
+ # main.py
980
+
981
+
982
+ ##
983
+
984
+
985
+ def _deploy_cmd(args) -> None:
986
+ dct = json.loads(args.cfg)
987
+ cfg: DeployConfig = unmarshal_obj(dct, DeployConfig)
988
+ dp = Deployment(
989
+ cfg,
990
+ [
991
+ UserConcern,
992
+ DirsConcern,
993
+ GlobalNginxConcern,
994
+ GlobalSupervisorConcern,
995
+ RepoConcern,
996
+ VenvConcern,
997
+ SupervisorConcern,
998
+ NginxConcern,
999
+ ],
1000
+ )
1001
+ dp.deploy()
1002
+
1003
+
1004
+ ##
1005
+
1006
+
1007
+ def _build_parser() -> argparse.ArgumentParser:
1008
+ parser = argparse.ArgumentParser()
1009
+
1010
+ subparsers = parser.add_subparsers()
1011
+
1012
+ parser_resolve = subparsers.add_parser('deploy')
1013
+ parser_resolve.add_argument('cfg')
1014
+ parser_resolve.set_defaults(func=_deploy_cmd)
1015
+
1016
+ return parser
1017
+
1018
+
1019
+ def _main(argv: ta.Optional[ta.Sequence[str]] = None) -> None:
1020
+ check_runtime_version()
1021
+
1022
+ if getattr(sys, 'platform') != 'linux': # noqa
1023
+ raise OSError('must run on linux')
1024
+
1025
+ configure_standard_logging()
1026
+
1027
+ parser = _build_parser()
1028
+ args = parser.parse_args(argv)
1029
+ if not getattr(args, 'func', None):
1030
+ parser.print_help()
1031
+ else:
1032
+ args.func(args)
1033
+
1034
+
1035
+ if __name__ == '__main__':
1036
+ _main()