rfsyncer 0.0.3__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.
rfsyncer/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from rfsyncer.syncer import Syncer
2
+ from rfsyncer.util.config import RfsyncerConfig
3
+
4
+ __all__ = ["RfsyncerConfig", "Syncer"]
rfsyncer/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from rfsyncer.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
rfsyncer/cli.py ADDED
@@ -0,0 +1,391 @@
1
+ import importlib
2
+ from enum import StrEnum, auto
3
+ from pathlib import Path
4
+ from types import SimpleNamespace
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from paramiko.config import SSHConfig
9
+ from rich.console import Console
10
+
11
+ from rfsyncer.syncer import Syncer
12
+ from rfsyncer.util.config import RfsyncerConfig
13
+ from rfsyncer.util.consts import DEFAULT_CONFIG_FILE
14
+ from rfsyncer.util.exceptions import HandledError
15
+ from rfsyncer.util.logger import add_file_handler, get_logger
16
+
17
+ app = typer.Typer(
18
+ pretty_exceptions_enable=False,
19
+ context_settings={"help_option_names": ["-h", "--help"]},
20
+ help="Rfsyncer by Headorteil 😎",
21
+ no_args_is_help=True,
22
+ )
23
+
24
+
25
+ def __version_callback(value: bool) -> None:
26
+ if value:
27
+ version = importlib.metadata.version("rfsyncer") # pyright: ignore[reportAttributeAccessIssue]
28
+ print(version) # noqa: T201
29
+ raise typer.Exit
30
+
31
+
32
+ class Color(StrEnum):
33
+ STANDARD = auto()
34
+ TRUECOLOR = auto()
35
+ AUTO = auto()
36
+ NONE = auto()
37
+ COLOR_256 = "256"
38
+
39
+
40
+ @app.callback(invoke_without_command=True)
41
+ def main(
42
+ ctx: typer.Context,
43
+ config_path: Annotated[
44
+ Path,
45
+ typer.Option(
46
+ "--config",
47
+ "-c",
48
+ file_okay=True,
49
+ readable=True,
50
+ resolve_path=True,
51
+ help="Config path",
52
+ ),
53
+ ] = DEFAULT_CONFIG_FILE,
54
+ verbosity: Annotated[
55
+ int,
56
+ typer.Option(
57
+ "--verbosity-level",
58
+ "-v",
59
+ rich_help_panel="Display",
60
+ min=0,
61
+ max=3,
62
+ help="Change the logs verbosity",
63
+ ),
64
+ ] = 2,
65
+ log_to_file: Annotated[
66
+ bool,
67
+ typer.Option(
68
+ "--log-to-file/--no-log-to-file",
69
+ rich_help_panel="Display",
70
+ help="Enable file logging (path defined in config)",
71
+ ),
72
+ ] = True,
73
+ processes: Annotated[
74
+ int,
75
+ typer.Option(
76
+ "--processes",
77
+ "-p",
78
+ help="Number of processes to pop",
79
+ ),
80
+ ] = 4,
81
+ flag: Annotated[
82
+ str,
83
+ typer.Option(
84
+ "--flag",
85
+ "-f",
86
+ help="json to pass to templating engines",
87
+ ),
88
+ ] = "",
89
+ display: Annotated[
90
+ bool,
91
+ typer.Option(
92
+ "--display/--no-display",
93
+ " /-D",
94
+ rich_help_panel="Display",
95
+ help="Display things that are not logs nor live like tables or diffs",
96
+ ),
97
+ ] = True,
98
+ pager: Annotated[
99
+ bool,
100
+ typer.Option(
101
+ "--pager/--no-pager",
102
+ "-P",
103
+ rich_help_panel="Display",
104
+ help="Display tables in less",
105
+ ),
106
+ ] = False,
107
+ live: Annotated[
108
+ bool,
109
+ typer.Option(
110
+ "--live/--no-live",
111
+ "-l/-L",
112
+ rich_help_panel="Display",
113
+ help="Display live objects like progress bars",
114
+ ),
115
+ ] = True,
116
+ debug: Annotated[
117
+ bool,
118
+ typer.Option(
119
+ "--debug/--no-debug",
120
+ "-d",
121
+ rich_help_panel="Display",
122
+ help="Use max verbosity and print file infos with logs",
123
+ ),
124
+ ] = False,
125
+ color: Annotated[
126
+ Color,
127
+ typer.Option(
128
+ "--color",
129
+ rich_help_panel="Display",
130
+ help="Color system",
131
+ ),
132
+ ] = Color.AUTO,
133
+ version: Annotated[ # noqa: ARG001
134
+ bool,
135
+ typer.Option(
136
+ "--version",
137
+ "-V",
138
+ callback=__version_callback,
139
+ is_eager=True,
140
+ help="Print the tool version",
141
+ ),
142
+ ] = False,
143
+ ) -> None:
144
+ """Set variables for all subcommands and run TUI if no command is provided."""
145
+ if color is Color.NONE:
146
+ console = Console(no_color=True)
147
+ else:
148
+ console = Console(color_system=color) # pyright: ignore[reportArgumentType]
149
+
150
+ if debug:
151
+ verbosity = 3
152
+
153
+ logger = get_logger(console, verbosity, debug)
154
+
155
+ try:
156
+ config = RfsyncerConfig(logger, config_path, flag)
157
+ config.init_config()
158
+ except HandledError as e:
159
+ logger.critical(e)
160
+ raise typer.Exit(code=1) from e
161
+ except Exception as e:
162
+ logger.critical("Can't parse config file -> %s", e)
163
+ raise typer.Exit(code=1) from e
164
+
165
+ if log_to_file:
166
+ add_file_handler(logger, config.log_file)
167
+
168
+ try:
169
+ syncer = Syncer(
170
+ config,
171
+ console=console,
172
+ display=display,
173
+ pager=pager,
174
+ live=live,
175
+ logger=logger, # pyright: ignore[reportCallIssue]
176
+ debug=debug,
177
+ processes=processes,
178
+ )
179
+ except Exception as e:
180
+ logger.critical(e)
181
+ raise typer.Exit(code=1) from e
182
+
183
+ ctx.obj = SimpleNamespace(
184
+ logger=logger,
185
+ syncer=syncer,
186
+ )
187
+
188
+
189
+ @app.command("ping", help="Test connectivity to remote hosts")
190
+ def ping(
191
+ ctx: typer.Context,
192
+ hosts: Annotated[
193
+ list[str] | None,
194
+ typer.Option(
195
+ "--hosts",
196
+ help="Hosts",
197
+ show_default="all hosts defined in the config file",
198
+ autocompletion=SSHConfig.from_path(
199
+ Path().home() / ".ssh/config",
200
+ ).get_hostnames,
201
+ ),
202
+ ] = None,
203
+ sudo: Annotated[
204
+ bool,
205
+ typer.Option(
206
+ "--sudo/--no-sudo",
207
+ "-s/-S",
208
+ help="Exec commands with sudo",
209
+ ),
210
+ ] = False,
211
+ insecure: Annotated[
212
+ bool,
213
+ typer.Option(
214
+ "--insecure/--no-insecure",
215
+ "-i/-I",
216
+ help="Insecure mode : don't check host keys",
217
+ ),
218
+ ] = False,
219
+ ) -> None:
220
+ try:
221
+ ctx.obj.syncer.ping(
222
+ hosts,
223
+ insecure=insecure,
224
+ sudo=sudo,
225
+ )
226
+ except Exception as e:
227
+ ctx.obj.logger.exception("Unhandled error")
228
+ raise typer.Exit(code=1) from e
229
+
230
+
231
+ @app.command("install", help="Install local tree to remote hosts")
232
+ def install(
233
+ ctx: typer.Context,
234
+ root: Annotated[
235
+ Path,
236
+ typer.Option(
237
+ "--root",
238
+ "-r",
239
+ help="Root path on wich make diff",
240
+ ),
241
+ ] = Path("./root"),
242
+ hosts: Annotated[
243
+ list[str] | None,
244
+ typer.Option(
245
+ "--hosts",
246
+ show_default="all hosts defined in the config file",
247
+ help="Hosts",
248
+ autocompletion=SSHConfig.from_path(
249
+ Path().home() / ".ssh/config",
250
+ ).get_hostnames,
251
+ ),
252
+ ] = None,
253
+ sudo: Annotated[
254
+ bool,
255
+ typer.Option(
256
+ "--sudo/--no-sudo",
257
+ "-s/-S",
258
+ help="Exec commands with sudo",
259
+ ),
260
+ ] = False,
261
+ insecure: Annotated[
262
+ bool,
263
+ typer.Option(
264
+ "--insecure/--no-insecure",
265
+ "-i/-I",
266
+ help="Insecure mode : don't check host keys",
267
+ ),
268
+ ] = False,
269
+ keep: Annotated[
270
+ bool,
271
+ typer.Option(
272
+ "--keep/--no-keep",
273
+ "-k/-K",
274
+ help="Keep remote tmp dir",
275
+ ),
276
+ ] = False,
277
+ ) -> None:
278
+ try:
279
+ ctx.obj.syncer.diff(
280
+ hosts,
281
+ root=root,
282
+ insecure=insecure,
283
+ sudo=sudo,
284
+ keep=keep,
285
+ install=True,
286
+ )
287
+ except Exception as e:
288
+ ctx.obj.logger.exception("Unhandled error")
289
+ raise typer.Exit(code=1) from e
290
+
291
+
292
+ @app.command("diff", help="Diff local tree with remote trees")
293
+ def diff(
294
+ ctx: typer.Context,
295
+ root: Annotated[
296
+ Path,
297
+ typer.Option(
298
+ "--root",
299
+ "-r",
300
+ help="Root path on wich make diff",
301
+ ),
302
+ ] = Path("./root"),
303
+ hosts: Annotated[
304
+ list[str] | None,
305
+ typer.Option(
306
+ "--hosts",
307
+ show_default="all hosts defined in the config file",
308
+ help="Hosts",
309
+ autocompletion=SSHConfig.from_path(
310
+ Path().home() / ".ssh/config",
311
+ ).get_hostnames,
312
+ ),
313
+ ] = None,
314
+ sudo: Annotated[
315
+ bool,
316
+ typer.Option(
317
+ "--sudo/--no-sudo",
318
+ "-s/-S",
319
+ help="Exec commands with sudo",
320
+ ),
321
+ ] = False,
322
+ insecure: Annotated[
323
+ bool,
324
+ typer.Option(
325
+ "--insecure/--no-insecure",
326
+ "-i/-I",
327
+ help="Insecure mode : don't check host keys",
328
+ ),
329
+ ] = False,
330
+ keep: Annotated[
331
+ bool,
332
+ typer.Option(
333
+ "--keep/--no-keep",
334
+ "-k/-K",
335
+ help="Keep remote tmp dir",
336
+ ),
337
+ ] = False,
338
+ upload: Annotated[
339
+ bool,
340
+ typer.Option(
341
+ "--force-upload/--no-force-upload",
342
+ "-f/-F",
343
+ help="Force upload to remote, may be useful with --keep",
344
+ ),
345
+ ] = False,
346
+ ) -> None:
347
+ try:
348
+ ctx.obj.syncer.diff(
349
+ hosts,
350
+ root=root,
351
+ insecure=insecure,
352
+ sudo=sudo,
353
+ keep=keep,
354
+ upload=upload,
355
+ )
356
+ except Exception as e:
357
+ ctx.obj.logger.exception("Unhandled error")
358
+ raise typer.Exit(code=1) from e
359
+
360
+
361
+ @app.command("clear", help="Clear remote hosts of rfsyncer temporary files")
362
+ def clear(
363
+ ctx: typer.Context,
364
+ hosts: Annotated[
365
+ list[str] | None,
366
+ typer.Option(
367
+ "--hosts",
368
+ show_default="all hosts defined in the config file",
369
+ help="Hosts",
370
+ autocompletion=SSHConfig.from_path(
371
+ Path().home() / ".ssh/config",
372
+ ).get_hostnames,
373
+ ),
374
+ ] = None,
375
+ insecure: Annotated[
376
+ bool,
377
+ typer.Option(
378
+ "--insecure/--no-insecure",
379
+ "-i/-I",
380
+ help="Insecure mode : don't check host keys",
381
+ ),
382
+ ] = False,
383
+ ) -> None:
384
+ try:
385
+ ctx.obj.syncer.clear(
386
+ hosts,
387
+ insecure=insecure,
388
+ )
389
+ except Exception as e:
390
+ ctx.obj.logger.exception("Unhandled error")
391
+ raise typer.Exit(code=1) from e
File without changes
@@ -0,0 +1,79 @@
1
+ import logging
2
+ from multiprocessing import Semaphore
3
+ from multiprocessing.queues import Queue as QueueType
4
+ from shlex import quote
5
+ from typing import Any
6
+
7
+ from rfsyncer.commands.ping import ping
8
+ from rfsyncer.ssh.connector import Connector
9
+ from rfsyncer.util.config import RfsyncerConfig
10
+ from rfsyncer.util.consts import (
11
+ ASKPASS_PATH,
12
+ REMOTE_TEMP_DIR,
13
+ )
14
+ from rfsyncer.util.display import mp_log
15
+
16
+
17
+ def clear(
18
+ queue: QueueType[Any],
19
+ config: RfsyncerConfig,
20
+ semaphore: Semaphore, # pyright: ignore[reportInvalidTypeForm, reportUnknownParameterType]
21
+ host: str,
22
+ insecure: bool,
23
+ **_: Any, # noqa: ANN401
24
+ ) -> Connector | None:
25
+ print_infos = (queue, host, None, None)
26
+ try:
27
+ connector = ping(
28
+ queue,
29
+ config,
30
+ semaphore,
31
+ host,
32
+ insecure,
33
+ )
34
+ if not connector:
35
+ return
36
+ host_config: dict[str, Any] = connector.host_config # pyright: ignore[reportAssignmentType]
37
+ real_hostname = host_config["real_hostname"]
38
+ user = host_config["user"]
39
+ hostname = host_config["hostname"]
40
+ print_infos = (queue, hostname, user, real_hostname)
41
+
42
+ with semaphore:
43
+ _, stderr = connector.exec(f"rm -rf {quote(str(REMOTE_TEMP_DIR))}") # pyright: ignore[reportAssignmentType]
44
+ if not stderr:
45
+ mp_log(
46
+ logging.INFO,
47
+ *print_infos,
48
+ "Temporary dir clear OK",
49
+ )
50
+ else:
51
+ mp_log(
52
+ logging.ERROR,
53
+ *print_infos,
54
+ "Temporary dir clear NOK : %s",
55
+ stderr,
56
+ )
57
+
58
+ _, stderr = connector.exec(f"rm -rf {quote(str(ASKPASS_PATH))}") # pyright: ignore[reportAssignmentType]
59
+ if not stderr:
60
+ mp_log(
61
+ logging.INFO,
62
+ *print_infos,
63
+ "Askpass clear OK",
64
+ )
65
+ else:
66
+ mp_log(
67
+ logging.ERROR,
68
+ *print_infos,
69
+ "Askpass clear NOK : %s",
70
+ stderr,
71
+ )
72
+ except Exception as e: # noqa: BLE001
73
+ mp_log(
74
+ logging.ERROR,
75
+ *print_infos,
76
+ "Error : %s",
77
+ str(e),
78
+ stop=True,
79
+ )