djevops 0.0.1__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.
- djevops-0.0.1/LICENSE +21 -0
- djevops-0.0.1/PKG-INFO +205 -0
- djevops-0.0.1/README.md +184 -0
- djevops-0.0.1/djevops/__init__.py +1 -0
- djevops-0.0.1/djevops/__main__.py +247 -0
- djevops-0.0.1/djevops/bin/celery.sh +12 -0
- djevops-0.0.1/djevops/bin/cronic +48 -0
- djevops-0.0.1/djevops/bin/gunicorn.sh +20 -0
- djevops-0.0.1/djevops/bin/init-run-dir.sh +10 -0
- djevops-0.0.1/djevops/bin/manage.sh +6 -0
- djevops-0.0.1/djevops/bin/with-bashrc.sh +10 -0
- djevops-0.0.1/djevops/check_django_settings.py +62 -0
- djevops-0.0.1/djevops/conf/.bash_profile +3 -0
- djevops-0.0.1/djevops/conf/crontab +3 -0
- djevops-0.0.1/djevops/conf/logrotate/nginx +14 -0
- djevops-0.0.1/djevops/conf/logrotate/service +12 -0
- djevops-0.0.1/djevops/conf/nginx/default +9 -0
- djevops-0.0.1/djevops/conf/nginx/django +29 -0
- djevops-0.0.1/djevops/conf/nginx/ssl +3 -0
- djevops-0.0.1/djevops/conf/postfix/generic +1 -0
- djevops-0.0.1/djevops/conf/postfix/main.cf +48 -0
- djevops-0.0.1/djevops/conf/postfix/sasl_passwd +1 -0
- djevops-0.0.1/djevops/conf/supervisor/celery.conf +16 -0
- djevops-0.0.1/djevops/conf/supervisor/gunicorn.conf +15 -0
- djevops-0.0.1/djevops/config.py +42 -0
- djevops-0.0.1/djevops/pyproject.toml +36 -0
- djevops-0.0.1/djevops/remote/__init__.py +0 -0
- djevops-0.0.1/djevops/remote/actions.py +47 -0
- djevops-0.0.1/djevops/remote/deploy.py +495 -0
- djevops-0.0.1/djevops/remote/scaffold.py +10 -0
- djevops-0.0.1/djevops/util.py +60 -0
- djevops-0.0.1/djevops/uv.lock +498 -0
- djevops-0.0.1/djevops.egg-info/PKG-INFO +205 -0
- djevops-0.0.1/djevops.egg-info/SOURCES.txt +42 -0
- djevops-0.0.1/djevops.egg-info/dependency_links.txt +1 -0
- djevops-0.0.1/djevops.egg-info/entry_points.txt +2 -0
- djevops-0.0.1/djevops.egg-info/requires.txt +9 -0
- djevops-0.0.1/djevops.egg-info/top_level.txt +1 -0
- djevops-0.0.1/pyproject.toml +36 -0
- djevops-0.0.1/setup.cfg +4 -0
- djevops-0.0.1/test/test_config.py +98 -0
- djevops-0.0.1/test/test_main.py +20 -0
- djevops-0.0.1/test/test_system.py +723 -0
- djevops-0.0.1/test/test_util.py +16 -0
djevops-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Herrmann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
djevops-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: djevops
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Host Django without Docker
|
|
5
|
+
Author: Michael Herrmann
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mherrmann/djevops
|
|
8
|
+
Project-URL: Repository, https://github.com/mherrmann/djevops
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: PyYAML==6.0.3
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: boto3; extra == "test"
|
|
15
|
+
Requires-Dist: celery==5.5.3; extra == "test"
|
|
16
|
+
Requires-Dist: django==5.1.2; extra == "test"
|
|
17
|
+
Requires-Dist: dnsimple==2.15.0; extra == "test"
|
|
18
|
+
Requires-Dist: hcloud==2.13.0; extra == "test"
|
|
19
|
+
Requires-Dist: requests==2.32.5; extra == "test"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# djevops: Host Django on bare metal
|
|
23
|
+
|
|
24
|
+
djevops is a command-line tool for deploying Django web apps to Linux VPSs.
|
|
25
|
+
Unlike other tools, djevops runs Django "on bare metal". That is, without
|
|
26
|
+
Docker. This makes development faster and easier.
|
|
27
|
+
|
|
28
|
+
To get started with djevops, all you need is SSH root access to a Linux VPS
|
|
29
|
+
running Ubuntu or Debian. Install djevops on your local machine with
|
|
30
|
+
`pip install djevops`. Then, execute `djevops init` in your Django app's Git
|
|
31
|
+
repository. You get a config file that looks similar to the following:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
server: 1.2.3.4
|
|
35
|
+
|
|
36
|
+
git:
|
|
37
|
+
repo: githubuser/reponame
|
|
38
|
+
branch: main
|
|
39
|
+
|
|
40
|
+
services:
|
|
41
|
+
web:
|
|
42
|
+
type: django
|
|
43
|
+
env:
|
|
44
|
+
clear:
|
|
45
|
+
ALLOWED_HOSTS: your.website.com
|
|
46
|
+
secret:
|
|
47
|
+
- DJANGO_SECRET_KEY
|
|
48
|
+
|
|
49
|
+
db:
|
|
50
|
+
type: sqlite
|
|
51
|
+
|
|
52
|
+
mail:
|
|
53
|
+
host: smtp.gmail.com
|
|
54
|
+
user: SMTP_USER
|
|
55
|
+
password: SMTP_PASSWORD
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Secrets such as `DJANGO_SECRET_KEY` or `SMTP_PASSWORD` can be specified as
|
|
59
|
+
constants in file `djevops/secrets.py`.
|
|
60
|
+
|
|
61
|
+
Most config values are optional. Fill in the ones you want and run
|
|
62
|
+
`djevops deploy`. djevops then clones your Git repo on the `server` and starts
|
|
63
|
+
all services. As you work on your Django app and push new commits to Git, simply
|
|
64
|
+
run `djevops deploy` again to apply them to your server.
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
<details>
|
|
69
|
+
<summary>Automatic SSL certificates</summary>
|
|
70
|
+
|
|
71
|
+
djevops generates and automatically renews SSL certificates for any domains you
|
|
72
|
+
specify in Django setting `ALLOWED_HOSTS`. The domains need to be tied to your
|
|
73
|
+
server's IP address.
|
|
74
|
+
</details>
|
|
75
|
+
|
|
76
|
+
<details>
|
|
77
|
+
<summary>Error emails</summary>
|
|
78
|
+
|
|
79
|
+
If you filled in the `mail` section in the config file, then you can make Django
|
|
80
|
+
email you when errors occur. To do so, set `ADMINS` in Django's `settings.py` as
|
|
81
|
+
follows:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
ADMINS = [('Your Name', 'your@email.com)]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Error emails require Django setting `DEBUG` to be `False`.
|
|
88
|
+
</details>
|
|
89
|
+
|
|
90
|
+
<details>
|
|
91
|
+
<summary>Automatic database backups</summary>
|
|
92
|
+
|
|
93
|
+
You can set up automatic database backups by adding a `backup` element to the
|
|
94
|
+
`db` section in the djevops config file. For example:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
db:
|
|
98
|
+
type: sqlite
|
|
99
|
+
backup:
|
|
100
|
+
type: s3
|
|
101
|
+
bucket: mybackup
|
|
102
|
+
access-key-id: S3_BACKUP_ACCESS_KEY
|
|
103
|
+
secret-access-key: S3_BACKUP_SECRET_KEY
|
|
104
|
+
path: db
|
|
105
|
+
region: us-east-1
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Backups are created continuously while your server is running. If you ever
|
|
109
|
+
re-install your server, then the latest backup is automatically restored.
|
|
110
|
+
|
|
111
|
+
djevops uses [Litestream](https://litestream.io/) for SQLite backups. Litestream
|
|
112
|
+
can store backups in S3, Azure Blob Storage and many others. The keys you add to
|
|
113
|
+
the `backup` element above get copied into a `replica` element in Litestream's
|
|
114
|
+
config. For more information about the available options, please
|
|
115
|
+
see [Litestream's documentation](https://litestream.io/reference/config/).
|
|
116
|
+
</details>
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary>Background tasks via Celery and Redis</summary>
|
|
120
|
+
|
|
121
|
+
If your Django app uses the `celery` Python package, then you can add a Celery
|
|
122
|
+
worker by adding the following item to the djevops config:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
services:
|
|
126
|
+
web:
|
|
127
|
+
# as before
|
|
128
|
+
celery:
|
|
129
|
+
type: celery
|
|
130
|
+
env:
|
|
131
|
+
inherit: web
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
To install Redis on the server (which many Django apps use as Celery's backend),
|
|
135
|
+
add an empty `redis` block:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
redis:
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This setup lets you run Python functions asynchronously and on a schedule such
|
|
142
|
+
as "every five hours". The service of type `celery` also runs the necessary
|
|
143
|
+
`beat` scheduler.
|
|
144
|
+
</details>
|
|
145
|
+
|
|
146
|
+
<details>
|
|
147
|
+
<summary>Easy access to log files</summary>
|
|
148
|
+
|
|
149
|
+
djevops writes the log file for each service to `/var/log/<service>.log`. To
|
|
150
|
+
read it, simply SSH into the server and do `less`, `tail -f`, etc. To prevent
|
|
151
|
+
log files from filling up your server's disk space, djevops also rotates and
|
|
152
|
+
compresses log files.
|
|
153
|
+
</details>
|
|
154
|
+
|
|
155
|
+
<details>
|
|
156
|
+
<summary>Secret handling</summary>
|
|
157
|
+
|
|
158
|
+
Very often, you have secrets that you need on the server but should not commit
|
|
159
|
+
to Git. djevops lets you specify such values in the file `djevops/secrets.py`,
|
|
160
|
+
and refer to them from your config file. The way this works is that `secrets.py`
|
|
161
|
+
gets executed on your local machine, and the produced values then get uploaded
|
|
162
|
+
as constants to the server. This gives you a lot of flexibility. You can
|
|
163
|
+
hardcode values in `secrets.py` and not commit that file to Git. Or you can for
|
|
164
|
+
example make `secrets.py` read from environment variables that are available
|
|
165
|
+
when you do `djevops deploy`:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
import os
|
|
169
|
+
MY_SECRET = os.environ['MY_SECRET']
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
You can also invoke password managers in `secrets.py`, etc.
|
|
173
|
+
</details>
|
|
174
|
+
|
|
175
|
+
<details>
|
|
176
|
+
<summary>Secure defaults</summary>
|
|
177
|
+
|
|
178
|
+
djevops uses secure defaults whenever possible. For example, each `service` runs
|
|
179
|
+
as a separate user. This means that environment variables cannot leak from one
|
|
180
|
+
service to another. djevops also makes sure that no unintended ports are open,
|
|
181
|
+
such as for example port 25 when using Postfix for sending emails.
|
|
182
|
+
</details>
|
|
183
|
+
|
|
184
|
+
<details>
|
|
185
|
+
<summary>Automatic OS updates</summary>
|
|
186
|
+
|
|
187
|
+
djevops sets up automatic OS updates to keep your server up-to-date and secure.
|
|
188
|
+
This does not apply major version upgrades, which could introduce potentially
|
|
189
|
+
breaking changes.
|
|
190
|
+
</details>
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
Install the `test` dependencies from
|
|
195
|
+
[`djevops/pyproject.toml`](djevops/pyproject.toml). The easiest way I know for
|
|
196
|
+
doing this is with [`uv`](https://docs.astral.sh/uv/):
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
uv venv
|
|
200
|
+
source .venv/bin/activate
|
|
201
|
+
uv sync --no-install-project --extra test
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Then, you can do `python -m unittest` to run tests. This requires some API keys
|
|
205
|
+
and environment variables.
|
djevops-0.0.1/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# djevops: Host Django on bare metal
|
|
2
|
+
|
|
3
|
+
djevops is a command-line tool for deploying Django web apps to Linux VPSs.
|
|
4
|
+
Unlike other tools, djevops runs Django "on bare metal". That is, without
|
|
5
|
+
Docker. This makes development faster and easier.
|
|
6
|
+
|
|
7
|
+
To get started with djevops, all you need is SSH root access to a Linux VPS
|
|
8
|
+
running Ubuntu or Debian. Install djevops on your local machine with
|
|
9
|
+
`pip install djevops`. Then, execute `djevops init` in your Django app's Git
|
|
10
|
+
repository. You get a config file that looks similar to the following:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
server: 1.2.3.4
|
|
14
|
+
|
|
15
|
+
git:
|
|
16
|
+
repo: githubuser/reponame
|
|
17
|
+
branch: main
|
|
18
|
+
|
|
19
|
+
services:
|
|
20
|
+
web:
|
|
21
|
+
type: django
|
|
22
|
+
env:
|
|
23
|
+
clear:
|
|
24
|
+
ALLOWED_HOSTS: your.website.com
|
|
25
|
+
secret:
|
|
26
|
+
- DJANGO_SECRET_KEY
|
|
27
|
+
|
|
28
|
+
db:
|
|
29
|
+
type: sqlite
|
|
30
|
+
|
|
31
|
+
mail:
|
|
32
|
+
host: smtp.gmail.com
|
|
33
|
+
user: SMTP_USER
|
|
34
|
+
password: SMTP_PASSWORD
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Secrets such as `DJANGO_SECRET_KEY` or `SMTP_PASSWORD` can be specified as
|
|
38
|
+
constants in file `djevops/secrets.py`.
|
|
39
|
+
|
|
40
|
+
Most config values are optional. Fill in the ones you want and run
|
|
41
|
+
`djevops deploy`. djevops then clones your Git repo on the `server` and starts
|
|
42
|
+
all services. As you work on your Django app and push new commits to Git, simply
|
|
43
|
+
run `djevops deploy` again to apply them to your server.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
<details>
|
|
48
|
+
<summary>Automatic SSL certificates</summary>
|
|
49
|
+
|
|
50
|
+
djevops generates and automatically renews SSL certificates for any domains you
|
|
51
|
+
specify in Django setting `ALLOWED_HOSTS`. The domains need to be tied to your
|
|
52
|
+
server's IP address.
|
|
53
|
+
</details>
|
|
54
|
+
|
|
55
|
+
<details>
|
|
56
|
+
<summary>Error emails</summary>
|
|
57
|
+
|
|
58
|
+
If you filled in the `mail` section in the config file, then you can make Django
|
|
59
|
+
email you when errors occur. To do so, set `ADMINS` in Django's `settings.py` as
|
|
60
|
+
follows:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
ADMINS = [('Your Name', 'your@email.com)]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Error emails require Django setting `DEBUG` to be `False`.
|
|
67
|
+
</details>
|
|
68
|
+
|
|
69
|
+
<details>
|
|
70
|
+
<summary>Automatic database backups</summary>
|
|
71
|
+
|
|
72
|
+
You can set up automatic database backups by adding a `backup` element to the
|
|
73
|
+
`db` section in the djevops config file. For example:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
db:
|
|
77
|
+
type: sqlite
|
|
78
|
+
backup:
|
|
79
|
+
type: s3
|
|
80
|
+
bucket: mybackup
|
|
81
|
+
access-key-id: S3_BACKUP_ACCESS_KEY
|
|
82
|
+
secret-access-key: S3_BACKUP_SECRET_KEY
|
|
83
|
+
path: db
|
|
84
|
+
region: us-east-1
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Backups are created continuously while your server is running. If you ever
|
|
88
|
+
re-install your server, then the latest backup is automatically restored.
|
|
89
|
+
|
|
90
|
+
djevops uses [Litestream](https://litestream.io/) for SQLite backups. Litestream
|
|
91
|
+
can store backups in S3, Azure Blob Storage and many others. The keys you add to
|
|
92
|
+
the `backup` element above get copied into a `replica` element in Litestream's
|
|
93
|
+
config. For more information about the available options, please
|
|
94
|
+
see [Litestream's documentation](https://litestream.io/reference/config/).
|
|
95
|
+
</details>
|
|
96
|
+
|
|
97
|
+
<details>
|
|
98
|
+
<summary>Background tasks via Celery and Redis</summary>
|
|
99
|
+
|
|
100
|
+
If your Django app uses the `celery` Python package, then you can add a Celery
|
|
101
|
+
worker by adding the following item to the djevops config:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
services:
|
|
105
|
+
web:
|
|
106
|
+
# as before
|
|
107
|
+
celery:
|
|
108
|
+
type: celery
|
|
109
|
+
env:
|
|
110
|
+
inherit: web
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
To install Redis on the server (which many Django apps use as Celery's backend),
|
|
114
|
+
add an empty `redis` block:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
redis:
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This setup lets you run Python functions asynchronously and on a schedule such
|
|
121
|
+
as "every five hours". The service of type `celery` also runs the necessary
|
|
122
|
+
`beat` scheduler.
|
|
123
|
+
</details>
|
|
124
|
+
|
|
125
|
+
<details>
|
|
126
|
+
<summary>Easy access to log files</summary>
|
|
127
|
+
|
|
128
|
+
djevops writes the log file for each service to `/var/log/<service>.log`. To
|
|
129
|
+
read it, simply SSH into the server and do `less`, `tail -f`, etc. To prevent
|
|
130
|
+
log files from filling up your server's disk space, djevops also rotates and
|
|
131
|
+
compresses log files.
|
|
132
|
+
</details>
|
|
133
|
+
|
|
134
|
+
<details>
|
|
135
|
+
<summary>Secret handling</summary>
|
|
136
|
+
|
|
137
|
+
Very often, you have secrets that you need on the server but should not commit
|
|
138
|
+
to Git. djevops lets you specify such values in the file `djevops/secrets.py`,
|
|
139
|
+
and refer to them from your config file. The way this works is that `secrets.py`
|
|
140
|
+
gets executed on your local machine, and the produced values then get uploaded
|
|
141
|
+
as constants to the server. This gives you a lot of flexibility. You can
|
|
142
|
+
hardcode values in `secrets.py` and not commit that file to Git. Or you can for
|
|
143
|
+
example make `secrets.py` read from environment variables that are available
|
|
144
|
+
when you do `djevops deploy`:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
import os
|
|
148
|
+
MY_SECRET = os.environ['MY_SECRET']
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
You can also invoke password managers in `secrets.py`, etc.
|
|
152
|
+
</details>
|
|
153
|
+
|
|
154
|
+
<details>
|
|
155
|
+
<summary>Secure defaults</summary>
|
|
156
|
+
|
|
157
|
+
djevops uses secure defaults whenever possible. For example, each `service` runs
|
|
158
|
+
as a separate user. This means that environment variables cannot leak from one
|
|
159
|
+
service to another. djevops also makes sure that no unintended ports are open,
|
|
160
|
+
such as for example port 25 when using Postfix for sending emails.
|
|
161
|
+
</details>
|
|
162
|
+
|
|
163
|
+
<details>
|
|
164
|
+
<summary>Automatic OS updates</summary>
|
|
165
|
+
|
|
166
|
+
djevops sets up automatic OS updates to keep your server up-to-date and secure.
|
|
167
|
+
This does not apply major version upgrades, which could introduce potentially
|
|
168
|
+
breaking changes.
|
|
169
|
+
</details>
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
Install the `test` dependencies from
|
|
174
|
+
[`djevops/pyproject.toml`](djevops/pyproject.toml). The easiest way I know for
|
|
175
|
+
doing this is with [`uv`](https://docs.astral.sh/uv/):
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
uv venv
|
|
179
|
+
source .venv/bin/activate
|
|
180
|
+
uv sync --no-install-project --extra test
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Then, you can do `python -m unittest` to run tests. This requires some API keys
|
|
184
|
+
and environment variables.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
GIT_HINT = "Don't forget to commit *and push* your changes to Git."
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from djevops import GIT_HINT
|
|
2
|
+
from djevops.config import get_services_users_envs, get_django_service
|
|
3
|
+
from djevops.util import git, get_apt_install_cmd, run_in_django_shell, \
|
|
4
|
+
run_silently
|
|
5
|
+
from functools import partial
|
|
6
|
+
from os import remove, makedirs
|
|
7
|
+
from os.path import dirname, exists
|
|
8
|
+
from runpy import run_path
|
|
9
|
+
from shlex import quote
|
|
10
|
+
from subprocess import run
|
|
11
|
+
from tempfile import NamedTemporaryFile
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
SAMPLE_SERVER_IP = '0.0.0.0'
|
|
21
|
+
|
|
22
|
+
SAMPLE_SECRETS_PY = """
|
|
23
|
+
# This file lets you store secrets that you can then refer to from your djevops
|
|
24
|
+
# config. For example:
|
|
25
|
+
#
|
|
26
|
+
# DJANGO_SECRET_KEY = '1234...'
|
|
27
|
+
#
|
|
28
|
+
# The motivation for keeping secrets in a separate file is that you usually do
|
|
29
|
+
# not want to commit them to Git. One approach you can use is to hard-code your
|
|
30
|
+
# secrets here and only store djevops' deploy.yml file in Git.
|
|
31
|
+
#
|
|
32
|
+
# Another approach is to read secrets from environment variables. For example:
|
|
33
|
+
#
|
|
34
|
+
# import os
|
|
35
|
+
# MY_SECRET = os.environ['MY_SECRET']
|
|
36
|
+
#
|
|
37
|
+
# This works if the environment variables are available when you execute
|
|
38
|
+
# `djevops deploy`. The produced values are uploaded as constants to your
|
|
39
|
+
# server. If all your secrets are read from environment variables in this way,
|
|
40
|
+
# then you can consider committing this file to Git.
|
|
41
|
+
#
|
|
42
|
+
# (Feel free to remove these comments once you are done reading them.)
|
|
43
|
+
""".lstrip()
|
|
44
|
+
|
|
45
|
+
SECRETS_NAME_RE = r'^[A-Z][A-Z0-9_]+$'
|
|
46
|
+
|
|
47
|
+
class CommandError(Exception):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def init(quiet=False):
|
|
51
|
+
if not exists('.git'):
|
|
52
|
+
raise CommandError('This directory is not a Git repository.')
|
|
53
|
+
remotes = git('remote').splitlines()
|
|
54
|
+
if not remotes:
|
|
55
|
+
raise CommandError(
|
|
56
|
+
"This Git repository has no remotes. If you add one, don't forget "
|
|
57
|
+
"to run `git push` after."
|
|
58
|
+
)
|
|
59
|
+
if not exists('manage.py'):
|
|
60
|
+
raise CommandError(
|
|
61
|
+
"There is no manage.py file in the current directory. If you add "
|
|
62
|
+
"one, don't forget to commit *and push* your changes to Git."
|
|
63
|
+
)
|
|
64
|
+
if not exists('requirements.txt'):
|
|
65
|
+
raise CommandError(
|
|
66
|
+
'Please create a requirements.txt file. For example, by running:\n'
|
|
67
|
+
' pip freeze > requirements.txt\n' + GIT_HINT
|
|
68
|
+
)
|
|
69
|
+
with open('requirements.txt') as f:
|
|
70
|
+
requirements = f.read()
|
|
71
|
+
for dep in ('django', 'gunicorn'):
|
|
72
|
+
if not re.search(
|
|
73
|
+
rf'^{dep}\s*\b', requirements, re.MULTILINE | re.IGNORECASE
|
|
74
|
+
):
|
|
75
|
+
raise CommandError(
|
|
76
|
+
f'Please add `{dep}` to your requirements.txt file. ' + GIT_HINT
|
|
77
|
+
)
|
|
78
|
+
remote = remotes[0]
|
|
79
|
+
remote_url = git('remote', 'get-url', remote)
|
|
80
|
+
if remote_url.startswith('https://'):
|
|
81
|
+
parsed_url = urlparse(remote_url)
|
|
82
|
+
git_server = parsed_url.hostname
|
|
83
|
+
git_repo = parsed_url.path.lstrip('/')
|
|
84
|
+
else:
|
|
85
|
+
m = re.match(r'git@([^:]+):(.*)$', remote_url)
|
|
86
|
+
if not m:
|
|
87
|
+
raise CommandError(f'Invalid Git remote URL: {remote_url}')
|
|
88
|
+
git_server = m.group(1)
|
|
89
|
+
git_repo = m.group(2)
|
|
90
|
+
if git_server == 'github.com' and git_repo.endswith('.git'):
|
|
91
|
+
git_repo = git_repo[:-4]
|
|
92
|
+
git_config = {}
|
|
93
|
+
if git_server != 'github.com':
|
|
94
|
+
git_config['server'] = git_server
|
|
95
|
+
git_config['repo'] = git_repo
|
|
96
|
+
git_config['branch'] = git('branch', '--show-current')
|
|
97
|
+
deploy_yml = {
|
|
98
|
+
'server': SAMPLE_SERVER_IP,
|
|
99
|
+
'git': git_config,
|
|
100
|
+
'services': {
|
|
101
|
+
'web': {
|
|
102
|
+
'type': 'django'
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for path in ('djevops/deploy.yml', 'djevops/secrets.py'):
|
|
107
|
+
if exists(path):
|
|
108
|
+
raise CommandError(f'{path} already exists.')
|
|
109
|
+
makedirs('djevops', exist_ok=True)
|
|
110
|
+
with open('djevops/deploy.yml', 'w') as f:
|
|
111
|
+
f.write(yaml.dump(deploy_yml, sort_keys=False))
|
|
112
|
+
with open('djevops/secrets.py', 'w') as f:
|
|
113
|
+
f.write(SAMPLE_SECRETS_PY)
|
|
114
|
+
if not quiet:
|
|
115
|
+
print('Created djevops/deploy.yml')
|
|
116
|
+
print('Created djevops/secrets.py')
|
|
117
|
+
print(f'To deploy your Django app to a server, run: djevops deploy')
|
|
118
|
+
|
|
119
|
+
def deploy(quiet=False, dry_run=False):
|
|
120
|
+
deploy_yml = 'djevops/deploy.yml'
|
|
121
|
+
with open(deploy_yml) as f:
|
|
122
|
+
deploy_config = yaml.safe_load(f)
|
|
123
|
+
secrets = get_secrets('djevops/secrets.py')
|
|
124
|
+
|
|
125
|
+
check_config(deploy_config, secrets)
|
|
126
|
+
|
|
127
|
+
if dry_run:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
server = deploy_config['server']
|
|
131
|
+
install_djevops_on_server('root', server, quiet)
|
|
132
|
+
rsync('-a', deploy_yml, f'root@{server}:/root/deploy.yml')
|
|
133
|
+
|
|
134
|
+
secrets_json = NamedTemporaryFile(mode='w', delete=False, suffix='.json')
|
|
135
|
+
json.dump(secrets, secrets_json, indent=2, sort_keys=True)
|
|
136
|
+
secrets_json.close()
|
|
137
|
+
try:
|
|
138
|
+
rsync('-a', secrets_json.name, f'root@{server}:/root/secrets.json')
|
|
139
|
+
finally:
|
|
140
|
+
remove(secrets_json.name)
|
|
141
|
+
|
|
142
|
+
run_with_djevops_venv(
|
|
143
|
+
'root', server, 'python -u -m djevops.remote.deploy', quiet
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def check_config(deploy_config, secrets):
|
|
147
|
+
server_ip = deploy_config.get('server')
|
|
148
|
+
if not server_ip or server_ip == SAMPLE_SERVER_IP:
|
|
149
|
+
raise CommandError(
|
|
150
|
+
"Please set your server's IP address in djevops/deploy.yml. For "
|
|
151
|
+
"example:\n"
|
|
152
|
+
" server: 1.2.3.4"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
django_service_name, django_service = get_django_service(deploy_config)
|
|
157
|
+
except LookupError:
|
|
158
|
+
raise CommandError(
|
|
159
|
+
'Please add at least one service of type `django` to '
|
|
160
|
+
'djevops/deploy.yml. For example:\n'
|
|
161
|
+
' services:\n'
|
|
162
|
+
' web:\n'
|
|
163
|
+
' type: django'
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
user_envs = get_services_users_envs(deploy_config, secrets)
|
|
167
|
+
django_env = user_envs[django_service_name][1]
|
|
168
|
+
|
|
169
|
+
has_db = bool(deploy_config.get('db'))
|
|
170
|
+
|
|
171
|
+
# Ensure `djevops.check_django_settings` is loadable:
|
|
172
|
+
django_shell_env = django_env | {'PYTHONPATH': ':'.join(sys.path)}
|
|
173
|
+
error_msg = run_in_django_shell([
|
|
174
|
+
'from djevops.check_django_settings import main',
|
|
175
|
+
f'main({server_ip!r}, {has_db})',
|
|
176
|
+
], env=django_shell_env)
|
|
177
|
+
if error_msg:
|
|
178
|
+
raise CommandError(error_msg)
|
|
179
|
+
|
|
180
|
+
def install_djevops_on_server(user, host, quiet):
|
|
181
|
+
ssh_ = lambda cmd: ssh(user, host, cmd, quiet)
|
|
182
|
+
ssh_(get_apt_install_cmd('rsync'))
|
|
183
|
+
ssh_(
|
|
184
|
+
'command -v uv >/dev/null 2>&1 || '
|
|
185
|
+
'curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1'
|
|
186
|
+
)
|
|
187
|
+
rsync(
|
|
188
|
+
'-raL',
|
|
189
|
+
dirname(__file__) + '/',
|
|
190
|
+
f'{user}@{host}:/opt/djevops/',
|
|
191
|
+
"--include=**.gitignore",
|
|
192
|
+
"--filter=:- .gitignore",
|
|
193
|
+
"--delete-after"
|
|
194
|
+
)
|
|
195
|
+
ssh_(
|
|
196
|
+
'ln -sf /opt/djevops/pyproject.toml /opt/pyproject.toml && '
|
|
197
|
+
'ln -sf /opt/djevops/uv.lock /opt/uv.lock && '
|
|
198
|
+
'cd /opt && '
|
|
199
|
+
'UV_PROJECT_ENVIRONMENT=/opt/djevops/.venv ~/.local/bin/uv sync -q && '
|
|
200
|
+
'rm /opt/pyproject.toml /opt/uv.lock'
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def get_secrets(path):
|
|
204
|
+
if not exists(path):
|
|
205
|
+
return {}
|
|
206
|
+
return {
|
|
207
|
+
k: v for k, v in run_path(path).items() if re.match(SECRETS_NAME_RE, k)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
def run_with_djevops_venv(user, host, cmd, quiet):
|
|
211
|
+
ssh(user, host, f'/opt/djevops/.venv/bin/{cmd}', quiet)
|
|
212
|
+
|
|
213
|
+
def rsync(*args):
|
|
214
|
+
ssh_cmd = get_ssh_command()
|
|
215
|
+
extra_rsync_args = [] if ssh_cmd == 'ssh' else ['-e', ssh_cmd]
|
|
216
|
+
run_silently(['rsync', *extra_rsync_args, *args])
|
|
217
|
+
|
|
218
|
+
def ssh(user, host, cmd, quiet):
|
|
219
|
+
ssh_cmd = get_ssh_command()
|
|
220
|
+
run_ = run_silently if quiet else partial(run, check=True)
|
|
221
|
+
run_(f'{ssh_cmd} {user}@{host} {quote(cmd)}', shell=True)
|
|
222
|
+
|
|
223
|
+
def get_ssh_command():
|
|
224
|
+
try:
|
|
225
|
+
return os.environ['DJEVOPS_SSH_COMMAND']
|
|
226
|
+
except KeyError:
|
|
227
|
+
return 'ssh'
|
|
228
|
+
|
|
229
|
+
def main():
|
|
230
|
+
if len(sys.argv) < 2 or len(sys.argv) > 3:
|
|
231
|
+
print('Usage: djevops init|deploy [--quiet]')
|
|
232
|
+
sys.exit(0)
|
|
233
|
+
command = sys.argv[1]
|
|
234
|
+
quiet = len(sys.argv) == 3 and sys.argv[2] == '--quiet'
|
|
235
|
+
try:
|
|
236
|
+
if command == 'init':
|
|
237
|
+
init(quiet)
|
|
238
|
+
elif command == 'deploy':
|
|
239
|
+
deploy(quiet)
|
|
240
|
+
else:
|
|
241
|
+
raise CommandError(f'Unknown command: {command}')
|
|
242
|
+
except CommandError as e:
|
|
243
|
+
sys.stderr.write(e.args[0] + '\n')
|
|
244
|
+
sys.exit(1)
|
|
245
|
+
|
|
246
|
+
if __name__ == '__main__':
|
|
247
|
+
main()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -eo pipefail
|
|
4
|
+
|
|
5
|
+
SERVICE=$1
|
|
6
|
+
|
|
7
|
+
DJANGO_PROJECT=$(find . -name wsgi.py | head -1 | xargs dirname | sed 's|^\./||')
|
|
8
|
+
|
|
9
|
+
exec celery -A $DJANGO_PROJECT worker -B \
|
|
10
|
+
-s /var/run/djevops/$SERVICE-schedule \
|
|
11
|
+
--pidfile /var/run/djevops/$SERVICE.pid \
|
|
12
|
+
-l info
|