-
Notifications
You must be signed in to change notification settings - Fork 0
/
install.py
247 lines (195 loc) · 8.14 KB
/
install.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
"""
Install script for the jobby app.
USAGE:
python install.py --uid="$(id -u)" --gid="$(id -g)" --password=supersecret
"""
import argparse
import os
import platform
import secrets
import shutil
import subprocess
from pathlib import Path
JOBBY_ENV_FILE = """# Environment variables for the jobby app.
# Database connection parameters:
DB_NAME=jobby
DB_USER=jobby
DB_HOST=db
DB_PORT=5432
# Set the settings to use to the "production" settings:
DJANGO_SETTINGS_MODULE = "project.settings.prod"
# The URL path at which the WSGI application will be mounted in the docker container.
# e.g.: MOUNT_POINT=/foo => site available under example.com/foo
MOUNT_POINT=/jobby
"""
DOCKER_ENV_FILE_TEMPLATE = """# Environment variables for docker compose.
# (note that this file should live in the project root)
DATA_DIR = {data_dir}
CONFIG_DIR = {app_config_dir}
# Set UID and GID so we can specify the user for the postgres service and its
# data volume: https://stackoverflow.com/a/56904335/9313033
# Note that this is not necessary under Windows as docker volumes are using
# SMB network shares:
# https://devilbox.readthedocs.io/en/latest/howto/uid-and-gid/find-uid-and-gid-on-win.html#docker-for-windows
{uid}
{gid}
"""
class NotSupported(Exception):
def __init__(self, msg="This operating system is not supported.", *args, **kwargs):
# noinspection PyArgumentList
super().__init__(msg, *args, **kwargs)
def get_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Install the jobby app")
parser.add_argument("--uid", help="The User ID for the postgres container", type=int)
parser.add_argument("--gid", help="The Group ID for the postgres container", type=int)
parser.add_argument("--password", help="Password for the database", type=str, default="foo")
parser.add_argument(
"--allowedhosts",
help="Comma-separated list of allowed host names for the Django settings 'ALLOWED_HOSTS'",
type=str,
default="localhost,127.0.0.1",
)
parser.add_argument("--uninstall", help="Remove jobby directories and files", action="store_true")
return parser
def write_jobby_env_file(app_config_dir: Path):
"""
Create the .env file that contains the environment variables for the app
container.
"""
# TODO: let user override vars when calling setup.py
with open(app_config_dir / ".env", "w") as f:
f.write(JOBBY_ENV_FILE)
def write_docker_env_file(
app_config_dir: Path,
data_dir: Path,
user_id: int | None = None,
group_id: int | None = None,
):
"""
Write the .env file that contains the environment variables required for
docker compose.
"""
# If the user did not specify a UID, leave these vars commented out:
uid = "# UID = 1000"
gid = "# GID = 1000"
if user_id:
uid = f"UID = {user_id}"
if group_id:
gid = f"GID = {group_id}"
env = DOCKER_ENV_FILE_TEMPLATE.format(app_config_dir=app_config_dir, data_dir=data_dir, uid=uid, gid=gid)
with open(Path(__file__).parent / ".env", "w") as f:
f.write(env)
def _generate_secret_key() -> str:
"""
Generate the SECRET_KEY for the Django settings.
See: https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-SECRET_KEY
"""
allowed_chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
return "".join(secrets.choice(allowed_chars) for _ in range(50))
def write_secrets(app_config_dir: Path, allowedhosts: str, key: str, password: str):
"""Write the files containing the secrets into the user config directory."""
secrets_dir = app_config_dir / ".secrets"
secrets_dir.mkdir(exist_ok=True)
for file_name, value in [(".allowedhosts", allowedhosts), (".key", key), (".passwd", password)]:
with open(secrets_dir / file_name, "w") as f:
f.write(value)
def get_config_home() -> Path:
"""Return the default directory for config files (os-dependent)."""
system = platform.system()
if system == "Linux":
# TODO: support XDG_CONFIG_DIRS
return os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
elif system == "Windows":
# TODO: implement
raise NotSupported()
else:
raise NotSupported()
def get_data_home() -> Path:
"""Return the default directory for the user data (os-dependent)."""
system = platform.system()
if system == "Linux":
# TODO: support XDG_DATA_DIRS
return os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")
elif system == "Windows":
# TODO: implement
raise NotSupported()
else:
raise NotSupported()
def get_data_directory() -> Path:
"""Return the directory where the app's persistent data should be stored."""
return get_data_home() / "jobby"
def get_config_directory() -> Path:
"""Return the directory where the app's user configs should be stored."""
return get_config_home() / "jobby"
def setup_data_directory() -> Path:
"""Create the directories for the persistent data."""
directory = get_data_directory()
directory.mkdir(parents=True, exist_ok=True)
directory.joinpath("media").mkdir(exist_ok=True)
directory.joinpath("data").mkdir(exist_ok=True)
with open(directory.parent / "README.txt", "w") as f:
f.write("This is where the file uploads and the database data of the jobby app are stored.")
return directory
def setup_config_directory() -> Path:
"""Create the directories for the user config files."""
directory = get_config_directory()
directory.mkdir(parents=True, exist_ok=True)
with open(directory / "README.txt", "w") as f:
f.write("This is where the user config of the jobby app is stored.")
return directory
def install(password: str, allowedhosts: str, uid: int | None = None, gid: int | None = None, **_kwargs):
"""
Setup directories for the database data and config files. Then create the
docker containers.
"""
print("This sets up directories for persistent storage and config files.")
print(f"Persistent data directory: {get_data_directory()}")
print(f"Config directory: {get_config_directory()}")
print(f"Database password: {password}")
if input("\nContinue? [y/n]: ").lower().strip() not in ("y", "yes", "j", "ja", "ok"):
print("Aborted.")
return
# Create directories:
data_dir = setup_data_directory()
app_config_dir = setup_config_directory()
# TODO: ask for confirmation if overriding existing stuff
# Write secrets and env files:
write_secrets(app_config_dir, allowedhosts=allowedhosts, key=_generate_secret_key(), password=password)
write_jobby_env_file(app_config_dir)
write_docker_env_file(app_config_dir, data_dir, uid, gid)
# Start containers:
print("Creating docker containers...")
subprocess.run(["docker", "compose", "up", "-d", "--build"])
print("Running migrations...")
subprocess.run(["docker", "exec", "-i", "jobby-app", "python3", "manage.py", "migrate"])
print("\nDone! jobby should now be up at http://localhost:8787/jobby/")
def uninstall():
data_dir = get_data_directory()
app_config_dir = get_config_directory()
print("This will delete the following directories and *everything* in it:")
print(data_dir)
print(app_config_dir)
if input("\nContinue? [y/n]: ").lower().strip() not in ("y", "yes", "j", "ja", "ok"):
print("Aborted.")
return
print("Stopping containers...")
subprocess.run(["docker", "compose", "down"])
print("Deleting directories...")
if data_dir.exists():
shutil.rmtree(data_dir)
if app_config_dir.exists():
shutil.rmtree(app_config_dir)
print("Done!")
# TODO: remove docker image?
# subprocess.run(["docker", "image", "rm", "jobby-web", "postgres:alpine"])
print("To delete the docker images try: docker image prune -a")
# TODO: is deleting the source directory itself a safe thing to do?
# shutil.rmtree(Path(__file__).parent)
print("You can now delete this jobby directory.")
if __name__ == "__main__":
_parser = get_parser()
_args = _parser.parse_args()
if _args.uninstall:
uninstall()
else:
install(**vars(_args))