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/config.py CHANGED
@@ -1,3 +1,160 @@
1
+ """
2
+ Fujin uses a ``fujin.toml`` file at the root of your project for configuration. Below are all available configuration options.
3
+
4
+ app
5
+ ---
6
+ The name of your project or application. Must be a valid Python package name.
7
+
8
+ app_bin
9
+ -------
10
+ Path to your application's executable. Used by the **app** subcommand for remote execution.
11
+ Default: ``.venv/bin/{app}``
12
+
13
+ version
14
+ --------
15
+ The version of your project to build and deploy. If not specified, automatically parsed from ``pyproject.toml`` under ``project.version``.
16
+
17
+ python_version
18
+ --------------
19
+ The Python version for your virtualenv. If not specified, automatically parsed from ``.python-version`` file.
20
+
21
+ versions_to_keep
22
+ ----------------
23
+ The number of versions to keep on the host. After each deploy, older versions are pruned based on this setting. By default, it keeps the latest 5 versions,
24
+ set this to `None` to never automatically prune.
25
+
26
+ build_command
27
+ -------------
28
+ The command to use to build your project's distribution file.
29
+
30
+ distfile
31
+ --------
32
+ Path to your project's distribution file. This should be the main artifact containing everything needed to run your project on the server.
33
+ Supports version placeholder, e.g., ``dist/app_name-{version}-py3-none-any.whl``
34
+
35
+ release_command
36
+ ---------------
37
+ Optional command to run at the end of deployment (e.g., database migrations).
38
+
39
+ requirements
40
+ ------------
41
+ Path to your requirements file.
42
+ Default: ``requirements.txt``
43
+
44
+ Webserver
45
+ ---------
46
+
47
+ type
48
+ ~~~~
49
+ The reverse proxy implementation to use. Available options:
50
+
51
+ - ``fujin.proxies.caddy`` (default)
52
+ - ``fujin.proxies.nginx``
53
+ - ``fujin.proxies.dummy`` (disables proxy functionality)
54
+
55
+ upstream
56
+ ~~~~~~~~
57
+ The address where your web application listens for requests. Supports any value compatible with your chosen web proxy:
58
+
59
+ - HTTP address (e.g., ``localhost:8000``)
60
+ - Unix socket (e.g., ``unix//run/project.sock``)
61
+
62
+ statics
63
+ ~~~~~~~
64
+
65
+ Defines the mapping of URL paths to local directories for serving static files. The syntax and support for static
66
+ file serving depend on the selected reverse proxy.
67
+
68
+ Example:
69
+
70
+ .. code-block:: toml
71
+
72
+ [webserver]
73
+ upstream = "unix//run/project.sock"
74
+ type = "fujin.proxies.caddy"
75
+ statics = { "/static/*" = "/var/www/example.com/static/" }
76
+
77
+ processes
78
+ ---------
79
+
80
+ A mapping of process names to commands that will be managed by the process manager. Define as many processes as needed, but
81
+ when using any proxy other than ``fujin.proxies.dummy``, a ``web`` process must be declared. Refer to the ``apps_dir``
82
+ setting on the host to understand how ``app_dir`` is determined.
83
+
84
+ Example:
85
+
86
+ .. code-block:: toml
87
+
88
+ [processes]
89
+ web = ".venv/bin/gunicorn myproject.wsgi:application"
90
+
91
+
92
+ .. note::
93
+
94
+ Commands are relative to your ``app_dir``. When generating systemd service files, the full path is automatically constructed.
95
+
96
+ Host Configuration
97
+ -------------------
98
+
99
+ ip
100
+ ~~
101
+ The IP address or hostname of the remote host.
102
+
103
+ domain_name
104
+ ~~~~~~~~~~~
105
+ The domain name pointing to this host. Used for web proxy configuration.
106
+
107
+ user
108
+ ~~~~
109
+ The login user for running remote tasks. Should have passwordless sudo access for optimal operation.
110
+
111
+ .. note::
112
+
113
+ You can create a user with these requirements using the ``fujin server create-user`` command.
114
+
115
+ envfile
116
+ ~~~~~~~
117
+ Path to the production environment file that will be copied to the host.
118
+
119
+ apps_dir
120
+ ~~~~~~~~
121
+
122
+ Base directory for project storage on the host. Path is relative to user's home directory.
123
+ Default: ``.local/share/fujin``. This value determines your project's ``app_dir``, which is ``{apps_dir}/{app}``.
124
+
125
+ password_env
126
+ ~~~~~~~~~~~~
127
+
128
+ Environment variable containing the user's password. Only needed if the user cannot run sudo without a password.
129
+
130
+ ssh_port
131
+ ~~~~~~~~
132
+
133
+ SSH port for connecting to the host.
134
+ Default: ``22``
135
+
136
+ key_filename
137
+ ~~~~~~~~~~~~
138
+
139
+ Path to the SSH private key file for authentication. Optional if using your system's default key location.
140
+
141
+ aliases
142
+ -------
143
+
144
+ A mapping of shortcut names to Fujin commands. Allows you to create convenient shortcuts for commonly used commands.
145
+
146
+ Example:
147
+
148
+ .. code-block:: toml
149
+
150
+ [aliases]
151
+ console = "app exec -i shell_plus" # open an interactive django shell
152
+ dbconsole = "app exec -i dbshell" # open an interactive django database shell
153
+ shell = "server exec --appenv -i bash" # SSH into the project directory with environment variables loaded
154
+
155
+
156
+ """
157
+
1
158
  from __future__ import annotations
