flask-nginx 0.8.0__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.
- flask_nginx/__init__.py +0 -0
- flask_nginx/__main__.py +19 -0
- flask_nginx/cli.py +111 -0
- flask_nginx/config.py +90 -0
- flask_nginx/core.py +177 -0
- flask_nginx/irds.py +182 -0
- flask_nginx/logo.py +47 -0
- flask_nginx/mailer.py +53 -0
- flask_nginx/mysql.py +498 -0
- flask_nginx/py.typed +0 -0
- flask_nginx/remote.py +20 -0
- flask_nginx/restartd.py +60 -0
- flask_nginx/rsync.py +34 -0
- flask_nginx/systemd/__init__.py +0 -0
- flask_nginx/systemd/cli.py +10 -0
- flask_nginx/systemd/nginx.py +645 -0
- flask_nginx/systemd/supervisor.py +259 -0
- flask_nginx/systemd/systemd.py +556 -0
- flask_nginx/systemd/utils.py +289 -0
- flask_nginx/templates/celery.service +28 -0
- flask_nginx/templates/nginx-app.conf +15 -0
- flask_nginx/templates/nginx-test.conf +40 -0
- flask_nginx/templates/nginx-upstream.conf +71 -0
- flask_nginx/templates/nginx.conf +67 -0
- flask_nginx/templates/secure-tunnel.service +15 -0
- flask_nginx/templates/ssl.conf +25 -0
- flask_nginx/templates/supervisord.ini +18 -0
- flask_nginx/templates/systemd.mount +16 -0
- flask_nginx/templates/systemd.service +39 -0
- flask_nginx/templating.py +119 -0
- flask_nginx/url.py +90 -0
- flask_nginx/utils.py +185 -0
- flask_nginx/watch.py +239 -0
- flask_nginx-0.8.0.dist-info/METADATA +133 -0
- flask_nginx-0.8.0.dist-info/RECORD +37 -0
- flask_nginx-0.8.0.dist-info/WHEEL +4 -0
- flask_nginx-0.8.0.dist-info/entry_points.txt +2 -0
flask_nginx/__init__.py
ADDED
|
File without changes
|
flask_nginx/__main__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# pylint: disable=unused-import
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from . import irds # noqa:
|
|
5
|
+
from . import mailer # noqa:
|
|
6
|
+
from . import mysql # noqa:
|
|
7
|
+
from . import remote # noqa:
|
|
8
|
+
from . import restartd # noqa:
|
|
9
|
+
from . import rsync # noqa:
|
|
10
|
+
from . import watch # noqa:
|
|
11
|
+
from .cli import cli
|
|
12
|
+
from .systemd import nginx # noqa:
|
|
13
|
+
from .systemd import supervisor # noqa:
|
|
14
|
+
from .systemd import systemd # noqa:
|
|
15
|
+
|
|
16
|
+
# from . import logo # noqa:
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
cli.main(prog_name="footprint")
|
flask_nginx/cli.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group(
|
|
9
|
+
# cls=DYMGroup,
|
|
10
|
+
epilog=click.style("Commands to manage websites\n", fg="magenta"),
|
|
11
|
+
)
|
|
12
|
+
@click.version_option()
|
|
13
|
+
def cli() -> None:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@cli.command()
|
|
18
|
+
def update() -> None:
|
|
19
|
+
"""Update this package"""
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
from .config import REPO
|
|
23
|
+
|
|
24
|
+
ret = subprocess.call([sys.executable, "-m", "pip", "install", "-U", REPO])
|
|
25
|
+
if ret:
|
|
26
|
+
click.secho(f"can't install {REPO}", fg="red")
|
|
27
|
+
raise click.Abort()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@cli.command()
|
|
31
|
+
def repo() -> None:
|
|
32
|
+
"""show git repository"""
|
|
33
|
+
|
|
34
|
+
from .config import REPO
|
|
35
|
+
|
|
36
|
+
click.echo(REPO)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.command()
|
|
40
|
+
def config_show() -> None:
|
|
41
|
+
"""Show configuration"""
|
|
42
|
+
from dataclasses import fields
|
|
43
|
+
from .config import get_config
|
|
44
|
+
|
|
45
|
+
Config = get_config()
|
|
46
|
+
|
|
47
|
+
n = max(len(f.name) for f in fields(Config))
|
|
48
|
+
|
|
49
|
+
for f in fields(Config):
|
|
50
|
+
k = f.name
|
|
51
|
+
v = getattr(Config, f.name)
|
|
52
|
+
click.echo(f"{k:<{n}}: {v}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@cli.command()
|
|
56
|
+
@click.option("-a", "--append", is_flag=True, help="append to file")
|
|
57
|
+
@click.argument("filename")
|
|
58
|
+
def config_dump(filename: str, append: bool) -> None:
|
|
59
|
+
"""Dump configuration"""
|
|
60
|
+
from .utils import require_mod
|
|
61
|
+
from .config import dump_to_file
|
|
62
|
+
|
|
63
|
+
require_mod("toml")
|
|
64
|
+
|
|
65
|
+
if not dump_to_file(filename, append):
|
|
66
|
+
click.secho("can't dump configuration!", fg="red", err=True)
|
|
67
|
+
raise click.Abort()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# @cli.command()
|
|
71
|
+
# @click.option("-p", "--with-python", is_flag=True)
|
|
72
|
+
# @click.option("-c", "--compile", "use_pip_compile", is_flag=True)
|
|
73
|
+
# @click.argument("project_dir", required=False)
|
|
74
|
+
def poetry_to_reqs(
|
|
75
|
+
project_dir: str,
|
|
76
|
+
with_python: bool,
|
|
77
|
+
use_pip_compile: bool = True,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Generate a requirements.txt file from pyproject.toml [**may require toml**]"""
|
|
80
|
+
import os
|
|
81
|
+
from contextlib import suppress
|
|
82
|
+
from .utils import toml_load
|
|
83
|
+
|
|
84
|
+
pyproject = "pyproject.toml"
|
|
85
|
+
if project_dir:
|
|
86
|
+
pyproject = os.path.join(project_dir, pyproject)
|
|
87
|
+
if not os.path.isfile(pyproject):
|
|
88
|
+
raise click.BadArgumentUsage("no pyproject.toml file!")
|
|
89
|
+
|
|
90
|
+
def fix(req: str) -> str:
|
|
91
|
+
if req.startswith("^"):
|
|
92
|
+
return f">={req[1:]}"
|
|
93
|
+
return req
|
|
94
|
+
|
|
95
|
+
reqs = "\n".join(
|
|
96
|
+
f"{k}{fix(v)}"
|
|
97
|
+
for k, v in sorted(
|
|
98
|
+
toml_load(pyproject)["tool"]["poetry"]["dependencies"].items(),
|
|
99
|
+
)
|
|
100
|
+
if with_python or k != "python" and isinstance(v, str)
|
|
101
|
+
)
|
|
102
|
+
if use_pip_compile:
|
|
103
|
+
try:
|
|
104
|
+
with open("requirements.in", "w", encoding="utf-8") as fp:
|
|
105
|
+
click.echo(reqs, file=fp)
|
|
106
|
+
subprocess.check_call(["pip-compile"])
|
|
107
|
+
finally:
|
|
108
|
+
with suppress(OSError):
|
|
109
|
+
os.remove("requirements.in")
|
|
110
|
+
else:
|
|
111
|
+
click.echo(reqs)
|
flask_nginx/config.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import field
|
|
7
|
+
from dataclasses import fields
|
|
8
|
+
from dataclasses import replace
|
|
9
|
+
from typing import IO
|
|
10
|
+
|
|
11
|
+
from .utils import toml_load
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
REPO = "git+https://github.com/arabidopsis/footprint.git"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Config:
|
|
19
|
+
mailhost: str = "uwa-edu-au.mail.protection.outlook.com"
|
|
20
|
+
# mailhost: str = "antivirus.uwa.edu.au"
|
|
21
|
+
datastore: str = "//drive.irds.uwa.edu.au/sci-ms-001"
|
|
22
|
+
# directories that *might* be in the static directory
|
|
23
|
+
static_dir: str = (
|
|
24
|
+
r"img|images|js|css|media|docs|tutorials|notebooks|downloads|\.well-known"
|
|
25
|
+
)
|
|
26
|
+
# basic files that have urls such as /robots.txt /favicon.ico etc.
|
|
27
|
+
static_files: str = (
|
|
28
|
+
r"robots\.txt|crossdomain\.xml|favicon\.ico|browserconfig\.xml|humans\.txt"
|
|
29
|
+
)
|
|
30
|
+
# exclude these filenames/directories from static consideration
|
|
31
|
+
exclude: list[str] = field(default_factory=lambda: ["__pycache__"])
|
|
32
|
+
# directory to put config files: (Ubuntu, RHEL8)
|
|
33
|
+
nginx_dirs: list[str] = field(
|
|
34
|
+
default_factory=lambda: ["/etc/nginx/sites-enabled", "/etc/nginx/conf.d"],
|
|
35
|
+
)
|
|
36
|
+
arg_color: str = "yellow" # use "none" for no color
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
XConfig: Config | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_config() -> Config:
|
|
43
|
+
global XConfig
|
|
44
|
+
if XConfig is None:
|
|
45
|
+
XConfig = _init_config(Config())
|
|
46
|
+
return XConfig
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _init_config(config: Config, application_dir: str = ".") -> Config:
|
|
50
|
+
project = os.path.join(application_dir, "pyproject.toml")
|
|
51
|
+
if os.path.isfile(project):
|
|
52
|
+
try:
|
|
53
|
+
d = toml_load(project)
|
|
54
|
+
if "tool" not in d:
|
|
55
|
+
return config
|
|
56
|
+
cfg = d["tool"].get("footprint")
|
|
57
|
+
if cfg is None:
|
|
58
|
+
return config
|
|
59
|
+
data = {}
|
|
60
|
+
for f in fields(config):
|
|
61
|
+
if f.name in cfg:
|
|
62
|
+
data[f.name] = cfg[f.name]
|
|
63
|
+
|
|
64
|
+
if data:
|
|
65
|
+
config = replace(config, **data)
|
|
66
|
+
|
|
67
|
+
except ImportError:
|
|
68
|
+
pass
|
|
69
|
+
except Exception:
|
|
70
|
+
import click
|
|
71
|
+
|
|
72
|
+
click.secho(f'can\'t load "{project}"', fg="red", bold=True, err=True)
|
|
73
|
+
return config
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def dump_toml(config: Config, out: IO[str]) -> bool:
|
|
77
|
+
try:
|
|
78
|
+
import toml # type: ignore
|
|
79
|
+
|
|
80
|
+
d = dict(tool=dict(footprint=asdict(config)))
|
|
81
|
+
toml.dump(d, out)
|
|
82
|
+
return True
|
|
83
|
+
except Exception:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def dump_to_file(filename: str, append: bool) -> bool:
|
|
88
|
+
config = get_config()
|
|
89
|
+
with open(filename, "a" if append else "w", encoding="utf-8") as fp:
|
|
90
|
+
return dump_toml(config, fp)
|
flask_nginx/core.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from contextlib import redirect_stderr
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from os.path import abspath
|
|
8
|
+
from os.path import expanduser
|
|
9
|
+
from os.path import isdir
|
|
10
|
+
from os.path import isfile
|
|
11
|
+
from os.path import normpath
|
|
12
|
+
from typing import cast
|
|
13
|
+
from typing import Iterator
|
|
14
|
+
from typing import NamedTuple
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from click import BadParameter
|
|
18
|
+
from click import secho
|
|
19
|
+
from click import style
|
|
20
|
+
from werkzeug.routing import Rule
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from flask import Flask
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# core ability
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StaticFolder(NamedTuple):
|
|
30
|
+
url: str | None
|
|
31
|
+
folder: str
|
|
32
|
+
rewrite: bool
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
STATIC_RULE = re.compile("^(.*)/<path:filename>$")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def topath(path: str) -> str:
|
|
39
|
+
return normpath(abspath(expanduser(path)))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_static_folders(app: Flask) -> list[StaticFolder]: # noqa: C901
|
|
43
|
+
def get_static_folder(rule: Rule) -> str | None:
|
|
44
|
+
bound_method = app.view_functions[rule.endpoint]
|
|
45
|
+
if hasattr(bound_method, "static_folder"):
|
|
46
|
+
return bound_method.static_folder # type: ignore
|
|
47
|
+
# __self__ is the blueprint of send_static_file method
|
|
48
|
+
if hasattr(bound_method, "__self__"):
|
|
49
|
+
bp = bound_method.__self__ # type: ignore
|
|
50
|
+
if bp.has_static_folder:
|
|
51
|
+
return bp.static_folder # type: ignore
|
|
52
|
+
# now just a lambda :(
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def find_static(app: Flask) -> Iterator[StaticFolder]:
|
|
56
|
+
if app.has_static_folder:
|
|
57
|
+
prefix, folder = app.static_url_path, app.static_folder
|
|
58
|
+
if folder is not None and isdir(folder):
|
|
59
|
+
yield StaticFolder(
|
|
60
|
+
prefix,
|
|
61
|
+
topath(folder),
|
|
62
|
+
(not folder.endswith(prefix) if prefix else False),
|
|
63
|
+
)
|
|
64
|
+
for r in app.url_map.iter_rules():
|
|
65
|
+
if not r.endpoint.endswith("static"):
|
|
66
|
+
continue
|
|
67
|
+
m = STATIC_RULE.match(r.rule)
|
|
68
|
+
if not m:
|
|
69
|
+
continue
|
|
70
|
+
rewrite = False
|
|
71
|
+
prefix = m.group(1)
|
|
72
|
+
folder = get_static_folder(r)
|
|
73
|
+
if folder is None:
|
|
74
|
+
if r.endpoint != "static":
|
|
75
|
+
# static view_func for app is now
|
|
76
|
+
# just a lambda.
|
|
77
|
+
secho(
|
|
78
|
+
f"location: can't find static folder for endpoint: {r.endpoint}",
|
|
79
|
+
fg="red",
|
|
80
|
+
err=True,
|
|
81
|
+
)
|
|
82
|
+
continue
|
|
83
|
+
if not folder.endswith(prefix):
|
|
84
|
+
rewrite = True
|
|
85
|
+
|
|
86
|
+
if not isdir(folder):
|
|
87
|
+
continue
|
|
88
|
+
yield StaticFolder(prefix, topath(folder), rewrite)
|
|
89
|
+
|
|
90
|
+
return list(set(find_static(app)))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_static_folders_for_app(
|
|
94
|
+
application_dir: str,
|
|
95
|
+
app: Flask | None = None,
|
|
96
|
+
prefix: str = "",
|
|
97
|
+
entrypoint: str = "app.app",
|
|
98
|
+
) -> list[StaticFolder]:
|
|
99
|
+
def fixstatic(s: StaticFolder) -> StaticFolder:
|
|
100
|
+
url = prefix + (s.url or "")
|
|
101
|
+
if url and s.folder.endswith(url):
|
|
102
|
+
path = s.folder[: -len(url)]
|
|
103
|
+
return StaticFolder(url, path, False)
|
|
104
|
+
return StaticFolder(url, s.folder, s.rewrite if not prefix else True)
|
|
105
|
+
|
|
106
|
+
if app is None:
|
|
107
|
+
app = find_application(
|
|
108
|
+
application_dir,
|
|
109
|
+
get_app_entrypoint(application_dir, entrypoint),
|
|
110
|
+
)
|
|
111
|
+
return [fixstatic(s) for s in get_static_folders(app)]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def find_application(application_dir: str, module: str) -> Flask:
|
|
115
|
+
import sys
|
|
116
|
+
from importlib import import_module
|
|
117
|
+
|
|
118
|
+
remove = False
|
|
119
|
+
if application_dir not in sys.path:
|
|
120
|
+
sys.path.append(application_dir)
|
|
121
|
+
remove = True
|
|
122
|
+
try:
|
|
123
|
+
# FIXME: we really want to run this
|
|
124
|
+
# under the virtual environment that this pertains too
|
|
125
|
+
venv = sys.prefix
|
|
126
|
+
secho(
|
|
127
|
+
f"trying to load application ({module}) using {venv}: ",
|
|
128
|
+
fg="yellow",
|
|
129
|
+
nl=False,
|
|
130
|
+
err=True,
|
|
131
|
+
)
|
|
132
|
+
with redirect_stderr(StringIO()) as stderr:
|
|
133
|
+
m = import_module(module)
|
|
134
|
+
app = m.application # type: ignore
|
|
135
|
+
v = stderr.getvalue()
|
|
136
|
+
if v:
|
|
137
|
+
secho(f"got possible errors ...{style(v[-100:], fg='red')}", err=True)
|
|
138
|
+
else:
|
|
139
|
+
secho("ok", fg="green", err=True)
|
|
140
|
+
return cast("Flask", app)
|
|
141
|
+
except (ImportError, AttributeError) as e:
|
|
142
|
+
raise BadParameter(f"can't load application from {application_dir}: {e}") from e
|
|
143
|
+
finally:
|
|
144
|
+
if remove:
|
|
145
|
+
sys.path.remove(application_dir)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_dot_env(fname: str) -> dict[str, str | None] | None:
|
|
149
|
+
try:
|
|
150
|
+
from dotenv import dotenv_values # type: ignore
|
|
151
|
+
|
|
152
|
+
return dotenv_values(fname)
|
|
153
|
+
except ImportError:
|
|
154
|
+
import click
|
|
155
|
+
|
|
156
|
+
click.secho(
|
|
157
|
+
'".flaskenv" file detected but no python-dotenv module found',
|
|
158
|
+
fg="yellow",
|
|
159
|
+
bold=True,
|
|
160
|
+
err=True,
|
|
161
|
+
)
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_app_entrypoint(application_dir: str, default: str = "app.app") -> str:
|
|
166
|
+
app = os.environ.get("FLASK_APP")
|
|
167
|
+
if app is not None:
|
|
168
|
+
return app
|
|
169
|
+
dot = os.path.join(application_dir, ".flaskenv")
|
|
170
|
+
if isfile(dot):
|
|
171
|
+
cfg = get_dot_env(dot)
|
|
172
|
+
if cfg is None:
|
|
173
|
+
return default
|
|
174
|
+
app = cfg.get("FLASK_APP")
|
|
175
|
+
if app is not None:
|
|
176
|
+
return app
|
|
177
|
+
return default
|
flask_nginx/irds.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from getpass import getuser
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from .cli import cli
|
|
9
|
+
from .systemd.systemd import systemd
|
|
10
|
+
from .systemd.utils import make_args
|
|
11
|
+
from .utils import get_pass
|
|
12
|
+
from .utils import which
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def mount_irds(path_str: str, user: str | None = None) -> int:
|
|
16
|
+
from .config import get_config
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
sudo = which("sudo")
|
|
21
|
+
mount = which("mount")
|
|
22
|
+
|
|
23
|
+
path = Path(path_str).expanduser().absolute()
|
|
24
|
+
if not path.exists():
|
|
25
|
+
path.mkdir(exist_ok=True, parents=True)
|
|
26
|
+
|
|
27
|
+
datastore = path / "datastore"
|
|
28
|
+
if datastore.exists():
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
if user is None:
|
|
32
|
+
user = getuser()
|
|
33
|
+
uid = os.getuid()
|
|
34
|
+
gid = os.getgid()
|
|
35
|
+
pheme = get_pass("PHEME", f"user {user} pheme")
|
|
36
|
+
cmd = [
|
|
37
|
+
sudo,
|
|
38
|
+
mount,
|
|
39
|
+
"-t",
|
|
40
|
+
"cifs",
|
|
41
|
+
"-o",
|
|
42
|
+
f"user={user}",
|
|
43
|
+
"-o",
|
|
44
|
+
f"pass={pheme}",
|
|
45
|
+
"-o",
|
|
46
|
+
f"uid={uid},gid={gid},forceuid,forcegid",
|
|
47
|
+
get_config().datastore,
|
|
48
|
+
str(path),
|
|
49
|
+
]
|
|
50
|
+
pmount = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
51
|
+
returncode = pmount.wait()
|
|
52
|
+
return returncode
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@cli.group(help=click.style("IRDS commands", fg="magenta"))
|
|
56
|
+
def irds() -> None:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@irds.command(name="mount")
|
|
61
|
+
@click.argument("directory")
|
|
62
|
+
@click.argument("user", required=False)
|
|
63
|
+
def mount_irds_cmd(directory: str, user: str | None) -> None:
|
|
64
|
+
"""Mount IRDS datastore."""
|
|
65
|
+
|
|
66
|
+
returncode = mount_irds(directory, user=user)
|
|
67
|
+
if returncode != 0:
|
|
68
|
+
click.secho("can't mound irds", fg="red")
|
|
69
|
+
raise click.Abort()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
MOUNT_ARGS = {
|
|
73
|
+
"mount_dir": "locations of repo",
|
|
74
|
+
"user": "user to run as [default: current user]",
|
|
75
|
+
"version": "SMB version [default: 3.0]",
|
|
76
|
+
"credentials": "file containg PHEME password as a line: password={pw}"
|
|
77
|
+
" (no spaces)\nroot owned with permission 600",
|
|
78
|
+
"password": "PHEME password",
|
|
79
|
+
"drive": "IRDS drive to mount",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
MOUNT_HELP = f"""
|
|
83
|
+
Generate a systemd mount file for a IRDS.
|
|
84
|
+
|
|
85
|
+
Use footprint irds systemd path/to/mount_dir ... etc.
|
|
86
|
+
with the following arguments:
|
|
87
|
+
|
|
88
|
+
\b
|
|
89
|
+
{make_args(MOUNT_ARGS)}
|
|
90
|
+
\b
|
|
91
|
+
example:
|
|
92
|
+
\b
|
|
93
|
+
footprint irds systemd ~/irds user=00033472
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@irds.command(name="systemd", help=MOUNT_HELP)
|
|
98
|
+
@click.option("-i", "--ignore-unknowns", is_flag=True, help="ignore unknown variables")
|
|
99
|
+
@click.option(
|
|
100
|
+
"-c",
|
|
101
|
+
"--credentials",
|
|
102
|
+
type=click.Path(file_okay=True, dir_okay=False, exists=True),
|
|
103
|
+
help="credentials file for CIFS",
|
|
104
|
+
)
|
|
105
|
+
@click.option("-t", "--template", metavar="TEMPLATE_FILE", help="template file")
|
|
106
|
+
@click.option("-n", "--no-check", is_flag=True, help="don't check parameters")
|
|
107
|
+
@click.argument(
|
|
108
|
+
"mount_dir",
|
|
109
|
+
type=click.Path(exists=True, dir_okay=True, file_okay=False),
|
|
110
|
+
required=True,
|
|
111
|
+
)
|
|
112
|
+
@click.argument("params", nargs=-1)
|
|
113
|
+
def systemd_mount_cmd(
|
|
114
|
+
mount_dir: str,
|
|
115
|
+
params: list[str],
|
|
116
|
+
template: str | None,
|
|
117
|
+
no_check: bool,
|
|
118
|
+
ignore_unknowns: bool,
|
|
119
|
+
credentials: str | None,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Generate a systemd unit file to mount IRDS.
|
|
122
|
+
|
|
123
|
+
PARAMS are key=value arguments for the template.
|
|
124
|
+
"""
|
|
125
|
+
import os
|
|
126
|
+
from getpass import getpass
|
|
127
|
+
|
|
128
|
+
from .config import get_config
|
|
129
|
+
|
|
130
|
+
params = list(params)
|
|
131
|
+
|
|
132
|
+
Config = get_config()
|
|
133
|
+
|
|
134
|
+
mount_dir = mount_dir or "."
|
|
135
|
+
mount_dir = os.path.abspath(os.path.expanduser(mount_dir))
|
|
136
|
+
|
|
137
|
+
def isadir(d: str) -> str | None:
|
|
138
|
+
return None if os.path.isdir(d) else f"{d}: not a directory"
|
|
139
|
+
|
|
140
|
+
def isafile(d: str) -> str | None:
|
|
141
|
+
return None if os.path.isfile(d) else f"{d}: not a file"
|
|
142
|
+
|
|
143
|
+
se = which("systemd-escape")
|
|
144
|
+
filename = subprocess.check_output(
|
|
145
|
+
[se, "-p", "--suffix=mount", mount_dir],
|
|
146
|
+
text=True,
|
|
147
|
+
).strip()
|
|
148
|
+
|
|
149
|
+
if credentials is not None:
|
|
150
|
+
params.append(f"credentials={os.path.expanduser(credentials)}")
|
|
151
|
+
|
|
152
|
+
systemd(
|
|
153
|
+
template or "systemd.mount",
|
|
154
|
+
mount_dir,
|
|
155
|
+
params,
|
|
156
|
+
help_args=MOUNT_ARGS,
|
|
157
|
+
check=not no_check,
|
|
158
|
+
output=filename,
|
|
159
|
+
ignore_unknowns=ignore_unknowns,
|
|
160
|
+
checks=[
|
|
161
|
+
(
|
|
162
|
+
"mount_dir",
|
|
163
|
+
lambda _, v: isadir(v),
|
|
164
|
+
),
|
|
165
|
+
("credentials", lambda _, v: isafile(v)),
|
|
166
|
+
],
|
|
167
|
+
default_values=[
|
|
168
|
+
("uid", lambda _: str(os.getuid())),
|
|
169
|
+
("gid", lambda _: str(os.getgid())),
|
|
170
|
+
("drive", lambda _: Config.datastore),
|
|
171
|
+
(
|
|
172
|
+
"password",
|
|
173
|
+
lambda params: (
|
|
174
|
+
getpass(f"PHEME password for {params['user']}: ")
|
|
175
|
+
if "credentials" not in params
|
|
176
|
+
else None
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
],
|
|
180
|
+
)
|
|
181
|
+
msg = click.style(f"footprint config systemd-install {filename}", fg="green")
|
|
182
|
+
click.echo(f'use: "{msg}" to install')
|
flask_nginx/logo.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .cli import cli
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def img2ico(png: str, out: str) -> None:
|
|
11
|
+
from PIL import Image # type: ignore
|
|
12
|
+
|
|
13
|
+
with open(png, "rb") as fp:
|
|
14
|
+
im = Image.open(fp)
|
|
15
|
+
|
|
16
|
+
im.thumbnail((128, 128), Image.ANTIALIAS) # type: ignore
|
|
17
|
+
|
|
18
|
+
size_tuples = [ # (256, 256),
|
|
19
|
+
(128, 128),
|
|
20
|
+
(64, 64),
|
|
21
|
+
(48, 48),
|
|
22
|
+
(32, 32),
|
|
23
|
+
(24, 24),
|
|
24
|
+
(16, 16),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
im.save(out, sizes=size_tuples)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@cli.command()
|
|
31
|
+
@click.option("-o", "--output", help="output filename")
|
|
32
|
+
@click.argument(
|
|
33
|
+
"image",
|
|
34
|
+
nargs=1,
|
|
35
|
+
type=click.Path(exists=True, dir_okay=False, file_okay=True),
|
|
36
|
+
)
|
|
37
|
+
def img_to_ico(image: str, output: str | None) -> None:
|
|
38
|
+
"Convert a image file to an .ico file [**requires Pillow**]."
|
|
39
|
+
from .utils import require_mod
|
|
40
|
+
|
|
41
|
+
require_mod("PIL", "Pillow")
|
|
42
|
+
|
|
43
|
+
# see https://anaconda.org/conda-forge/svg2png
|
|
44
|
+
if output is None:
|
|
45
|
+
out, _ = os.path.splitext(image)
|
|
46
|
+
output = out + ".ico"
|
|
47
|
+
img2ico(image, output)
|
flask_nginx/mailer.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import smtplib
|
|
4
|
+
from email.mime.text import MIMEText
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from .cli import cli
|
|
9
|
+
|
|
10
|
+
# from email.mime.image import MIMEImage
|
|
11
|
+
# from email.mime.multipart import MIMEMultipart
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def sendmail(
|
|
15
|
+
html: str,
|
|
16
|
+
you: str,
|
|
17
|
+
me: str = "footprint@uwa.edu.au",
|
|
18
|
+
mailhost: str | None = None,
|
|
19
|
+
subject: str = "footprint monitor",
|
|
20
|
+
) -> None:
|
|
21
|
+
from .config import get_config
|
|
22
|
+
|
|
23
|
+
if mailhost is None:
|
|
24
|
+
mailhost = get_config().mailhost
|
|
25
|
+
msg = MIMEText(html, "html")
|
|
26
|
+
|
|
27
|
+
msg["Subject"] = subject
|
|
28
|
+
msg["From"] = me
|
|
29
|
+
msg["To"] = you
|
|
30
|
+
|
|
31
|
+
with smtplib.SMTP() as s:
|
|
32
|
+
s.connect(mailhost)
|
|
33
|
+
s.sendmail(me, [you], msg.as_string())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cli.command()
|
|
37
|
+
@click.option("-m", "--mailhost", help="mail host to use [default from config]")
|
|
38
|
+
@click.argument("email")
|
|
39
|
+
@click.argument("message", nargs=-1)
|
|
40
|
+
def email_test(email: str, message: list[str], mailhost: str | None) -> None:
|
|
41
|
+
"""Test email setup from this host"""
|
|
42
|
+
import platform
|
|
43
|
+
|
|
44
|
+
if not message:
|
|
45
|
+
raise click.BadArgumentUsage("no message")
|
|
46
|
+
|
|
47
|
+
sendmail(
|
|
48
|
+
" ".join(message),
|
|
49
|
+
you=email,
|
|
50
|
+
mailhost=mailhost,
|
|
51
|
+
subject=f"Message from footprint on {platform.node()}",
|
|
52
|
+
)
|
|
53
|
+
click.secho("message sent!", fg="green", bold=True)
|