fujin-cli 0.2.0__py3-none-any.whl → 0.3.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.
Potentially problematic release.
This version of fujin-cli might be problematic. Click here for more details.
- fujin/__main__.py +22 -4
- fujin/commands/__init__.py +1 -2
- fujin/commands/_base.py +34 -41
- fujin/commands/app.py +78 -10
- fujin/commands/config.py +16 -125
- fujin/commands/deploy.py +93 -31
- fujin/commands/docs.py +16 -0
- fujin/commands/down.py +33 -15
- fujin/commands/init.py +82 -0
- fujin/commands/proxy.py +71 -0
- fujin/commands/prune.py +42 -0
- fujin/commands/redeploy.py +39 -10
- fujin/commands/rollback.py +49 -0
- fujin/commands/secrets.py +11 -0
- fujin/commands/server.py +65 -30
- fujin/commands/up.py +4 -5
- fujin/config.py +186 -27
- fujin/connection.py +75 -0
- fujin/hooks.py +48 -8
- fujin/process_managers/__init__.py +16 -5
- fujin/process_managers/systemd.py +98 -48
- fujin/proxies/__init__.py +23 -6
- fujin/proxies/caddy.py +192 -30
- fujin/proxies/dummy.py +16 -3
- fujin/proxies/nginx.py +109 -36
- fujin/templates/simple.service +14 -0
- fujin/templates/web.service +12 -11
- fujin/templates/web.socket +2 -2
- fujin_cli-0.3.0.dist-info/METADATA +66 -0
- fujin_cli-0.3.0.dist-info/RECORD +35 -0
- fujin/host.py +0 -85
- fujin/templates/other.service +0 -15
- fujin_cli-0.2.0.dist-info/METADATA +0 -52
- fujin_cli-0.2.0.dist-info/RECORD +0 -29
- {fujin_cli-0.2.0.dist-info → fujin_cli-0.3.0.dist-info}/WHEEL +0 -0
- {fujin_cli-0.2.0.dist-info → fujin_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {fujin_cli-0.2.0.dist-info → fujin_cli-0.3.0.dist-info}/licenses/LICENSE.txt +0 -0
fujin/commands/down.py
CHANGED
|
@@ -1,30 +1,48 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from typing import Annotated
|
|
4
5
|
|
|
5
6
|
import cappa
|
|
6
|
-
from rich.prompt import
|
|
7
|
+
from rich.prompt import Confirm
|
|
7
8
|
|
|
8
|
-
from fujin.commands import
|
|
9
|
+
from fujin.commands import BaseCommand
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@cappa.command(
|
|
12
13
|
help="Tear down the project by stopping services and cleaning up resources"
|
|
13
14
|
)
|
|
14
15
|
@dataclass
|
|
15
|
-
class Down(
|
|
16
|
+
class Down(BaseCommand):
|
|
17
|
+
full: Annotated[
|
|
18
|
+
bool,
|
|
19
|
+
cappa.Arg(
|
|
20
|
+
short="-f",
|
|
21
|
+
long="--full",
|
|
22
|
+
help="Stop and uninstall proxy as part of teardown",
|
|
23
|
+
),
|
|
24
|
+
] = False
|
|
16
25
|
|
|
17
26
|
def __call__(self):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
try:
|
|
28
|
+
confirm = Confirm.ask(
|
|
29
|
+
f"""[red]You are about to delete all project files, stop all services, and remove all configurations on the host {self.config.host.ip} for the project {self.config.app_name}. Any assets in your project folder will be lost (sqlite not in there ?). Are you sure you want to proceed? This action is irreversible.[/red]""",
|
|
30
|
+
)
|
|
31
|
+
except KeyboardInterrupt as e:
|
|
32
|
+
raise cappa.Exit("Teardown aborted", code=0)
|
|
33
|
+
if not confirm:
|
|
24
34
|
return
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
with self.connection() as conn:
|
|
36
|
+
hook_manager = self.create_hook_manager(conn)
|
|
37
|
+
hook_manager.pre_teardown()
|
|
38
|
+
process_manager = self.create_process_manager(conn)
|
|
39
|
+
conn.run(f"rm -rf {self.app_dir}")
|
|
40
|
+
self.create_web_proxy(conn).teardown()
|
|
41
|
+
process_manager.uninstall_services()
|
|
42
|
+
process_manager.reload_configuration()
|
|
43
|
+
if self.full:
|
|
44
|
+
self.create_web_proxy(conn).uninstall()
|
|
45
|
+
hook_manager.post_teardown()
|
|
46
|
+
self.stdout.output(
|
|
47
|
+
"[green]Project teardown completed successfully![/green]"
|
|
48
|
+
)
|
fujin/commands/init.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import cappa
|
|
7
|
+
import tomli_w
|
|
8
|
+
|
|
9
|
+
from fujin.commands import BaseCommand
|
|
10
|
+
from fujin.config import tomllib
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@cappa.command(help="Generate a sample configuration file")
|
|
14
|
+
class Init(BaseCommand):
|
|
15
|
+
profile: Annotated[
|
|
16
|
+
str, cappa.Arg(choices=["simple", "falco"], short="-p", long="--profile")
|
|
17
|
+
] = "simple"
|
|
18
|
+
|
|
19
|
+
def __call__(self):
|
|
20
|
+
fujin_toml = Path("fujin.toml")
|
|
21
|
+
if fujin_toml.exists():
|
|
22
|
+
raise cappa.Exit("fujin.toml file already exists", code=1)
|
|
23
|
+
profile_to_func = {"simple": simple_config, "falco": falco_config}
|
|
24
|
+
app_name = Path().resolve().stem.replace("-", "_").replace(" ", "_").lower()
|
|
25
|
+
config = profile_to_func[self.profile](app_name)
|
|
26
|
+
if not Path(".python-version").exists():
|
|
27
|
+
config["python_version"] = "3.12"
|
|
28
|
+
pyproject_toml = Path("pyproject.toml")
|
|
29
|
+
if pyproject_toml.exists():
|
|
30
|
+
pyproject = tomllib.loads(pyproject_toml.read_text())
|
|
31
|
+
config["app"] = pyproject.get("project", {}).get("name", app_name)
|
|
32
|
+
if pyproject.get("project", {}).get("version"):
|
|
33
|
+
# fujin will read the version itself from the pyproject
|
|
34
|
+
config.pop("version")
|
|
35
|
+
fujin_toml.write_text(tomli_w.dumps(config))
|
|
36
|
+
self.stdout.output(
|
|
37
|
+
"[green]Sample configuration file generated successfully![/green]"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def simple_config(app_name) -> dict:
|
|
42
|
+
return {
|
|
43
|
+
"app": app_name,
|
|
44
|
+
"version": "0.1.0",
|
|
45
|
+
"build_command": "uv build && uv pip compile pyproject.toml -o requirements.txt",
|
|
46
|
+
"distfile": f"dist/{app_name}-{{version}}-py3-none-any.whl",
|
|
47
|
+
"webserver": {
|
|
48
|
+
"upstream": "localhost:8000",
|
|
49
|
+
"type": "fujin.proxies.caddy",
|
|
50
|
+
},
|
|
51
|
+
"release_command": f"{app_name} migrate",
|
|
52
|
+
"processes": {
|
|
53
|
+
"web": f".venv/bin/gunicorn {app_name}.wsgi:app --bind 0.0.0.0:8000"
|
|
54
|
+
},
|
|
55
|
+
"aliases": {"shell": "server exec --appenv -i bash"},
|
|
56
|
+
"host": {
|
|
57
|
+
"ip": "127.0.0.1",
|
|
58
|
+
"user": "root",
|
|
59
|
+
"domain_name": f"{app_name}.com",
|
|
60
|
+
"envfile": ".env.prod",
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def falco_config(app_name: str) -> dict:
|
|
66
|
+
config = simple_config(app_name)
|
|
67
|
+
config.update(
|
|
68
|
+
{
|
|
69
|
+
"release_command": f"{config['app']} setup",
|
|
70
|
+
"processes": {
|
|
71
|
+
"web": f".venv/bin/{config['app']} prodserver",
|
|
72
|
+
"worker": f".venv/bin/{config['app']} qcluster",
|
|
73
|
+
},
|
|
74
|
+
"aliases": {
|
|
75
|
+
"console": "app exec -i shell_plus",
|
|
76
|
+
"dbconsole": "app exec -i dbshell",
|
|
77
|
+
"print_settings": "app exec print_settings --format=pprint",
|
|
78
|
+
"shell": "server exec --appenv -i bash",
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
return config
|
fujin/commands/proxy.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import cappa
|
|
5
|
+
|
|
6
|
+
from fujin.commands import BaseCommand
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@cappa.command(help="Manage web proxy.")
|
|
10
|
+
@dataclass
|
|
11
|
+
class Proxy(BaseCommand):
|
|
12
|
+
@cappa.command(help="Install the proxy on the remote host")
|
|
13
|
+
def install(self):
|
|
14
|
+
with self.connection() as conn:
|
|
15
|
+
self.create_web_proxy(conn).install()
|
|
16
|
+
|
|
17
|
+
@cappa.command(help="Uninstall the proxy from the remote host")
|
|
18
|
+
def uninstall(self):
|
|
19
|
+
with self.connection() as conn:
|
|
20
|
+
self.create_web_proxy(conn).uninstall()
|
|
21
|
+
|
|
22
|
+
@cappa.command(help="Start the proxy on the remote host")
|
|
23
|
+
def start(self):
|
|
24
|
+
with self.connection() as conn:
|
|
25
|
+
self.create_web_proxy(conn).start()
|
|
26
|
+
self.stdout.output("[green]Proxy started successfully![/green]")
|
|
27
|
+
|
|
28
|
+
@cappa.command(help="Stop the proxy on the remote host")
|
|
29
|
+
def stop(self):
|
|
30
|
+
with self.connection() as conn:
|
|
31
|
+
self.create_web_proxy(conn).stop()
|
|
32
|
+
self.stdout.output("[green]Proxy stopped successfully![/green]")
|
|
33
|
+
|
|
34
|
+
@cappa.command(help="Restart the proxy on the remote host")
|
|
35
|
+
def restart(self):
|
|
36
|
+
with self.connection() as conn:
|
|
37
|
+
self.create_web_proxy(conn).restart()
|
|
38
|
+
self.stdout.output("[green]Proxy restarted successfully![/green]")
|
|
39
|
+
|
|
40
|
+
@cappa.command(help="Check the status of the proxy on the remote host")
|
|
41
|
+
def status(self):
|
|
42
|
+
with self.connection() as conn:
|
|
43
|
+
self.create_web_proxy(conn).status()
|
|
44
|
+
|
|
45
|
+
@cappa.command(help="View the logs of the proxy on the remote host")
|
|
46
|
+
def logs(self):
|
|
47
|
+
with self.connection() as conn:
|
|
48
|
+
self.create_web_proxy(conn).logs()
|
|
49
|
+
|
|
50
|
+
@cappa.command(
|
|
51
|
+
name="export-config",
|
|
52
|
+
help="Export the proxy configuration file locally to the .fujin directory",
|
|
53
|
+
)
|
|
54
|
+
def export_config(
|
|
55
|
+
self,
|
|
56
|
+
overwrite: Annotated[
|
|
57
|
+
bool, cappa.Arg(help="overwrite any existing config file")
|
|
58
|
+
] = False,
|
|
59
|
+
):
|
|
60
|
+
with self.connection() as conn:
|
|
61
|
+
proxy = self.create_web_proxy(conn)
|
|
62
|
+
if proxy.config_file.exists() and not overwrite:
|
|
63
|
+
self.stdout.output(
|
|
64
|
+
f"[blue]{proxy.config_file} already exists, use --overwrite to overwrite it content.[/blue]"
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
self.config.local_config_dir.mkdir(exist_ok=True)
|
|
68
|
+
proxy.export_config()
|
|
69
|
+
self.stdout.output(
|
|
70
|
+
f"[green]Config file successfully to {proxy.config_file}[/green]"
|
|
71
|
+
)
|
fujin/commands/prune.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import cappa
|
|
5
|
+
from rich.prompt import Confirm
|
|
6
|
+
|
|
7
|
+
from fujin.commands import BaseCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@cappa.command(
|
|
11
|
+
help="Prune old version artifacts, keeping only the specified number of recent versions"
|
|
12
|
+
)
|
|
13
|
+
@dataclass
|
|
14
|
+
class Prune(BaseCommand):
|
|
15
|
+
keep: Annotated[
|
|
16
|
+
int,
|
|
17
|
+
cappa.Arg(
|
|
18
|
+
short="-k",
|
|
19
|
+
long="--keep",
|
|
20
|
+
help="Number of version artifacts to retain (minimum 1)",
|
|
21
|
+
),
|
|
22
|
+
] = 2
|
|
23
|
+
|
|
24
|
+
def __call__(self):
|
|
25
|
+
if self.keep < 1:
|
|
26
|
+
raise cappa.Exit("The minimum value for the --keep option is 1", code=1)
|
|
27
|
+
with self.connection() as conn, conn.cd(self.app_dir):
|
|
28
|
+
result = conn.run(
|
|
29
|
+
f"sed -n '{self.keep + 1},$p' .versions", hide=True
|
|
30
|
+
).stdout.strip()
|
|
31
|
+
result_list = result.split("\n")
|
|
32
|
+
if result == "":
|
|
33
|
+
self.stdout.output("[blue]No versions to prune[/blue]")
|
|
34
|
+
return
|
|
35
|
+
if not Confirm.ask(
|
|
36
|
+
f"[red]The following versions will be permanently deleted: {', '.join(result_list)}. This action is irreversible. Are you sure you want to proceed?[/red]"
|
|
37
|
+
):
|
|
38
|
+
return
|
|
39
|
+
to_prune = [f"{self.app_dir}/v{v}" for v in result_list]
|
|
40
|
+
conn.run(f"rm -r {' '.join(to_prune)}", warn=True)
|
|
41
|
+
conn.run(f"sed -i '{self.keep + 1},$d' .versions", warn=True)
|
|
42
|
+
self.stdout.output("[green]Pruning completed successfully[/green]")
|
fujin/commands/redeploy.py
CHANGED
|
@@ -1,19 +1,48 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import hashlib
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import cappa
|
|
6
6
|
|
|
7
|
+
from fujin.commands import BaseCommand
|
|
7
8
|
from .deploy import Deploy
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@cappa.command(help="Redeploy the application to apply code and environment changes")
|
|
11
|
-
class Redeploy(
|
|
12
|
-
|
|
12
|
+
class Redeploy(BaseCommand):
|
|
13
13
|
def __call__(self):
|
|
14
|
-
deploy = Deploy(
|
|
15
|
-
deploy.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
self.
|
|
14
|
+
deploy = Deploy()
|
|
15
|
+
deploy.build_app()
|
|
16
|
+
local_requirements = hashlib.md5(
|
|
17
|
+
self.config.requirements.read_bytes()
|
|
18
|
+
).hexdigest()
|
|
19
|
+
with self.app_environment() as conn:
|
|
20
|
+
hook_manager = self.create_hook_manager(conn)
|
|
21
|
+
hook_manager.pre_deploy()
|
|
22
|
+
current_host_version = conn.run(
|
|
23
|
+
"head -n 1 .versions", warn=True, hide=True
|
|
24
|
+
).stdout.strip()
|
|
25
|
+
try:
|
|
26
|
+
host_requirements = (
|
|
27
|
+
conn.run(
|
|
28
|
+
f"md5sum v{current_host_version}/requirements.txt",
|
|
29
|
+
warn=True,
|
|
30
|
+
hide=True,
|
|
31
|
+
)
|
|
32
|
+
.stdout.strip()
|
|
33
|
+
.split()[0]
|
|
34
|
+
)
|
|
35
|
+
skip_requirements = host_requirements == local_requirements
|
|
36
|
+
except IndexError:
|
|
37
|
+
skip_requirements = False
|
|
38
|
+
deploy.transfer_files(conn, skip_requirements=skip_requirements)
|
|
39
|
+
if skip_requirements and current_host_version != self.config.version:
|
|
40
|
+
conn.run(
|
|
41
|
+
f"cp v{current_host_version}/requirements.txt {deploy.versioned_assets_dir}/requirements.txt "
|
|
42
|
+
)
|
|
43
|
+
deploy.install_project(conn, skip_setup=skip_requirements)
|
|
44
|
+
deploy.release(conn)
|
|
45
|
+
self.create_process_manager(conn).restart_services()
|
|
46
|
+
deploy.update_version_history(conn)
|
|
47
|
+
hook_manager.post_deploy()
|
|
48
|
+
self.stdout.output("[green]Redeployment completed successfully![/green]")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import cappa
|
|
4
|
+
from rich.prompt import Prompt, Confirm
|
|
5
|
+
|
|
6
|
+
from fujin.commands import BaseCommand
|
|
7
|
+
from fujin.commands.deploy import Deploy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@cappa.command(help="Rollback application to a previous version")
|
|
11
|
+
@dataclass
|
|
12
|
+
class Rollback(BaseCommand):
|
|
13
|
+
def __call__(self):
|
|
14
|
+
with self.app_environment() as conn:
|
|
15
|
+
result = conn.run(
|
|
16
|
+
"sed -n '2,$p' .versions", warn=True, hide=True
|
|
17
|
+
).stdout.strip()
|
|
18
|
+
if not result:
|
|
19
|
+
self.stdout.output("[blue]No rollback targets available")
|
|
20
|
+
return
|
|
21
|
+
versions: list[str] = result.split("\n")
|
|
22
|
+
try:
|
|
23
|
+
version = Prompt.ask(
|
|
24
|
+
"Enter the version you want to rollback to:",
|
|
25
|
+
choices=versions,
|
|
26
|
+
default=versions[0],
|
|
27
|
+
)
|
|
28
|
+
except KeyboardInterrupt as e:
|
|
29
|
+
raise cappa.Exit("Rollback aborted by user.", code=0) from e
|
|
30
|
+
|
|
31
|
+
current_app_version = conn.run(
|
|
32
|
+
"head -n 1 .versions", warn=True, hide=True
|
|
33
|
+
).stdout.strip()
|
|
34
|
+
versions_to_clean = [current_app_version] + versions[
|
|
35
|
+
: versions.index(version)
|
|
36
|
+
]
|
|
37
|
+
confirm = Confirm.ask(
|
|
38
|
+
f"[blue]Rolling back to v{version} will permanently delete versions {', '.join(versions_to_clean)}. This action is irreversible. Are you sure you want to proceed?[/blue]"
|
|
39
|
+
)
|
|
40
|
+
if not confirm:
|
|
41
|
+
return
|
|
42
|
+
deploy = Deploy()
|
|
43
|
+
deploy.install_project(conn, version)
|
|
44
|
+
self.create_process_manager(conn).restart_services()
|
|
45
|
+
conn.run(f"rm -r {' '.join(f'v{v}' for v in versions_to_clean)}", warn=True)
|
|
46
|
+
conn.run(f"sed -i '1,/{version}/{{/{version}/!d}}' .versions", warn=True)
|
|
47
|
+
self.stdout.output(
|
|
48
|
+
f"[green]Rollback to version {version} from {current_app_version} completed successfully![/green]"
|
|
49
|
+
)
|
fujin/commands/server.py
CHANGED
|
@@ -1,30 +1,47 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import secrets
|
|
4
|
+
from functools import partial
|
|
3
5
|
from typing import Annotated
|
|
4
6
|
|
|
5
7
|
import cappa
|
|
6
8
|
|
|
7
|
-
from fujin.commands import
|
|
9
|
+
from fujin.commands import BaseCommand
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
@cappa.command(help="Manage server operations")
|
|
11
|
-
class Server(
|
|
12
|
-
|
|
13
|
+
class Server(BaseCommand):
|
|
13
14
|
@cappa.command(help="Display information about the host system")
|
|
14
15
|
def info(self):
|
|
15
|
-
self.
|
|
16
|
+
with self.connection() as conn:
|
|
17
|
+
result = conn.run(f"command -v fastfetch", warn=True, hide=True)
|
|
18
|
+
if result.ok:
|
|
19
|
+
conn.run("fastfetch", pty=True)
|
|
20
|
+
else:
|
|
21
|
+
self.stdout.output(conn.run("cat /etc/os-release", hide=True).stdout)
|
|
16
22
|
|
|
17
23
|
@cappa.command(help="Setup uv, web proxy, and install necessary dependencies")
|
|
18
24
|
def bootstrap(self):
|
|
19
|
-
self.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
with self.connection() as conn:
|
|
26
|
+
hook_manager = self.create_hook_manager(conn)
|
|
27
|
+
hook_manager.pre_bootstrap()
|
|
28
|
+
conn.run("sudo apt update && sudo apt upgrade -y", pty=True)
|
|
29
|
+
conn.run("sudo apt install -y sqlite3 curl rsync", pty=True)
|
|
30
|
+
result = conn.run("command -v uv", warn=True)
|
|
31
|
+
if not result.ok:
|
|
32
|
+
conn.run("curl -LsSf https://astral.sh/uv/install.sh | sh")
|
|
33
|
+
conn.run("uv tool update-shell")
|
|
34
|
+
conn.run("uv tool install fastfetch-bin-edge")
|
|
35
|
+
self.create_web_proxy(conn).install()
|
|
36
|
+
hook_manager.post_bootstrap()
|
|
37
|
+
self.stdout.output(
|
|
38
|
+
"[green]Server bootstrap completed successfully![/green]"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@cappa.command(help="Stop and uninstall the web proxy")
|
|
42
|
+
def uninstall_proxy(self):
|
|
43
|
+
with self.connection() as conn:
|
|
44
|
+
self.create_web_proxy(conn).uninstall()
|
|
28
45
|
|
|
29
46
|
@cappa.command(
|
|
30
47
|
help="Execute an arbitrary command on the server, optionally in interactive mode"
|
|
@@ -33,25 +50,43 @@ class Server(AppCommand):
|
|
|
33
50
|
self,
|
|
34
51
|
command: str,
|
|
35
52
|
interactive: Annotated[bool, cappa.Arg(default=False, short="-i")],
|
|
53
|
+
appenv: Annotated[
|
|
54
|
+
bool,
|
|
55
|
+
cappa.Arg(
|
|
56
|
+
default=False,
|
|
57
|
+
long="--appenv",
|
|
58
|
+
help="Change to app directory and enable app environment",
|
|
59
|
+
),
|
|
60
|
+
],
|
|
36
61
|
):
|
|
37
|
-
if
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
context = self.app_environment() if appenv else self.connection()
|
|
63
|
+
with context as conn:
|
|
64
|
+
if interactive:
|
|
65
|
+
conn.run(command, pty=interactive, warn=True)
|
|
66
|
+
else:
|
|
67
|
+
self.stdout.output(conn.run(command, hide=True).stdout)
|
|
42
68
|
|
|
43
69
|
@cappa.command(
|
|
44
70
|
name="create-user", help="Create a new user with sudo and ssh access"
|
|
45
71
|
)
|
|
46
|
-
def create_user(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
def create_user(
|
|
73
|
+
self,
|
|
74
|
+
name: str,
|
|
75
|
+
with_password: Annotated[bool, cappa.Arg(long="--with-password")] = False,
|
|
76
|
+
):
|
|
77
|
+
with self.connection() as conn:
|
|
78
|
+
run_pty = partial(conn.run, pty=True)
|
|
79
|
+
run_pty(
|
|
80
|
+
f"sudo adduser --disabled-password --gecos '' {name}",
|
|
81
|
+
)
|
|
82
|
+
run_pty(f"sudo mkdir -p /home/{name}/.ssh")
|
|
83
|
+
run_pty(f"sudo cp ~/.ssh/authorized_keys /home/{name}/.ssh/")
|
|
84
|
+
run_pty(f"sudo chown -R {name}:{name} /home/{name}/.ssh")
|
|
85
|
+
if with_password:
|
|
86
|
+
password = secrets.token_hex(8)
|
|
87
|
+
run_pty(f"echo '{name}:{password}' | sudo chpasswd")
|
|
88
|
+
self.stdout.output(f"[green]Generated password: [/green]{password}")
|
|
89
|
+
run_pty(f"sudo chmod 700 /home/{name}/.ssh")
|
|
90
|
+
run_pty(f"sudo chmod 600 /home/{name}/.ssh/authorized_keys")
|
|
91
|
+
run_pty(f"echo '{name} ALL=(ALL) NOPASSWD:ALL' | sudo tee -a /etc/sudoers")
|
|
92
|
+
self.stdout.output(f"[green]New user {name} created successfully![/green]")
|
fujin/commands/up.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import cappa
|
|
2
|
+
from fujin.commands import BaseCommand
|
|
2
3
|
|
|
3
|
-
from fujin.commands import AppCommand
|
|
4
4
|
from .deploy import Deploy
|
|
5
5
|
from .server import Server
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@cappa.command(help="Run everything required to deploy an application to a fresh host.")
|
|
9
|
-
class Up(
|
|
10
|
-
|
|
9
|
+
class Up(BaseCommand):
|
|
11
10
|
def __call__(self):
|
|
12
|
-
Server(
|
|
13
|
-
Deploy(
|
|
11
|
+
Server().bootstrap()
|
|
12
|
+
Deploy()()
|
|
14
13
|
self.stdout.output(
|
|
15
14
|
"[green]Server bootstrapped and application deployed successfully![/green]"
|
|
16
15
|
)
|