2
159
 
3
160
  import os
@@ -13,51 +170,45 @@ if sys.version_info >= (3, 11):
13
170
  else:
14
171
  import tomli as tomllib
15
172
 
16
- try:
17
- from enum import StrEnum
18
- except ImportError:
19
- from enum import Enum
20
-
21
- class StrEnum(str, Enum):
22
- pass
23
-
24
-
25
- class Hook(StrEnum):
26
- PRE_DEPLOY = "pre_deploy"
173
+ from .hooks import HooksDict
27
174
 
28
175
 
29
176
  class Config(msgspec.Struct, kw_only=True):
30
- app: str
177
+ app_name: str = msgspec.field(name="app")
31
178
  app_bin: str = ".venv/bin/{app}"
32
179
  version: str = msgspec.field(default_factory=lambda: read_version_from_pyproject())
180
+ versions_to_keep: int | None = 5
33
181
  python_version: str = msgspec.field(default_factory=lambda: find_python_version())
34
182
  build_command: str
35
- _distfile: str = msgspec.field(name="distfile")
36
- aliases: dict[str, str]
37
- hosts: dict[str, HostConfig]
38
- processes: dict[str, str]
183
+ release_command: str | None = None
184
+ skip_project_install: bool = False
185
+ distfile: str
186
+ aliases: dict[str, str] = msgspec.field(default_factory=dict)
187
+ host: HostConfig
188
+ processes: dict[str, str] = msgspec.field(default_factory=dict)
39
189
  process_manager: str = "fujin.process_managers.systemd"
40
190
  webserver: Webserver
41
191
  _requirements: str = msgspec.field(name="requirements", default="requirements.txt")
42
- hooks: dict[Hook, str] = msgspec.field(default=dict)
192
+ hooks: HooksDict = msgspec.field(default_factory=dict)
193
+ local_config_dir: Path = Path(".fujin")
43
194
 
44
195
  def __post_init__(self):
45
- self.app_bin = self.app_bin.format(app=self.app)
46
- self._distfile = self._distfile.format(version=self.version)
196
+ self.app_bin = self.app_bin.format(app=self.app_name)
197
+ # self._distfile = self._distfile.format(version=self.version)
47
198
 
48
199
  if "web" not in self.processes and self.webserver.type != "fujin.proxies.dummy":
49
200
  raise ValueError(
50
201
  "Missing web process or set the proxy to 'fujin.proxies.dummy' to disable the use of a proxy"
51
202
  )
52
203
 
53
- @property
54
- def distfile(self) -> Path:
55
- return Path(self._distfile)
56
-
57
204
  @property
58
205
  def requirements(self) -> Path:
59
206
  return Path(self._requirements)
60
207
 
208
+ def get_distfile_path(self, version: str | None = None) -> Path:
209
+ version = version or self.version
210
+ return Path(self.distfile.format(version=version))
211
+
61
212
  @classmethod
62
213
  def read(cls) -> Config:
63
214
  fujin_toml = Path("fujin.toml")
@@ -76,17 +227,21 @@ class HostConfig(msgspec.Struct, kw_only=True):
76
227
  domain_name: str
77
228
  user: str
78
229
  _envfile: str = msgspec.field(name="envfile")
