pyeasydeploy 0.1.0__tar.gz
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.
- pyeasydeploy-0.1.0/PKG-INFO +162 -0
- pyeasydeploy-0.1.0/README.md +136 -0
- pyeasydeploy-0.1.0/pyeasydeploy/__init__.py +103 -0
- pyeasydeploy-0.1.0/pyeasydeploy/connection.py +153 -0
- pyeasydeploy-0.1.0/pyeasydeploy/models.py +241 -0
- pyeasydeploy-0.1.0/pyeasydeploy/packages.py +187 -0
- pyeasydeploy-0.1.0/pyeasydeploy/py.typed +0 -0
- pyeasydeploy-0.1.0/pyeasydeploy/python.py +141 -0
- pyeasydeploy-0.1.0/pyeasydeploy/supervisor.py +216 -0
- pyeasydeploy-0.1.0/pyeasydeploy/transfer.py +174 -0
- pyeasydeploy-0.1.0/pyeasydeploy/venv.py +122 -0
- pyeasydeploy-0.1.0/pyeasydeploy.egg-info/PKG-INFO +162 -0
- pyeasydeploy-0.1.0/pyeasydeploy.egg-info/SOURCES.txt +16 -0
- pyeasydeploy-0.1.0/pyeasydeploy.egg-info/dependency_links.txt +1 -0
- pyeasydeploy-0.1.0/pyeasydeploy.egg-info/requires.txt +1 -0
- pyeasydeploy-0.1.0/pyeasydeploy.egg-info/top_level.txt +1 -0
- pyeasydeploy-0.1.0/pyproject.toml +49 -0
- pyeasydeploy-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyeasydeploy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple and replicable Python server deployment toolkit
|
|
5
|
+
Author: Beltrán Offerrall
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/offerrall/pyeasydeploy
|
|
8
|
+
Project-URL: Repository, https://github.com/offerrall/pyeasydeploy
|
|
9
|
+
Project-URL: Issues, https://github.com/offerrall/pyeasydeploy/issues
|
|
10
|
+
Keywords: deployment,devops,automation,ssh,fabric,supervisor
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: fabric>=3.0.0
|
|
26
|
+
|
|
27
|
+
# pyeasydeploy 0.1.0
|
|
28
|
+
|
|
29
|
+
A small library for deploying Python applications to Linux servers over SSH. Plain Python functions on top of [Fabric](https://www.fabfile.org/): no agents on the server, no YAML, no DSL to learn. Your deploy script reads top to bottom.
|
|
30
|
+
|
|
31
|
+
It doesn't try to compete with Ansible or Docker. If you have a few servers, you write Python, and you want your deploy to be just another `deploy.py` in your project, it might be for you.
|
|
32
|
+
|
|
33
|
+
## A complete deploy
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from pyeasydeploy import (
|
|
37
|
+
SupervisorService, connect_to_host, create_venv,
|
|
38
|
+
deploy_supervisor_service, get_target_python_instance,
|
|
39
|
+
install_local_package,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
APP = "myapp"
|
|
43
|
+
USER = "deploy"
|
|
44
|
+
|
|
45
|
+
conn = connect_to_host(
|
|
46
|
+
host="203.0.113.10",
|
|
47
|
+
user=USER,
|
|
48
|
+
key_filename="~/.ssh/id_ed25519",
|
|
49
|
+
sudo_password="...", # better: os.environ["SUDO_PASSWORD"]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
py = get_target_python_instance(conn, "3.11")
|
|
53
|
+
venv = create_venv(conn, py, f"/home/{USER}/venvs/{APP}")
|
|
54
|
+
install_local_package(conn, venv, f"./{APP}")
|
|
55
|
+
|
|
56
|
+
deploy_supervisor_service(conn, SupervisorService(
|
|
57
|
+
name=APP,
|
|
58
|
+
command=f"{venv.venv_path}/bin/python -m {APP}",
|
|
59
|
+
directory=f"/home/{USER}",
|
|
60
|
+
user=USER,
|
|
61
|
+
))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Connect, pick an interpreter, create the venv, install your package with its dependencies, and leave it running as a supervised service that survives reboots. The `venv` object returned by `create_venv` carries its own path: the service command is built from it, no paths repeated by hand.
|
|
65
|
+
|
|
66
|
+
## The ideas behind it
|
|
67
|
+
|
|
68
|
+
**Destructive and reproducible.** Uploads remove the destination and copy from scratch, every time. After each deploy, the server has exactly what you have locally — no leftovers from previous versions. This is not configurable; it's the contract. (The one safety net: paths like `/`, `/home` or `/etc` are rejected as destinations.)
|
|
69
|
+
|
|
70
|
+
**Fail early, fail clearly.** Models validate on construction: a relative path or a service name that would corrupt the INI file blows up on your laptop with a useful message, before touching the server. Functions that need sudo check for it upfront — an immediate error with instructions, instead of the classic hang waiting for a password that will never come.
|
|
71
|
+
|
|
72
|
+
**Trust the user.** The library validates *form* (types, absolute paths, dangerous characters), not your *facts*: if you hand-build a `PythonInstance` pointing at an exotic interpreter, it's accepted. You know what's on your server.
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install pyeasydeploy
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Python ≥ 3.10 on your machine. On the server: SSH and some `python3` (tested on Debian/Ubuntu).
|
|
81
|
+
|
|
82
|
+
## Quick guide
|
|
83
|
+
|
|
84
|
+
### Connecting
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
conn = connect_to_host(host, user, password="...") # password (reused for sudo)
|
|
88
|
+
conn = connect_to_host(host, user, key_filename="~/.ssh/id_ed25519") # SSH key
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
With key auth and sudo operations, add `sudo_password=`. The connection is lazy: a wrong password shows up on the first command, not at connect time.
|
|
92
|
+
|
|
93
|
+
### Remote Python
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
py = get_any_python_instance(conn) # newest on the server
|
|
97
|
+
py = get_target_python_instance(conn, "3.11") # a specific one
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Only real interpreters are matched (`python3.X-config` and friends are filtered out), and version matching is component-wise: `"3.1"` means 3.1, not 3.11. For non-standard locations, build the model yourself:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
py = PythonInstance(version="3.12", executable="/opt/py312/bin/python3.12")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Venvs and packages
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
venv = create_venv(conn, py, "/home/deploy/venvs/myapp") # idempotent
|
|
110
|
+
|
|
111
|
+
install_packages(conn, venv, ["fastapi", "uvicorn[standard]"])
|
|
112
|
+
install_local_package(conn, venv, "./myapp")
|
|
113
|
+
install_package_from_private_github(conn, venv, "git@github.com:org/private.git")
|
|
114
|
+
|
|
115
|
+
run_in_venv(conn, venv, "python -m myapp --check")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Installs use `uv` inside the venv (fast; `use_uv=False` for classic pip). Private repos are cloned **on your machine** with your own credentials, then the source is uploaded: the server never needs access to your GitHub.
|
|
119
|
+
|
|
120
|
+
### Files
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
upload_directory(conn, "./data", "/home/deploy/data")
|
|
124
|
+
upload_file(conn, "config.toml", "/home/deploy/myapp/config.toml")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
⚠️ Destructive: the destination is removed before copying. `.git`, `__pycache__`, venvs and similar are excluded by default (`DEFAULT_IGNORE`); pass `ignore=[]` to upload everything.
|
|
128
|
+
|
|
129
|
+
### Services
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
install_supervisor(conn) # once per server
|
|
133
|
+
|
|
134
|
+
deploy_supervisor_service(conn, SupervisorService(
|
|
135
|
+
name="myapp",
|
|
136
|
+
command=f"{venv.venv_path}/bin/python -m myapp",
|
|
137
|
+
extra={
|
|
138
|
+
"stdout_logfile_maxbytes": "10MB", # any supervisord option,
|
|
139
|
+
"stdout_logfile_backups": 5, # passed through verbatim
|
|
140
|
+
"stopsignal": "INT",
|
|
141
|
+
},
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
supervisor_status(conn)
|
|
145
|
+
supervisor_restart(conn, "myapp")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Named fields cover the common cases; the `extra` dict accepts any supervisord option with no restrictions — the library only blocks what would corrupt the generated file.
|
|
149
|
+
|
|
150
|
+
## What it is not
|
|
151
|
+
|
|
152
|
+
- **Not Ansible/Terraform.** No inventories, no state, no declarative idempotency. Imperative on purpose.
|
|
153
|
+
- **Not provisioning.** It installs supervisor because services are its job, and that's where it stops: nginx, databases and the rest of your server are up to you.
|
|
154
|
+
- **No secret management.** The passwords you pass in are your environment's responsibility.
|
|
155
|
+
- **No fleet orchestration.** One connection, one server. For several, write a loop.
|
|
156
|
+
- **Linux targets only.** The source machine can be Windows, macOS or Linux.
|
|
157
|
+
|
|
158
|
+
For many of those cases, bigger tools will do it better. This one exists for when you don't need them.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# pyeasydeploy 0.1.0
|
|
2
|
+
|
|
3
|
+
A small library for deploying Python applications to Linux servers over SSH. Plain Python functions on top of [Fabric](https://www.fabfile.org/): no agents on the server, no YAML, no DSL to learn. Your deploy script reads top to bottom.
|
|
4
|
+
|
|
5
|
+
It doesn't try to compete with Ansible or Docker. If you have a few servers, you write Python, and you want your deploy to be just another `deploy.py` in your project, it might be for you.
|
|
6
|
+
|
|
7
|
+
## A complete deploy
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from pyeasydeploy import (
|
|
11
|
+
SupervisorService, connect_to_host, create_venv,
|
|
12
|
+
deploy_supervisor_service, get_target_python_instance,
|
|
13
|
+
install_local_package,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
APP = "myapp"
|
|
17
|
+
USER = "deploy"
|
|
18
|
+
|
|
19
|
+
conn = connect_to_host(
|
|
20
|
+
host="203.0.113.10",
|
|
21
|
+
user=USER,
|
|
22
|
+
key_filename="~/.ssh/id_ed25519",
|
|
23
|
+
sudo_password="...", # better: os.environ["SUDO_PASSWORD"]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
py = get_target_python_instance(conn, "3.11")
|
|
27
|
+
venv = create_venv(conn, py, f"/home/{USER}/venvs/{APP}")
|
|
28
|
+
install_local_package(conn, venv, f"./{APP}")
|
|
29
|
+
|
|
30
|
+
deploy_supervisor_service(conn, SupervisorService(
|
|
31
|
+
name=APP,
|
|
32
|
+
command=f"{venv.venv_path}/bin/python -m {APP}",
|
|
33
|
+
directory=f"/home/{USER}",
|
|
34
|
+
user=USER,
|
|
35
|
+
))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Connect, pick an interpreter, create the venv, install your package with its dependencies, and leave it running as a supervised service that survives reboots. The `venv` object returned by `create_venv` carries its own path: the service command is built from it, no paths repeated by hand.
|
|
39
|
+
|
|
40
|
+
## The ideas behind it
|
|
41
|
+
|
|
42
|
+
**Destructive and reproducible.** Uploads remove the destination and copy from scratch, every time. After each deploy, the server has exactly what you have locally — no leftovers from previous versions. This is not configurable; it's the contract. (The one safety net: paths like `/`, `/home` or `/etc` are rejected as destinations.)
|
|
43
|
+
|
|
44
|
+
**Fail early, fail clearly.** Models validate on construction: a relative path or a service name that would corrupt the INI file blows up on your laptop with a useful message, before touching the server. Functions that need sudo check for it upfront — an immediate error with instructions, instead of the classic hang waiting for a password that will never come.
|
|
45
|
+
|
|
46
|
+
**Trust the user.** The library validates *form* (types, absolute paths, dangerous characters), not your *facts*: if you hand-build a `PythonInstance` pointing at an exotic interpreter, it's accepted. You know what's on your server.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install pyeasydeploy
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Python ≥ 3.10 on your machine. On the server: SSH and some `python3` (tested on Debian/Ubuntu).
|
|
55
|
+
|
|
56
|
+
## Quick guide
|
|
57
|
+
|
|
58
|
+
### Connecting
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
conn = connect_to_host(host, user, password="...") # password (reused for sudo)
|
|
62
|
+
conn = connect_to_host(host, user, key_filename="~/.ssh/id_ed25519") # SSH key
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
With key auth and sudo operations, add `sudo_password=`. The connection is lazy: a wrong password shows up on the first command, not at connect time.
|
|
66
|
+
|
|
67
|
+
### Remote Python
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
py = get_any_python_instance(conn) # newest on the server
|
|
71
|
+
py = get_target_python_instance(conn, "3.11") # a specific one
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Only real interpreters are matched (`python3.X-config` and friends are filtered out), and version matching is component-wise: `"3.1"` means 3.1, not 3.11. For non-standard locations, build the model yourself:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
py = PythonInstance(version="3.12", executable="/opt/py312/bin/python3.12")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Venvs and packages
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
venv = create_venv(conn, py, "/home/deploy/venvs/myapp") # idempotent
|
|
84
|
+
|
|
85
|
+
install_packages(conn, venv, ["fastapi", "uvicorn[standard]"])
|
|
86
|
+
install_local_package(conn, venv, "./myapp")
|
|
87
|
+
install_package_from_private_github(conn, venv, "git@github.com:org/private.git")
|
|
88
|
+
|
|
89
|
+
run_in_venv(conn, venv, "python -m myapp --check")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Installs use `uv` inside the venv (fast; `use_uv=False` for classic pip). Private repos are cloned **on your machine** with your own credentials, then the source is uploaded: the server never needs access to your GitHub.
|
|
93
|
+
|
|
94
|
+
### Files
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
upload_directory(conn, "./data", "/home/deploy/data")
|
|
98
|
+
upload_file(conn, "config.toml", "/home/deploy/myapp/config.toml")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
⚠️ Destructive: the destination is removed before copying. `.git`, `__pycache__`, venvs and similar are excluded by default (`DEFAULT_IGNORE`); pass `ignore=[]` to upload everything.
|
|
102
|
+
|
|
103
|
+
### Services
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
install_supervisor(conn) # once per server
|
|
107
|
+
|
|
108
|
+
deploy_supervisor_service(conn, SupervisorService(
|
|
109
|
+
name="myapp",
|
|
110
|
+
command=f"{venv.venv_path}/bin/python -m myapp",
|
|
111
|
+
extra={
|
|
112
|
+
"stdout_logfile_maxbytes": "10MB", # any supervisord option,
|
|
113
|
+
"stdout_logfile_backups": 5, # passed through verbatim
|
|
114
|
+
"stopsignal": "INT",
|
|
115
|
+
},
|
|
116
|
+
))
|
|
117
|
+
|
|
118
|
+
supervisor_status(conn)
|
|
119
|
+
supervisor_restart(conn, "myapp")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Named fields cover the common cases; the `extra` dict accepts any supervisord option with no restrictions — the library only blocks what would corrupt the generated file.
|
|
123
|
+
|
|
124
|
+
## What it is not
|
|
125
|
+
|
|
126
|
+
- **Not Ansible/Terraform.** No inventories, no state, no declarative idempotency. Imperative on purpose.
|
|
127
|
+
- **Not provisioning.** It installs supervisor because services are its job, and that's where it stops: nginx, databases and the rest of your server are up to you.
|
|
128
|
+
- **No secret management.** The passwords you pass in are your environment's responsibility.
|
|
129
|
+
- **No fleet orchestration.** One connection, one server. For several, write a loop.
|
|
130
|
+
- **Linux targets only.** The source machine can be Windows, macOS or Linux.
|
|
131
|
+
|
|
132
|
+
For many of those cases, bigger tools will do it better. This one exists for when you don't need them.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""pyeasydeploy — deploy Python apps to Linux servers over SSH.
|
|
2
|
+
|
|
3
|
+
No agents, no YAML, no magic: plain Python functions that do exactly
|
|
4
|
+
what they say. See the README for the philosophy (destructive and
|
|
5
|
+
reproducible uploads, fail-fast validation, trust in the user).
|
|
6
|
+
|
|
7
|
+
Typical flow::
|
|
8
|
+
|
|
9
|
+
from pyeasydeploy import (
|
|
10
|
+
connect_to_host, get_target_python_instance, create_venv,
|
|
11
|
+
install_local_package, deploy_supervisor_service,
|
|
12
|
+
SupervisorService,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
conn = connect_to_host(host, user, key_filename="~/.ssh/id_ed25519",
|
|
16
|
+
sudo_password="...")
|
|
17
|
+
py = get_target_python_instance(conn, "3.11")
|
|
18
|
+
venv = create_venv(conn, py, "/home/deploy/venvs/myapp")
|
|
19
|
+
install_local_package(conn, venv, "./myapp")
|
|
20
|
+
deploy_supervisor_service(conn, SupervisorService(
|
|
21
|
+
name="myapp",
|
|
22
|
+
command="/home/deploy/venvs/myapp/bin/python -m myapp",
|
|
23
|
+
))
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
# Models (foundation layer; importable standalone)
|
|
29
|
+
from .models import PythonInstance, SupervisorService, VenvPython
|
|
30
|
+
|
|
31
|
+
# Connection
|
|
32
|
+
from .connection import connect_to_host, has_sudo_password, require_sudo
|
|
33
|
+
|
|
34
|
+
# Remote Python discovery
|
|
35
|
+
from .python import (
|
|
36
|
+
get_any_python_instance,
|
|
37
|
+
get_python_instances,
|
|
38
|
+
get_target_python_instance,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Virtual environments
|
|
42
|
+
from .venv import create_venv, delete_venv, run_in_venv
|
|
43
|
+
|
|
44
|
+
# File transfer
|
|
45
|
+
from .transfer import DEFAULT_IGNORE, upload_directory, upload_file
|
|
46
|
+
|
|
47
|
+
# Package installation
|
|
48
|
+
from .packages import (
|
|
49
|
+
install_local_package,
|
|
50
|
+
install_package_from_github,
|
|
51
|
+
install_package_from_private_github,
|
|
52
|
+
install_packages,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Supervisor services
|
|
56
|
+
from .supervisor import (
|
|
57
|
+
check_supervisor_installed,
|
|
58
|
+
create_supervisor_config,
|
|
59
|
+
deploy_supervisor_service,
|
|
60
|
+
install_supervisor,
|
|
61
|
+
supervisor_restart,
|
|
62
|
+
supervisor_start,
|
|
63
|
+
supervisor_status,
|
|
64
|
+
supervisor_stop,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"__version__",
|
|
69
|
+
# models
|
|
70
|
+
"PythonInstance",
|
|
71
|
+
"VenvPython",
|
|
72
|
+
"SupervisorService",
|
|
73
|
+
# connection
|
|
74
|
+
"connect_to_host",
|
|
75
|
+
"has_sudo_password",
|
|
76
|
+
"require_sudo",
|
|
77
|
+
# python
|
|
78
|
+
"get_python_instances",
|
|
79
|
+
"get_target_python_instance",
|
|
80
|
+
"get_any_python_instance",
|
|
81
|
+
# venv
|
|
82
|
+
"create_venv",
|
|
83
|
+
"delete_venv",
|
|
84
|
+
"run_in_venv",
|
|
85
|
+
# transfer
|
|
86
|
+
"upload_file",
|
|
87
|
+
"upload_directory",
|
|
88
|
+
"DEFAULT_IGNORE",
|
|
89
|
+
# packages
|
|
90
|
+
"install_packages",
|
|
91
|
+
"install_local_package",
|
|
92
|
+
"install_package_from_github",
|
|
93
|
+
"install_package_from_private_github",
|
|
94
|
+
# supervisor
|
|
95
|
+
"install_supervisor",
|
|
96
|
+
"check_supervisor_installed",
|
|
97
|
+
"create_supervisor_config",
|
|
98
|
+
"deploy_supervisor_service",
|
|
99
|
+
"supervisor_start",
|
|
100
|
+
"supervisor_stop",
|
|
101
|
+
"supervisor_restart",
|
|
102
|
+
"supervisor_status",
|
|
103
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""SSH connection factory for pyeasydeploy.
|
|
2
|
+
|
|
3
|
+
Builds Fabric Connection objects with authentication and sudo correctly
|
|
4
|
+
wired. Depends only on models-level philosophy (validate at the
|
|
5
|
+
boundary); imports nothing from the rest of the package.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from fabric import Connection
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def connect_to_host(
|
|
14
|
+
host: str,
|
|
15
|
+
user: str,
|
|
16
|
+
password: Optional[str] = None,
|
|
17
|
+
key_filename: Optional[str] = None,
|
|
18
|
+
sudo_password: Optional[str] = None,
|
|
19
|
+
port: int = 22,
|
|
20
|
+
) -> Connection:
|
|
21
|
+
"""Create an SSH connection to a remote host.
|
|
22
|
+
|
|
23
|
+
Exactly one of ``password`` or ``key_filename`` must be provided
|
|
24
|
+
for SSH authentication.
|
|
25
|
+
|
|
26
|
+
Sudo is configured independently: if ``sudo_password`` is given it
|
|
27
|
+
is used for ``conn.sudo()`` calls; otherwise, if ``password`` is
|
|
28
|
+
given it is reused for sudo (the common case where the SSH user's
|
|
29
|
+
password is also the sudo password). With key-based auth and no
|
|
30
|
+
``sudo_password``, any later ``conn.sudo()`` call would block
|
|
31
|
+
forever waiting for a password prompt — so functions in this
|
|
32
|
+
library that need sudo will refuse early instead (see
|
|
33
|
+
``require_sudo``).
|
|
34
|
+
|
|
35
|
+
Note: the connection is lazy. Fabric does not open the SSH session
|
|
36
|
+
here; authentication errors surface on the first command run.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
host: Remote host address (IP or hostname).
|
|
40
|
+
user: Username for the SSH connection.
|
|
41
|
+
password: Password for SSH authentication. Mutually exclusive
|
|
42
|
+
with key_filename.
|
|
43
|
+
key_filename: Path to an SSH private key file. Mutually
|
|
44
|
+
exclusive with password.
|
|
45
|
+
sudo_password: Password for sudo on the remote host. Defaults
|
|
46
|
+
to ``password`` when that is provided. Required later by
|
|
47
|
+
any sudo-using function when authenticating with a key
|
|
48
|
+
(unless the remote user has passwordless sudo configured,
|
|
49
|
+
in which case sudo works without it).
|
|
50
|
+
port: SSH port. Defaults to 22.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A Fabric Connection, with sudo password configured when
|
|
54
|
+
available.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
TypeError: If any argument has the wrong type.
|
|
58
|
+
ValueError: If host or user are empty, both or neither of
|
|
59
|
+
password/key_filename are provided, or port is outside
|
|
60
|
+
1-65535.
|
|
61
|
+
"""
|
|
62
|
+
if not isinstance(host, str):
|
|
63
|
+
raise TypeError(f"host must be str, got {type(host).__name__}")
|
|
64
|
+
if not host.strip():
|
|
65
|
+
raise ValueError("host must be a non-empty string")
|
|
66
|
+
if not isinstance(user, str):
|
|
67
|
+
raise TypeError(f"user must be str, got {type(user).__name__}")
|
|
68
|
+
if not user.strip():
|
|
69
|
+
raise ValueError("user must be a non-empty string")
|
|
70
|
+
for name, value in (
|
|
71
|
+
("password", password),
|
|
72
|
+
("key_filename", key_filename),
|
|
73
|
+
("sudo_password", sudo_password),
|
|
74
|
+
):
|
|
75
|
+
if value is not None and not isinstance(value, str):
|
|
76
|
+
raise TypeError(f"{name} must be str or None, got {type(value).__name__}")
|
|
77
|
+
if isinstance(port, bool) or not isinstance(port, int):
|
|
78
|
+
raise TypeError(f"port must be int, got {type(port).__name__}")
|
|
79
|
+
if not 1 <= port <= 65535:
|
|
80
|
+
raise ValueError(f"port must be in 1-65535, got {port}")
|
|
81
|
+
|
|
82
|
+
if password is None and key_filename is None:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"You must provide either 'password' or 'key_filename' "
|
|
85
|
+
"for authentication"
|
|
86
|
+
)
|
|
87
|
+
if password is not None and key_filename is not None:
|
|
88
|
+
raise ValueError("Provide either 'password' or 'key_filename', not both")
|
|
89
|
+
|
|
90
|
+
connect_kwargs = {}
|
|
91
|
+
if password is not None:
|
|
92
|
+
connect_kwargs["password"] = password
|
|
93
|
+
else:
|
|
94
|
+
connect_kwargs["key_filename"] = key_filename
|
|
95
|
+
|
|
96
|
+
conn = Connection(
|
|
97
|
+
host=host,
|
|
98
|
+
user=user,
|
|
99
|
+
port=port,
|
|
100
|
+
connect_kwargs=connect_kwargs,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
effective_sudo = sudo_password if sudo_password is not None else password
|
|
104
|
+
if effective_sudo is not None:
|
|
105
|
+
conn.config.sudo.password = effective_sudo
|
|
106
|
+
|
|
107
|
+
return conn
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def has_sudo_password(conn: Connection) -> bool:
|
|
111
|
+
"""Return True if a sudo password is configured on this connection.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
conn: Connection created by connect_to_host (or any Fabric
|
|
115
|
+
Connection).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True when conn.config.sudo.password is set to a non-empty value.
|
|
119
|
+
"""
|
|
120
|
+
return bool(getattr(conn.config.sudo, "password", None))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def require_sudo(conn: Connection, what: str = "this operation") -> None:
|
|
124
|
+
"""Fail fast if the connection cannot run sudo non-interactively.
|
|
125
|
+
|
|
126
|
+
Call this at the top of any function that uses ``conn.sudo()``.
|
|
127
|
+
Turns the silent infinite hang (sudo waiting for a password that
|
|
128
|
+
will never arrive) into an immediate, explanatory error.
|
|
129
|
+
|
|
130
|
+
Note: a remote user with passwordless sudo (NOPASSWD in sudoers)
|
|
131
|
+
does not need a sudo password; pass an empty-string check bypass by
|
|
132
|
+
configuring ``sudo_password=""`` is NOT supported — instead, this
|
|
133
|
+
check is advisory: it only raises when no password is configured
|
|
134
|
+
AND the connection was key-authenticated, the exact combination
|
|
135
|
+
that hangs.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
conn: Connection to check.
|
|
139
|
+
what: Human description of the operation, used in the error
|
|
140
|
+
message.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
PermissionError: If no sudo password is configured and the
|
|
144
|
+
connection has no SSH password to fall back on.
|
|
145
|
+
"""
|
|
146
|
+
if has_sudo_password(conn):
|
|
147
|
+
return
|
|
148
|
+
raise PermissionError(
|
|
149
|
+
f"{what} requires sudo, but no sudo password is configured for "
|
|
150
|
+
f"this connection. Pass sudo_password= to connect_to_host(), or "
|
|
151
|
+
f"configure passwordless sudo (NOPASSWD) for the remote user and "
|
|
152
|
+
f"call the function with check_sudo=False if it offers it."
|
|
153
|
+
)
|