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,19 @@
1
+ import dataclasses as dc
2
+
3
+
4
+ @dc.dataclass(frozen=True)
5
+ class DeployConfig:
6
+ python_bin: str
7
+ app_name: str
8
+ repo_url: str
9
+ revision: str
10
+ requirements_txt: str
11
+ entrypoint: str
12
+
13
+
14
+ @dc.dataclass(frozen=True)
15
+ class HostConfig:
16
+ username: str = 'deploy'
17
+
18
+ global_supervisor_conf_file_path: str = '/etc/supervisor/conf.d/supervisord.conf'
19
+ global_nginx_conf_file_path: str = '/etc/nginx/sites-enabled/deploy.conf'
@@ -0,0 +1 @@
1
+ # @omlish-lite
@@ -0,0 +1,115 @@
1
+ # ruff: noqa: UP006
2
+ import abc
3
+ import dataclasses as dc
4
+ import enum
5
+ import os.path
6
+ import shlex
7
+ import typing as ta
8
+
9
+ from omlish.lite.cached import cached_nullary
10
+ from omlish.lite.logs import log
11
+ from omlish.lite.subprocesses import subprocess_check_call
12
+
13
+ from ..configs import DeployConfig
14
+ from ..configs import HostConfig
15
+
16
+
17
+ ##
18
+
19
+
20
+ class Phase(enum.Enum):
21
+ HOST = enum.auto()
22
+ ENV = enum.auto()
23
+ BACKEND = enum.auto()
24
+ FRONTEND = enum.auto()
25
+ START_BACKEND = enum.auto()
26
+ START_FRONTEND = enum.auto()
27
+
28
+
29
+ def run_in_phase(*ps: Phase):
30
+ def inner(fn):
31
+ fn.__deployment_phases__ = ps
32
+ return fn
33
+ return inner
34
+
35
+
36
+ class Concern(abc.ABC):
37
+ def __init__(self, d: 'Deployment') -> None:
38
+ super().__init__()
39
+ self._d = d
40
+
41
+ _phase_fns: ta.ClassVar[ta.Mapping[Phase, ta.Sequence[ta.Callable]]]
42
+
43
+ def __init_subclass__(cls, **kwargs):
44
+ super().__init_subclass__(**kwargs)
45
+ dct: ta.Dict[Phase, ta.List[ta.Callable]] = {}
46
+ for fn, ps in [
47
+ (v, ps)
48
+ for a in dir(cls)
49
+ if not (a.startswith('__') and a.endswith('__'))
50
+ for v in [getattr(cls, a, None)]
51
+ for ps in [getattr(v, '__deployment_phases__', None)]
52
+ if ps
53
+ ]:
54
+ dct.update({p: [*dct.get(p, []), fn] for p in ps})
55
+ cls._phase_fns = dct
56
+
57
+ @dc.dataclass(frozen=True)
58
+ class Output(abc.ABC):
59
+ path: str
60
+ is_file: bool
61
+
62
+ def outputs(self) -> ta.Sequence[Output]:
63
+ return ()
64
+
65
+ def run_phase(self, p: Phase) -> None:
66
+ for fn in self._phase_fns.get(p, ()):
67
+ fn.__get__(self, type(self))()
68
+
69
+
70
+ ##
71
+
72
+
73
+ class Deployment:
74
+
75
+ def __init__(
76
+ self,
77
+ cfg: DeployConfig,
78
+ concern_cls_list: ta.List[ta.Type[Concern]],
79
+ host_cfg: HostConfig = HostConfig(),
80
+ ) -> None:
81
+ super().__init__()
82
+ self._cfg = cfg
83
+ self._host_cfg = host_cfg
84
+
85
+ self._concerns: ta.List[Concern] = [cls(self) for cls in concern_cls_list]
86
+
87
+ @property
88
+ def cfg(self) -> DeployConfig:
89
+ return self._cfg
90
+
91
+ @property
92
+ def host_cfg(self) -> HostConfig:
93
+ return self._host_cfg
94
+
95
+ def sh(self, *ss: str) -> None:
96
+ s = ' && '.join(ss)
97
+ log.info('Executing: %s', s)
98
+ subprocess_check_call(s, shell=True)
99
+
100
+ def ush(self, *ss: str) -> None:
101
+ s = ' && '.join(ss)
102
+ self.sh(f'su - {self._host_cfg.username} -c {shlex.quote(s)}')
103
+
104
+ @cached_nullary
105
+ def home_dir(self) -> str:
106
+ return os.path.expanduser(f'~{self._host_cfg.username}')
107
+
108
+ @cached_nullary
109
+ def deploy(self) -> None:
110
+ for p in Phase:
111
+ log.info('Phase %s', p.name)
112
+ for c in self._concerns:
113
+ c.run_phase(p)
114
+
115
+ log.info('Shitty deploy complete!')
File without changes
@@ -0,0 +1,28 @@
1
+ import os.path
2
+ import pwd
3
+
4
+ from omlish.lite.logs import log
5
+
6
+ from ..base import Concern
7
+ from ..base import Phase
8
+ from ..base import run_in_phase
9
+
10
+
11
+ class DirsConcern(Concern):
12
+ @run_in_phase(Phase.HOST)
13
+ def create_dirs(self) -> None:
14
+ pwn = pwd.getpwnam(self._d.host_cfg.username)
15
+
16
+ for dn in [
17
+ 'app',
18
+ 'conf',
19
+ 'conf/env',
20
+ 'conf/nginx',
21
+ 'conf/supervisor',
22
+ 'venv',
23
+ ]:
24
+ fp = os.path.join(self._d.home_dir(), dn)
25
+ if not os.path.exists(fp):
26
+ log.info('Creating directory: %s', fp)
27
+ os.mkdir(fp)
28
+ os.chown(fp, pwn.pw_uid, pwn.pw_gid)
@@ -0,0 +1,47 @@
1
+ """
2
+ TODO:
3
+ - https://stackoverflow.com/questions/3011067/restart-nginx-without-sudo
4
+ """
5
+ import os.path
6
+ import textwrap
7
+
8
+ from omlish.lite.logs import log
9
+
10
+ from ..base import Concern
11
+ from ..base import Phase
12
+ from ..base import run_in_phase
13
+
14
+
15
+ class GlobalNginxConcern(Concern):
16
+ @run_in_phase(Phase.HOST)
17
+ def create_global_nginx_conf(self) -> None:
18
+ nginx_conf_dir = os.path.join(self._d.home_dir(), 'conf/nginx')
19
+ if not os.path.isfile(self._d.host_cfg.global_nginx_conf_file_path):
20
+ log.info('Writing global nginx conf at %s', self._d.host_cfg.global_nginx_conf_file_path)
21
+ with open(self._d.host_cfg.global_nginx_conf_file_path, 'w') as f:
22
+ f.write(f'include {nginx_conf_dir}/*.conf;\n')
23
+
24
+
25
+ class NginxConcern(Concern):
26
+ @run_in_phase(Phase.FRONTEND)
27
+ def create_nginx_conf(self) -> None:
28
+ nginx_conf = textwrap.dedent(f"""
29
+ server {{
30
+ listen 80;
31
+ location / {{
32
+ proxy_pass http://127.0.0.1:8000/;
33
+ }}
34
+ }}
35
+ """)
36
+ nginx_conf_file = os.path.join(self._d.home_dir(), f'conf/nginx/{self._d.cfg.app_name}.conf')
37
+ log.info('Writing nginx conf to %s', nginx_conf_file)
38
+ with open(nginx_conf_file, 'w') as f:
39
+ f.write(nginx_conf)
40
+
41
+ @run_in_phase(Phase.START_FRONTEND)
42
+ def poke_nginx(self) -> None:
43
+ log.info('Starting nginx')
44
+ self._d.sh('service nginx start')
45
+
46
+ log.info('Poking nginx')
47
+ self._d.sh('nginx -s reload')
@@ -0,0 +1,17 @@
1
+ from ..base import Concern
2
+ from ..base import Phase
3
+ from ..base import run_in_phase
4
+
5
+
6
+ class RepoConcern(Concern):
7
+ @run_in_phase(Phase.ENV)
8
+ def clone_repo(self) -> None:
9
+ clone_submodules = False
10
+ self._d.ush(
11
+ 'cd ~/app',
12
+ f'git clone --depth 1 {self._d.cfg.repo_url} {self._d.cfg.app_name}',
13
+ *([
14
+ f'cd {self._d.cfg.app_name}',
15
+ 'git submodule update --init',
16
+ ] if clone_submodules else []),
17
+ )
@@ -0,0 +1,46 @@
1
+ import os.path
2
+ import textwrap
3
+
4
+ from omlish.lite.logs import log
5
+
6
+ from ..base import Concern
7
+ from ..base import Phase
8
+ from ..base import run_in_phase
9
+
10
+
11
+ class GlobalSupervisorConcern(Concern):
12
+ @run_in_phase(Phase.HOST)
13
+ def create_global_supervisor_conf(self) -> None:
14
+ sup_conf_dir = os.path.join(self._d.home_dir(), 'conf/supervisor')
15
+ with open(self._d.host_cfg.global_supervisor_conf_file_path) as f:
16
+ glo_sup_conf = f.read()
17
+ if sup_conf_dir not in glo_sup_conf:
18
+ log.info('Updating global supervisor conf at %s', self._d.host_cfg.global_supervisor_conf_file_path) # noqa
19
+ glo_sup_conf += textwrap.dedent(f"""
20
+ [include]
21
+ files = {self._d.home_dir()}/conf/supervisor/*.conf
22
+ """)
23
+ with open(self._d.host_cfg.global_supervisor_conf_file_path, 'w') as f:
24
+ f.write(glo_sup_conf)
25
+
26
+
27
+ class SupervisorConcern(Concern):
28
+ @run_in_phase(Phase.BACKEND)
29
+ def create_supervisor_conf(self) -> None:
30
+ sup_conf = textwrap.dedent(f"""
31
+ [program:{self._d.cfg.app_name}]
32
+ command={self._d.home_dir()}/venv/{self._d.cfg.app_name}/bin/python -m {self._d.cfg.entrypoint}
33
+ directory={self._d.home_dir()}/app/{self._d.cfg.app_name}
34
+ user={self._d.host_cfg.username}
35
+ autostart=true
36
+ autorestart=true
37
+ """)
38
+ sup_conf_file = os.path.join(self._d.home_dir(), f'conf/supervisor/{self._d.cfg.app_name}.conf')
39
+ log.info('Writing supervisor conf to %s', sup_conf_file)
40
+ with open(sup_conf_file, 'w') as f:
41
+ f.write(sup_conf)
42
+
43
+ @run_in_phase(Phase.START_BACKEND)
44
+ def poke_supervisor(self) -> None:
45
+ log.info('Poking supervisor')
46
+ self._d.sh('kill -HUP 1')
@@ -0,0 +1,88 @@
1
+ """
2
+ # https://serverfault.com/questions/617823/how-to-set-systemd-service-dependencies
3
+ PIDFile=/run/nginx.pid
4
+ ExecStartPre=/usr/sbin/nginx -t
5
+ ExecStart=/usr/sbin/nginx
6
+ ExecReload=/bin/kill -s HUP $MAINPID
7
+ ExecStop=/bin/kill -s QUIT $MAINPID
8
+ PrivateTmp=true
9
+
10
+ # https://gist.github.com/clemensg/7dd024169efe8ce6e7fa4a0b3caa3780
11
+ Type=forking
12
+ PIDFile=/var/run/nginx.pid
13
+ ExecStartPre=/usr/sbin/nginx -t
14
+ ExecStart=/usr/sbin/nginx
15
+ ExecReload=/usr/bin/kill -s HUP $MAINPID
16
+ ExecStop=/usr/bin/kill -s QUIT $MAINPID
17
+ # Hardening
18
+ InaccessiblePaths=/etc/gnupg /etc/shadow /etc/ssh
19
+ ProtectSystem=full
20
+ ProtectKernelTunables=yes
21
+ ProtectControlGroups=yes
22
+ SystemCallFilter=~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io
23
+ MemoryDenyWriteExecute=yes
24
+ RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
25
+ RestrictRealtime=yes
26
+ """
27
+ import os.path
28
+ import textwrap
29
+
30
+ from omlish.lite.logs import log
31
+
32
+ from ..base import Concern
33
+ from ..base import Phase
34
+ from ..base import run_in_phase
35
+
36
+
37
+ class GlobalSystemdConcern(Concern):
38
+ @run_in_phase(Phase.HOST)
39
+ def enable_user_linger(self) -> None:
40
+ log.info('Enabling user linger')
41
+ self._d.sh(f'loginctl enable-linger {self._d.host_cfg.username}')
42
+
43
+
44
+ class SystemdConcern(Concern):
45
+ service_name: str
46
+
47
+ @run_in_phase(Phase.HOST)
48
+ def create_systemd_path(self) -> None:
49
+ sd_svc_dir = os.path.join(self._d.home_dir(), '.config/systemd/user')
50
+ if not os.path.exists(sd_svc_dir):
51
+ log.info('Creating directory: %s', sd_svc_dir)
52
+ os.makedirs(sd_svc_dir)
53
+
54
+ @run_in_phase(Phase.BACKEND)
55
+ def create_systemd_service(self) -> None:
56
+ sd_svc = textwrap.dedent(f"""
57
+ [Unit]
58
+ Description={self.service_name}
59
+ After= \
60
+ syslog.target \
61
+ network.target \
62
+ remote-fs.target \
63
+ nss-lookup.target \
64
+ network-online.target
65
+
66
+ [Service]
67
+ Type=simple
68
+ StandardOutput=journal
69
+ ExecStart={self._d.home_dir()}/venv/{self._d.cfg.app_name}/bin/python -m {self._d.cfg.entrypoint}
70
+ WorkingDirectory={self._d.home_dir()}/app/{self._d.cfg.app_name}
71
+
72
+ Restart=always
73
+ RestartSec=3
74
+
75
+ [Install]
76
+ WantedBy=multi-user.target
77
+ """)
78
+ sd_svc_file = os.path.join(self._d.home_dir(), f'.config/systemd/user/{self.service_name}.service')
79
+ log.info('Writing systemd service to %s', sd_svc_file)
80
+ with open(sd_svc_file, 'w') as f:
81
+ f.write(sd_svc)
82
+
83
+ @run_in_phase(Phase.START_BACKEND)
84
+ def poke_systemd(self) -> None:
85
+ log.info('Poking systemd')
86
+ self._d.sh('systemctl --user daemon-reload')
87
+ self._d.sh(f'systemctl --user enable {self.service_name}')
88
+ self._d.sh(f'systemctl --user restart {self.service_name}')
@@ -0,0 +1,25 @@
1
+ import pwd
2
+
3
+ from omlish.lite.logs import log
4
+
5
+ from ..base import Concern
6
+ from ..base import Phase
7
+ from ..base import run_in_phase
8
+
9
+
10
+ class UserConcern(Concern):
11
+ @run_in_phase(Phase.HOST)
12
+ def create_user(self) -> None:
13
+ try:
14
+ pwd.getpwnam(self._d.host_cfg.username)
15
+ except KeyError:
16
+ log.info('Creating user %s', self._d.host_cfg.username)
17
+ self._d.sh(' '.join([
18
+ 'adduser',
19
+ '--system',
20
+ '--disabled-password',
21
+ '--group',
22
+ '--shell /bin/bash',
23
+ self._d.host_cfg.username,
24
+ ]))
25
+ pwd.getpwnam(self._d.host_cfg.username)
@@ -0,0 +1,22 @@
1
+ """
2
+ TODO:
3
+ - use LinuxInterpResolver lol
4
+ """
5
+ from ..base import Concern
6
+ from ..base import Phase
7
+ from ..base import run_in_phase
8
+
9
+
10
+ class VenvConcern(Concern):
11
+ @run_in_phase(Phase.ENV)
12
+ def setup_venv(self) -> None:
13
+ self._d.ush(
14
+ 'cd ~/venv',
15
+ f'{self._d.cfg.python_bin} -mvenv {self._d.cfg.app_name}',
16
+
17
+ # https://stackoverflow.com/questions/77364550/attributeerror-module-pkgutil-has-no-attribute-impimporter-did-you-mean
18
+ f'{self._d.cfg.app_name}/bin/python -m ensurepip',
19
+ f'{self._d.cfg.app_name}/bin/python -mpip install --upgrade setuptools pip',
20
+
21
+ f'{self._d.cfg.app_name}/bin/python -mpip install -r ~deploy/app/{self._d.cfg.app_name}/{self._d.cfg.requirements_txt}', # noqa
22
+ )
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env python3
2
+ # @omdev-amalg ../_executor.py
3
+ r"""
4
+ TODO:
5
+ - flock
6
+ - interp.py
7
+ - systemd
8
+
9
+ deployment matrix
10
+ - os: ubuntu / amzn / generic
11
+ - arch: amd64 / arm64
12
+ - host: bare / docker
13
+ - init: supervisor-provided / supervisor-must-configure / systemd (/ self?)
14
+ - interp: system / pyenv / interp.py
15
+ - venv: none / yes
16
+ - nginx: no / provided / must-configure
17
+
18
+ ==
19
+
20
+ ~deploy
21
+ deploy.pid (flock)
22
+ /app
23
+ /<appspec> - shallow clone
24
+ /conf
25
+ /env
26
+ <appspec>.env
27
+ /nginx
28
+ <appspec>.conf
29
+ /supervisor
30
+ <appspec>.conf
31
+ /venv
32
+ /<appspec>
33
+
34
+ ?
35
+ /logs
36
+ /wrmsr--omlish--<spec>
37
+
38
+ spec = <name>--<rev>--<when>
39
+
40
+ https://docs.docker.com/config/containers/multi-service_container/#use-a-process-manager
41
+ https://serverfault.com/questions/211525/supervisor-not-loading-new-configuration-files
42
+ """ # noqa
43
+ # ruff: noqa: UP007
44
+ import argparse
45
+ import json
46
+ import sys
47
+ import typing as ta
48
+
49
+ from omlish.lite.logs import configure_standard_logging
50
+ from omlish.lite.marshal import unmarshal_obj
51
+ from omlish.lite.runtime import check_runtime_version
52
+
53
+ from ..configs import DeployConfig
54
+ from .base import Deployment
55
+ from .concerns.dirs import DirsConcern
56
+ from .concerns.nginx import GlobalNginxConcern
57
+ from .concerns.nginx import NginxConcern
58
+ from .concerns.repo import RepoConcern
59
+ from .concerns.supervisor import GlobalSupervisorConcern
60
+ from .concerns.supervisor import SupervisorConcern
61
+ from .concerns.user import UserConcern
62
+ from .concerns.venv import VenvConcern
63
+
64
+
65
+ ##
66
+
67
+
68
+ def _deploy_cmd(args) -> None:
69
+ dct = json.loads(args.cfg)
70
+ cfg: DeployConfig = unmarshal_obj(dct, DeployConfig)
71
+ dp = Deployment(
72
+ cfg,
73
+ [
74
+ UserConcern,
75
+ DirsConcern,
76
+ GlobalNginxConcern,
77
+ GlobalSupervisorConcern,
78
+ RepoConcern,
79
+ VenvConcern,
80
+ SupervisorConcern,
81
+ NginxConcern,
82
+ ],
83
+ )
84
+ dp.deploy()
85
+
86
+
87
+ ##
88
+
89
+
90
+ def _build_parser() -> argparse.ArgumentParser:
91
+ parser = argparse.ArgumentParser()
92
+
93
+ subparsers = parser.add_subparsers()
94
+
95
+ parser_resolve = subparsers.add_parser('deploy')
96
+ parser_resolve.add_argument('cfg')
97
+ parser_resolve.set_defaults(func=_deploy_cmd)
98
+
99
+ return parser
100
+
101
+
102
+ def _main(argv: ta.Optional[ta.Sequence[str]] = None) -> None:
103
+ check_runtime_version()
104
+
105
+ if getattr(sys, 'platform') != 'linux': # noqa
106
+ raise OSError('must run on linux')
107
+
108
+ configure_standard_logging()
109
+
110
+ parser = _build_parser()
111
+ args = parser.parse_args(argv)
112
+ if not getattr(args, 'func', None):
113
+ parser.print_help()
114
+ else:
115
+ args.func(args)
116
+
117
+
118
+ if __name__ == '__main__':
119
+ _main()
@@ -0,0 +1 @@
1
+ # @omlish-lite