79
- projects_dir: str = "/home/{user}/.local/share/fujin"
230
+ apps_dir: str = ".local/share/fujin"
80
231
  password_env: str | None = None
81
232
  ssh_port: int = 22
82
233
  _key_filename: str | None = msgspec.field(name="key_filename", default=None)
83
- default: bool = False
84
234
 
85
235
  def __post_init__(self):
86
- self.projects_dir = self.projects_dir.format(user=self.user)
236
+ self.apps_dir = f"/home/{self.user}/{self.apps_dir}"
87
237
 
88
238
  def to_dict(self):
89
- return {f: getattr(self, f) for f in self.__struct_fields__}
239
+ d = {f: getattr(self, f) for f in self.__struct_fields__}
240
+ d.pop("_key_filename")
241
+ d.pop("_envfile")
242
+ d["key_filename"] = self.key_filename
243
+ d["envfile"] = self.envfile
244
+ return d
90
245
 
91
246
  @property
92
247
  def envfile(self) -> Path:
@@ -107,10 +262,14 @@ class HostConfig(msgspec.Struct, kw_only=True):
107
262
  raise ImproperlyConfiguredError(msg)
108
263
  return password
109
264
 
265
+ def get_app_dir(self, app_name: str) -> str:
266
+ return f"{self.apps_dir}/{app_name}"
267
+
110
268
 
111
269
  class Webserver(msgspec.Struct):
112
270
  upstream: str
113
271
  type: str = "fujin.proxies.caddy"
272
+ statics: dict[str, str] = msgspec.field(default_factory=dict)
114
273
 
115
274
 
116
275
  def read_version_from_pyproject():
