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.
Files changed (44) hide show
  1. djevops-0.0.1/LICENSE +21 -0
  2. djevops-0.0.1/PKG-INFO +205 -0
  3. djevops-0.0.1/README.md +184 -0
  4. djevops-0.0.1/djevops/__init__.py +1 -0
  5. djevops-0.0.1/djevops/__main__.py +247 -0
  6. djevops-0.0.1/djevops/bin/celery.sh +12 -0
  7. djevops-0.0.1/djevops/bin/cronic +48 -0
  8. djevops-0.0.1/djevops/bin/gunicorn.sh +20 -0
  9. djevops-0.0.1/djevops/bin/init-run-dir.sh +10 -0
  10. djevops-0.0.1/djevops/bin/manage.sh +6 -0
  11. djevops-0.0.1/djevops/bin/with-bashrc.sh +10 -0
  12. djevops-0.0.1/djevops/check_django_settings.py +62 -0
  13. djevops-0.0.1/djevops/conf/.bash_profile +3 -0
  14. djevops-0.0.1/djevops/conf/crontab +3 -0
  15. djevops-0.0.1/djevops/conf/logrotate/nginx +14 -0
  16. djevops-0.0.1/djevops/conf/logrotate/service +12 -0
  17. djevops-0.0.1/djevops/conf/nginx/default +9 -0
  18. djevops-0.0.1/djevops/conf/nginx/django +29 -0
  19. djevops-0.0.1/djevops/conf/nginx/ssl +3 -0
  20. djevops-0.0.1/djevops/conf/postfix/generic +1 -0
  21. djevops-0.0.1/djevops/conf/postfix/main.cf +48 -0
  22. djevops-0.0.1/djevops/conf/postfix/sasl_passwd +1 -0
  23. djevops-0.0.1/djevops/conf/supervisor/celery.conf +16 -0
  24. djevops-0.0.1/djevops/conf/supervisor/gunicorn.conf +15 -0
  25. djevops-0.0.1/djevops/config.py +42 -0
  26. djevops-0.0.1/djevops/pyproject.toml +36 -0
  27. djevops-0.0.1/djevops/remote/__init__.py +0 -0
  28. djevops-0.0.1/djevops/remote/actions.py +47 -0
  29. djevops-0.0.1/djevops/remote/deploy.py +495 -0
  30. djevops-0.0.1/djevops/remote/scaffold.py +10 -0
  31. djevops-0.0.1/djevops/util.py +60 -0
  32. djevops-0.0.1/djevops/uv.lock +498 -0
  33. djevops-0.0.1/djevops.egg-info/PKG-INFO +205 -0
  34. djevops-0.0.1/djevops.egg-info/SOURCES.txt +42 -0
  35. djevops-0.0.1/djevops.egg-info/dependency_links.txt +1 -0
  36. djevops-0.0.1/djevops.egg-info/entry_points.txt +2 -0
  37. djevops-0.0.1/djevops.egg-info/requires.txt +9 -0
  38. djevops-0.0.1/djevops.egg-info/top_level.txt +1 -0
  39. djevops-0.0.1/pyproject.toml +36 -0
  40. djevops-0.0.1/setup.cfg +4 -0
  41. djevops-0.0.1/test/test_config.py +98 -0
  42. djevops-0.0.1/test/test_main.py +20 -0
  43. djevops-0.0.1/test/test_system.py +723 -0
  44. 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.
@@ -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