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 +4 -0
- rfsyncer/__main__.py +4 -0
- rfsyncer/cli.py +391 -0
- rfsyncer/commands/__init__.py +0 -0
- rfsyncer/commands/clear.py +79 -0
- rfsyncer/commands/diff.py +926 -0
- rfsyncer/commands/ping.py +65 -0
- rfsyncer/ssh/__init__.py +0 -0
- rfsyncer/ssh/connector.py +137 -0
- rfsyncer/syncer.py +285 -0
- rfsyncer/util/__init__.py +0 -0
- rfsyncer/util/config.py +73 -0
- rfsyncer/util/consts.py +34 -0
- rfsyncer/util/display.py +144 -0
- rfsyncer/util/exceptions.py +2 -0
- rfsyncer/util/hash.py +15 -0
- rfsyncer/util/logger.py +52 -0
- rfsyncer/util/parser.py +16 -0
- rfsyncer/util/types.py +30 -0
- rfsyncer-0.0.3.dist-info/METADATA +345 -0
- rfsyncer-0.0.3.dist-info/RECORD +23 -0
- rfsyncer-0.0.3.dist-info/WHEEL +4 -0
- rfsyncer-0.0.3.dist-info/entry_points.txt +3 -0
rfsyncer/__init__.py
ADDED
rfsyncer/__main__.py
ADDED
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
|
+
)
|