fujin/connection.py ADDED
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from functools import partial
5
+ from typing import TYPE_CHECKING
6
+
7
+ import cappa
8
+ from fabric import Connection
9
+ from invoke import Responder
10
+ from invoke.exceptions import UnexpectedExit
11
+ from paramiko.ssh_exception import (
12
+ AuthenticationException,
13
+ NoValidConnectionsError,
14
+ SSHException,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from fujin.config import HostConfig
19
+
20
+
21
+ def _get_watchers(host: HostConfig) -> list[Responder]:
22
+ if not host.password:
23
+ return []
24
+ return [
25
+ Responder(
26
+ pattern=r"\[sudo\] password:",
27
+ response=f"{host.password}\n",
28
+ ),
29
+ Responder(
30
+ pattern=rf"\[sudo\] password for {host.user}:",
31
+ response=f"{host.password}\n",
32
+ ),
33
+ ]
34
+
35
+
36
+ @contextmanager
37
+ def host_connection(host: HostConfig) -> Connection:
38
+ connect_kwargs = None
39
+ if host.key_filename:
40
+ connect_kwargs = {"key_filename": str(host.key_filename)}
41
+ elif host.password:
42
+ connect_kwargs = {"password": host.password}
43
+ conn = Connection(
44
+ host.ip,
45
+ user=host.user,
46
+ port=host.ssh_port,
47
+ connect_kwargs=connect_kwargs,
48
+ )
49
+ try:
50
+ conn.run = partial(
51
+ conn.run,
52
+ env={
53
+ "PATH": f"/home/{host.user}/.cargo/bin:/home/{host.user}/.local/bin:$PATH"
54
+ },
55
+ watchers=_get_watchers(host),
56
+ )
57
+ yield conn
58
+ except AuthenticationException as e:
59
+ msg = f"Authentication failed for {host.user}@{host.ip} -p {host.ssh_port}.\n"
60
+ if host.key_filename:
61
+ msg += f"An SSH key was provided at {host.key_filename.resolve()}. Please verify its validity and correctness."
62
+ elif host.password:
63
+ msg += f"A password was provided through the environment variable {host.password_env}. Please ensure it is correct for the user {host.user}."
64
+ else:
65
+ msg += "No password or SSH key was provided. Ensure your current host has SSH access to the target host."
66
+ raise cappa.Exit(msg, code=1) from e
67
+ except (UnexpectedExit, NoValidConnectionsError) as e:
68
+ raise cappa.Exit(str(e), code=1) from e
69
+ except SSHException as e:
70
+ raise cappa.Exit(
71
+ f"{e}, possible causes: incorrect user, or either you or the server may be offline",
72
+ code=1,
73
+ ) from e
74
+ finally:
75
+ conn.close()
fujin/hooks.py CHANGED
@@ -1,15 +1,55 @@
1
1
  from dataclasses import dataclass
2
2
 
3
- from fujin.config import Config
4
- from fujin.host import Host
5
- from fujin.config import Hook
3
+ from fujin.connection import Connection
4
+ from rich import print as rich_print
5
+
6
+ try:
7
+ from enum import StrEnum
8
+ except ImportError:
9
+ from enum import Enum
10
+
11
+ class StrEnum(str, Enum):
12
+ pass
13
+
14
+
15
+ class Hook(StrEnum):
16
+ PRE_DEPLOY = "pre_deploy"
17
+ POST_DEPLOY = "post_deploy"
18
+ PRE_BOOTSTRAP = "pre_bootstrap"
19
+ POST_BOOTSTRAP = "post_bootstrap"
20
+ PRE_TEARDOWN = "pre_teardown"
21
+ POST_TEARDOWN = "post_teardown"
22
+
23
+
24
+ HooksDict = dict[Hook, dict]
6
25
 
7
26
 
8
27
  @dataclass(frozen=True, slots=True)
9
28
  class HookManager:
10
- config: Config
11
- host: Host
29
+ app_name: str
30
+ hooks: HooksDict
31
+ conn: Connection
32
+
33
+ def _run_hook(self, type_: Hook) -> None:
34
+ if hooks := self.hooks.get(type_):
35
+ for name, command in hooks.items():
36
+ rich_print(f"[blue]Running {type_} hook {name} [/blue]")
37
+ self.conn.run(command, pty=True)
38
+
39
+ def pre_deploy(self) -> None:
40
+ self._run_hook(Hook.PRE_DEPLOY)
41
+
42
+ def post_deploy(self) -> None:
43
+ self._run_hook(Hook.POST_DEPLOY)
44
+
45
+ def pre_bootstrap(self) -> None:
46
+ self._run_hook(Hook.PRE_BOOTSTRAP)
47
+
48
+ def post_bootstrap(self) -> None:
49
+ self._run_hook(Hook.POST_BOOTSTRAP)
50
+
51
+ def pre_teardown(self) -> None:
52
+ self._run_hook(Hook.PRE_TEARDOWN)
12
53
 
13
- def pre_deploy(self):
14
- if pre_deploy := self.config.hooks.get(Hook.PRE_DEPLOY):
15
- self.host.run(pre_deploy)
54
+ def post_teardown(self) -> None:
55
+ self._run_hook(Hook.POST_TEARDOWN)
@@ -1,18 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Protocol
3
+ from typing import Protocol
4
+ from typing import TYPE_CHECKING
5
+
6
+ from fujin.connection import Connection
4
7
 
5
8
  if TYPE_CHECKING:
6
9
  from fujin.config import Config
7
- from fujin.host import Host
8
10
 
9
11
 
10
12
  class ProcessManager(Protocol):
11
- host: Host
12
- config: Config
13
13
  service_names: list[str]
14
14
 
15
- def get_service_name(self, name: str): ...
15
+ @classmethod
16
+ def create(cls, config: Config, conn: Connection) -> ProcessManager: ...
17
+
18
+ def get_service_name(self, process_name: str): ...
16
19
 
17
20
  def install_services(self) -> None: ...
18
21
 
@@ -24,6 +27,14 @@ class ProcessManager(Protocol):
24
27
 
25
28
  def stop_services(self, *names) -> None: ...
26
29
 
30
+ def is_enabled(self, *names) -> dict[str, bool]: ...
31
+
32
+ def is_active(self, *names) -> dict[str, bool]: ...
33
+
27
34
  def service_logs(self, name: str, follow: bool = False): ...
28
35
 
29
36
  def reload_configuration(self) -> None: ...
37
+
38
+ def get_configuration_files(
39
+ self, ignore_local: bool = False
40
+ ) -> list[tuple[str, str]]: ...
@@ -5,101 +5,151 @@ from dataclasses import dataclass
5
5
  from pathlib import Path
6
6
 
7
7
  from fujin.config import Config
8
- from fujin.host import Host
9
-
10
-
11
- @dataclass(frozen=True, slots=True)
12
- class SystemdFile:
13
- name: str
14
- body: str
8
+ from fujin.connection import Connection
15
9
 
16
10
 
17
11
  @dataclass(frozen=True, slots=True)
18
12
  class ProcessManager:
19
- config: Config
20
- host: Host
13
+ conn: Connection
14
+ app_name: str
15
+ processes: dict[str, str]
16
+ app_dir: str
17
+ user: str
18
+ is_using_unix_socket: bool
19
+ local_config_dir: Path
20
+
21
+ @classmethod
22
+ def create(cls, config: Config, conn: Connection):
23
+ return cls(
24
+ processes=config.processes,
25
+ app_name=config.app_name,
26
+ app_dir=config.host.get_app_dir(config.app_name),
27
+ conn=conn,
28
+ user=config.host.user,
29
+ is_using_unix_socket="unix" in config.webserver.upstream
30
+ and config.webserver.type != "fujin.proxies.dummy",
31
+ local_config_dir=config.local_config_dir,
32
+ )
21
33
 
22
34
  @property
23
35
  def service_names(self) -> list[str]:
24
- return [self.get_service_name(name) for name in self.config.processes]
36
+ services = [self.get_service_name(name) for name in self.processes]
37
+ if self.is_using_unix_socket:
38
+ services.append(f"{self.app_name}.socket")
39
+ return services
40
+
41
+ def get_service_name(self, process_name: str):
42
+ if process_name == "web":
43
+ return f"{self.app_name}.service"
44
+ return f"{self.app_name}-{process_name}.service"
25
45
 
26
- def get_service_name(self, name: str):
27
- if name == "web":
28
- return self.config.app
29
- return f"{self.config.app}-{name}.service"
46
+ def run_pty(self, *args, **kwargs):
47
+ return self.conn.run(*args, **kwargs, pty=True)
30
48
 
31
49
  def install_services(self) -> None:
32
50
  conf_files = self.get_configuration_files()
33
- for conf_file in conf_files:
34
- self.host.sudo(
35
- f"echo '{conf_file.body}' | sudo tee /etc/systemd/system/{conf_file.name}",
51
+ for filename, content in conf_files:
52
+ self.run_pty(
53
+ f"echo '{content}' | sudo tee /etc/systemd/system/{filename}",
36
54
  hide="out",
37
55
  )
38
56
 
39
- self.host.sudo(f"systemctl enable --now {self.config.app}.socket")
40
- for name in self.service_names:
41
- # the main web service is launched by the socket service
42
- if name != f"{self.config.app}.service":
43
- self.host.sudo(f"systemctl enable {name}")
57
+ for name in self.processes:
58
+ if name == "web" and self.is_using_unix_socket:
59
+ self.run_pty(f"sudo systemctl enable --now {self.app_name}.socket")
60
+ else:
61
+ self.run_pty(f"sudo systemctl enable {self.get_service_name(name)}")
44
62
 
45
- def get_configuration_files(self) -> list[SystemdFile]:
63
+ def get_configuration_files(
64
+ self, ignore_local: bool = False
65
+ ) -> list[tuple[str, str]]:
46
66
  templates_folder = (
47
67
  Path(importlib.util.find_spec("fujin").origin).parent / "templates"
48
68
  )
49
69
  web_service_content = (templates_folder / "web.service").read_text()
50
70
  web_socket_content = (templates_folder / "web.socket").read_text()
51
- other_service_content = (templates_folder / "other.service").read_text()
71
+ simple_service_content = (templates_folder / "simple.service").read_text()
72
+ if not self.is_using_unix_socket:
73
+ web_service_content = web_service_content.replace(
74
+ "Requires={app_name}.socket\n", ""
75
+ )
76
+
52
77
  context = {
53
- "app": self.config.app,
54
- "user": self.host.config.user,
55
- "project_dir": self.host.project_dir(self.config.app),
78
+ "app_name": self.app_name,
79
+ "user": self.user,
80
+ "app_dir": self.app_dir,
56
81
  }
57
82
 
58
83
  files = []
59
- for name, command in self.config.processes.items():
84
+ for name, command in self.processes.items():
85
+ template = web_service_content if name == "web" else simple_service_content
60
86
  name = self.get_service_name(name)
61
- if name == "web":
62
- body = web_service_content.format(**context, command=command)
63
- files.append(
64
- SystemdFile(
65
- name=f"{self.config.app}.socket",
66
- body=web_socket_content.format(**context),
67
- )
68
- )
69
- else:
70
- body = other_service_content.format(**context, command=command)
71
- files.append(SystemdFile(name=name, body=body))
87
+ local_config = self.local_config_dir / name
88
+ body = (
89
+ local_config.read_text()
90
+ if local_config.exists() and not ignore_local
91
+ else template.format(**context, command=command)
92
+ )
93
+ files.append((name, body))
94
+ # if using unix then we are sure a web process was defined and the proxy is not dummy
95
+ if self.is_using_unix_socket:
96
+ name = f"{self.app_name}.socket"
97
+ local_config = self.local_config_dir / name
98
+ body = (
99
+ local_config.read_text()
100
+ if local_config.exists() and not ignore_local
101
+ else web_socket_content.format(**context)
102
+ )
103
+ files.append((name, body))
72
104
  return files
73
105
 
74
106
  def uninstall_services(self) -> None:
75
107
  self.stop_services()
76
- self.host.sudo(f"systemctl disable {self.config.app}.socket")
77
108
  for name in self.service_names:
78
- # was never enabled in the first place, look at the code above
79
- if name != f"{self.config.app}.service":
80
- self.host.sudo(f"systemctl disable {name}")
109
+ self.run_pty(f"sudo systemctl disable {name}", warn=True)
110
+ self.run_pty(f"sudo rm /etc/systemd/system/{name}", warn=True)
81
111
 
82
112
  def start_services(self, *names) -> None:
83
113
  names = names or self.service_names
84
114
  for name in names:
85
115
  if name in self.service_names:
86
- self.host.sudo(f"systemctl start {name}")
116
+ self.run_pty(f"sudo systemctl start {name}")
87
117
 
88
118
  def restart_services(self, *names) -> None:
89
119
  names = names or self.service_names
90
120
  for name in names:
91
121
  if name in self.service_names:
92
- self.host.sudo(f"systemctl restart {name}")
122
+ self.run_pty(f"sudo systemctl restart {name}")
93
123
 
94
124
  def stop_services(self, *names) -> None:
95
125
  names = names or self.service_names
96
126
  for name in names:
97
127
  if name in self.service_names:
98
- self.host.sudo(f"systemctl stop {name}")
128
+ self.run_pty(f"sudo systemctl stop {name}")
129
+
130
+ def is_enabled(self, *names) -> dict[str, bool]:
131
+ names = names or self.service_names
132
+ return {
133
+ name: self.run_pty(
134
+ f"sudo systemctl is-enabled {name}", warn=True, hide=True
135
+ ).stdout.strip()
136
+ == "enabled"
137
+ for name in names
138
+ }
139
+
140
+ def is_active(self, *names) -> dict[str, bool]:
141
+ names = names or self.service_names
142
+ return {
143
+ name: self.run_pty(
144
+ f"sudo systemctl is-active {name}", warn=True, hide=True
145
+ ).stdout.strip()
146
+ == "active"
147
+ for name in names
148
+ }
99
149
 
100
150
  def service_logs(self, name: str, follow: bool = False):
101
151
  # TODO: add more options here
102
- self.host.sudo(f"journalctl -u {name} -r {'-f' if follow else ''}")
152
+ self.run_pty(f"sudo journalctl -u {name} {'-f' if follow else ''}", warn=True)
103
153
 
104
154
  def reload_configuration(self) -> None:
105
- self.host.sudo(f"systemctl daemon-reload")
155
+ self.run_pty(f"sudo systemctl daemon-reload")
fujin/proxies/__init__.py CHANGED
@@ -1,18 +1,35 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Protocol
3
+ from pathlib import Path
4
+ from typing import Protocol
4
5
 
5
- if TYPE_CHECKING:
6
- from fujin.config import Config
7
- from fujin.host import Host
6
+ from fujin.config import Config
7
+ from fujin.config import HostConfig
8
+ from fujin.connection import Connection
8
9
 
9
10
 
10
11
  class WebProxy(Protocol):
11
- host: Host
12
- config: Config
12
+ config_file: Path
13
+
14
+ @classmethod
15
+ def create(cls, config: Config, conn: Connection) -> WebProxy: ...
13
16
 
14
17
  def install(self) -> None: ...
15
18
 
19
+ def uninstall(self) -> None: ...
20
+
16
21
  def setup(self) -> None: ...
17
22
 
18
23
  def teardown(self) -> None: ...
24
+
25
+ def start(self) -> None: ...
26
+
27
+ def stop(self) -> None: ...
28
+
29
+ def status(self) -> None: ...
30
+
31
+ def restart(self) -> None: ...
32
+
33
+ def logs(self) -> None: ...
34
+
35
+ def export_config(self) -> None: ...