1
|
|
|
#!/usr/bin/env python |
2
|
|
|
# -*- coding: UTF-8 -*- |
3
|
|
|
|
4
|
|
|
# Isomer - The distributed application framework |
5
|
|
|
# ============================================== |
6
|
|
|
# Copyright (C) 2011-2020 Heiko 'riot' Weinen <[email protected]> and others. |
7
|
|
|
# |
8
|
|
|
# This program is free software: you can redistribute it and/or modify |
9
|
|
|
# it under the terms of the GNU Affero General Public License as published by |
10
|
|
|
# the Free Software Foundation, either version 3 of the License, or |
11
|
|
|
# (at your option) any later version. |
12
|
|
|
# |
13
|
|
|
# This program is distributed in the hope that it will be useful, |
14
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
15
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16
|
|
|
# GNU Affero General Public License for more details. |
17
|
|
|
# |
18
|
|
|
# You should have received a copy of the GNU Affero General Public License |
19
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
20
|
|
|
|
21
|
|
|
""" |
22
|
|
|
|
23
|
|
|
Module: remote |
24
|
|
|
============== |
25
|
|
|
|
26
|
|
|
Remote instance management functionality. |
27
|
|
|
|
28
|
|
|
This module allows deploying and maintaining instances on remote systems via SSH. |
29
|
|
|
|
30
|
|
|
""" |
31
|
|
|
|
32
|
|
|
import os |
33
|
|
|
import spur |
34
|
|
|
import tomlkit |
35
|
|
|
import click |
36
|
|
|
import getpass |
37
|
|
|
|
38
|
|
|
from typing import Optional |
39
|
|
|
from binascii import hexlify |
40
|
|
|
from click_didyoumean import DYMGroup |
41
|
|
|
|
42
|
|
|
from isomer.logger import warn, error, debug, verbose |
43
|
|
|
from isomer.misc.std import std_now |
44
|
|
|
from isomer.tool import ( |
45
|
|
|
log, |
46
|
|
|
run_process, |
47
|
|
|
install_isomer, |
48
|
|
|
get_isomer, |
49
|
|
|
format_result, |
50
|
|
|
) # , ask_password |
51
|
|
|
from isomer.misc.path import get_etc_remote_keys_path |
52
|
|
|
from isomer.tool.defaults import platforms, key_defaults |
53
|
|
|
from isomer.error import abort, EXIT_INVALID_PARAMETER, EXIT_INVALID_VALUE |
54
|
|
|
from isomer.tool.cli import cli |
55
|
|
|
from isomer.tool.etc import load_remotes, remote_template, write_remote |
56
|
|
|
|
57
|
|
|
key_dispatch_table: Optional[dict] |
58
|
|
|
|
59
|
|
|
try: |
60
|
|
|
# noinspection PyPackageRequirements |
61
|
|
|
from paramiko import DSSKey, RSAKey |
62
|
|
|
|
63
|
|
|
key_dispatch_table = {"dsa": DSSKey, "rsa": RSAKey} |
64
|
|
|
except ImportError: |
65
|
|
|
key_dispatch_table = DSSKey = RSAKey = None |
66
|
|
|
log("Could not load paramiko. Remote operations disabled.", lvl=warn) |
67
|
|
|
|
68
|
|
|
|
69
|
|
|
def get_remote_home(username): |
70
|
|
|
"""Expands a username into a correct home directory""" |
71
|
|
|
|
72
|
|
|
if username == "root": |
73
|
|
|
return "/root/" |
74
|
|
|
else: |
75
|
|
|
return "/home/" + username |
76
|
|
|
|
77
|
|
|
|
78
|
|
|
@cli.group( |
79
|
|
|
cls=DYMGroup, |
80
|
|
|
short_help="Remote Isomer Management" |
81
|
|
|
) |
82
|
|
|
@click.option("--name", "-n", default="default") |
83
|
|
|
@click.option("--install", "-i", is_flag=True, default=False) |
84
|
|
|
@click.option( |
85
|
|
|
"--platform", |
86
|
|
|
"-p", |
87
|
|
|
default=list(platforms.keys())[0], |
88
|
|
|
type=click.Choice(platforms.keys()), |
89
|
|
|
) |
90
|
|
|
@click.option( |
91
|
|
|
"--source", "-s", default="git", type=click.Choice(["link", "copy", "git"]) |
92
|
|
|
) |
93
|
|
|
@click.option("--url", "-u", default=None) |
94
|
|
|
@click.option("--existing", "-e", default=None) |
95
|
|
|
@click.pass_context |
96
|
|
|
def remote(ctx, name, install, platform, source, url, existing): |
97
|
|
|
"""Remote instance control (Work in Progress!)""" |
98
|
|
|
|
99
|
|
|
ctx.obj["remote"] = name |
100
|
|
|
ctx.obj["platform"] = platform |
101
|
|
|
ctx.obj["source"] = source |
102
|
|
|
ctx.obj["url"] = url |
103
|
|
|
ctx.obj["existing"] = existing |
104
|
|
|
|
105
|
|
|
if ctx.invoked_subcommand == "add": |
106
|
|
|
return |
107
|
|
|
|
108
|
|
|
remotes = ctx.obj["remotes"] = load_remotes() |
109
|
|
|
|
110
|
|
|
if ctx.invoked_subcommand == "list": |
111
|
|
|
return |
112
|
|
|
|
113
|
|
|
# log('Remote configurations:', remotes, pretty=True) |
114
|
|
|
|
115
|
|
|
host_config = remotes.get(name, None) |
116
|
|
|
|
117
|
|
|
if host_config is None: |
118
|
|
|
log("Cannot proceed, remote unknown", lvl=error) |
119
|
|
|
abort(5000) |
120
|
|
|
|
121
|
|
|
ctx.obj["host_config"] = host_config |
122
|
|
|
|
123
|
|
|
if platform is None: |
124
|
|
|
platform = ctx.obj["host_config"].get("platform", "debian") |
125
|
|
|
ctx.obj["platform"] = platform |
126
|
|
|
|
127
|
|
|
spur_config = dict(host_config["login"]) |
128
|
|
|
|
129
|
|
|
if spur_config["private_key_file"] == "": |
130
|
|
|
spur_config.pop("private_key_file") |
131
|
|
|
|
132
|
|
|
if spur_config["port"] != 22: |
133
|
|
|
log( |
134
|
|
|
"Warning! Using any port other than 22 is not supported right now.", |
135
|
|
|
lvl=warn, |
136
|
|
|
) |
137
|
|
|
|
138
|
|
|
spur_config.pop("port") |
139
|
|
|
|
140
|
|
|
shell = spur.SshShell(**spur_config) |
141
|
|
|
|
142
|
|
|
if install: |
143
|
|
|
success, result = run_process("/", ["iso", "info"], shell) |
144
|
|
|
if success: |
145
|
|
|
log("Isomer version on remote:", format_result(result)) |
146
|
|
|
else: |
147
|
|
|
log('Use "remote install" for now') |
148
|
|
|
# if existing is None: |
149
|
|
|
# get_isomer(source, url, '/root', shell=shell) |
150
|
|
|
# destination = '/' + host_config['login']['username'] + '/repository' |
151
|
|
|
# else: |
152
|
|
|
# destination = existing |
153
|
|
|
# install_isomer(platform, host_config.get('use_sudo', True), shell, cwd=destination) |
154
|
|
|
|
155
|
|
|
ctx.obj["shell"] = shell |
156
|
|
|
|
157
|
|
|
|
158
|
|
|
@remote.command() |
159
|
|
|
@click.argument("hostname") |
160
|
|
|
@click.option( |
161
|
|
|
"--username", |
162
|
|
|
"-u", |
163
|
|
|
prompt=True, |
164
|
|
|
default=lambda: os.environ.get("USER", ""), |
|
|
|
|
165
|
|
|
show_default="current user", |
166
|
|
|
) |
167
|
|
|
@click.option("--password", "-pw", default="") |
168
|
|
|
@click.option("--port", "-p", default=22) |
169
|
|
|
@click.option("--use-sudo", "-s", default=False, is_flag=True) |
170
|
|
|
@click.option("--make-key", "-m", default=False, is_flag=True) |
171
|
|
|
@click.option("--key-file", "-k", default="") |
172
|
|
|
@click.option( |
173
|
|
|
"--key-type", |
174
|
|
|
"-t", |
175
|
|
|
default=key_defaults["type"], |
176
|
|
|
help="Key type (%s)" % key_defaults["type"], |
177
|
|
|
type=click.Choice(["dsa", "rsa"]), |
178
|
|
|
) |
179
|
|
|
@click.option( |
180
|
|
|
"--key-bits", |
181
|
|
|
"-b", |
182
|
|
|
type=int, |
183
|
|
|
default=int(key_defaults["bits"]), |
184
|
|
|
help="Key bits (%i)" % int(key_defaults["bits"]), |
185
|
|
|
) |
186
|
|
|
@click.pass_context |
187
|
|
|
def add( |
188
|
|
|
ctx, |
189
|
|
|
hostname, |
190
|
|
|
username, |
191
|
|
|
password, |
192
|
|
|
port, |
193
|
|
|
use_sudo, |
194
|
|
|
make_key, |
195
|
|
|
key_file, |
196
|
|
|
key_type, |
197
|
|
|
key_bits, |
198
|
|
|
): |
199
|
|
|
"""Adds a new remote""" |
200
|
|
|
|
201
|
|
|
if make_key: |
202
|
|
|
if key_dispatch_table is None: |
203
|
|
|
log( |
204
|
|
|
"You'll need to install paramiko to generate remote keys. Use pip3 install paramiko", |
205
|
|
|
lvl=error, |
206
|
|
|
) |
207
|
|
|
abort(5000) |
208
|
|
|
|
209
|
|
|
if key_file == "": |
210
|
|
|
key_file = os.path.join(get_etc_remote_keys_path(), ctx.obj["remote"]) |
211
|
|
|
|
212
|
|
|
phrase = None |
213
|
|
|
|
214
|
|
|
if key_type == "dsa" and key_bits > 1024: |
215
|
|
|
log("DSA Keys must be 1024 bits", lvl=error) |
216
|
|
|
abort(5000) |
217
|
|
|
|
218
|
|
|
# generating private key |
219
|
|
|
prv = key_dispatch_table[key_type].generate(bits=key_bits, progress_func=log) |
220
|
|
|
prv.write_private_key_file(key_file, password=phrase) |
221
|
|
|
# generating public key |
222
|
|
|
pub = key_dispatch_table[key_type](filename=key_file, password=phrase) |
223
|
|
|
with open("%s.pub" % key_file, "w") as f: |
224
|
|
|
f.write("%s %s" % (pub.get_name(), pub.get_base64())) |
225
|
|
|
f.write(" %s" % key_defaults["comment"]) |
226
|
|
|
|
227
|
|
|
key_hash = hexlify(pub.get_fingerprint()) |
228
|
|
|
log( |
229
|
|
|
"Fingerprint: %d %s %s.pub (%s)" |
230
|
|
|
% ( |
231
|
|
|
key_bits, |
232
|
|
|
":".join( |
233
|
|
|
[str(key_hash)[i: 2 + i] for i in range(0, len(key_hash), 2)] |
234
|
|
|
), |
235
|
|
|
key_file, |
236
|
|
|
key_type.upper(), |
237
|
|
|
) |
238
|
|
|
) |
239
|
|
|
|
240
|
|
|
new_remote = remote_template |
241
|
|
|
new_remote["name"] = ctx.obj["remote"] |
242
|
|
|
new_remote["platform"] = ctx.obj["platform"] |
243
|
|
|
new_remote["use_sudo"] = use_sudo |
244
|
|
|
new_remote["source"] = ctx.obj["source"] |
245
|
|
|
new_remote["url"] = ctx.obj["url"] |
246
|
|
|
|
247
|
|
|
new_remote["login"]["hostname"] = hostname |
248
|
|
|
new_remote["login"]["username"] = username |
249
|
|
|
new_remote["login"]["password"] = password |
250
|
|
|
new_remote["login"]["port"] = port |
251
|
|
|
new_remote["login"]["private_key_file"] = key_file |
252
|
|
|
|
253
|
|
|
log("New remote:", new_remote, pretty=True) |
254
|
|
|
write_remote(new_remote) |
255
|
|
|
|
256
|
|
|
|
257
|
|
|
@remote.command() |
258
|
|
|
@click.option( |
259
|
|
|
"--accept", |
260
|
|
|
"-a", |
261
|
|
|
help="Accept missing host key and add it to known_hosts", |
262
|
|
|
is_flag=True, |
263
|
|
|
default=False, |
264
|
|
|
) |
265
|
|
|
@click.pass_context |
266
|
|
|
def upload_key(ctx, accept): |
267
|
|
|
"""Upload a remote key to a user account on a remote machine""" |
268
|
|
|
|
269
|
|
|
login_config = dict(ctx.obj["host_config"]["login"]) |
270
|
|
|
|
271
|
|
|
if login_config["password"] == "": |
272
|
|
|
login_config["password"] = getpass.getpass() |
273
|
|
|
|
274
|
|
|
with open(login_config["private_key_file"] + ".pub") as f: |
275
|
|
|
key = f.read() |
276
|
|
|
|
277
|
|
|
username = login_config["username"] |
278
|
|
|
|
279
|
|
|
if accept: |
280
|
|
|
host_key_flag = spur.ssh.MissingHostKey.warn |
281
|
|
|
else: |
282
|
|
|
host_key_flag = spur.ssh.MissingHostKey.raise_error |
283
|
|
|
|
284
|
|
|
shell = spur.SshShell( |
285
|
|
|
hostname=login_config["hostname"], |
286
|
|
|
username=login_config["username"], |
287
|
|
|
password=login_config["password"], |
288
|
|
|
missing_host_key=host_key_flag, |
289
|
|
|
) |
290
|
|
|
|
291
|
|
|
try: |
292
|
|
|
with shell.open("/home/" + username + "/.ssh/authorized_keys", "r") as f: |
293
|
|
|
result = f.read() |
294
|
|
|
except spur.ssh.ConnectionError as e: |
295
|
|
|
log("SSH Connection error:\n", e, lvl=error) |
296
|
|
|
log( |
297
|
|
|
"Host not in known hosts or other problem. Use --accept to add to known_hosts." |
298
|
|
|
) |
299
|
|
|
abort(50071) |
300
|
|
|
except FileNotFoundError as e: |
301
|
|
|
log("No authorized key file yet, creating") |
302
|
|
|
success, result = run_process( |
303
|
|
|
"/home/" + username, |
304
|
|
|
["/bin/mkdir", "/home/" + username + "/.ssh"], |
305
|
|
|
shell=shell, |
306
|
|
|
) |
307
|
|
|
if not success: |
308
|
|
|
log( |
309
|
|
|
"Error creating .ssh directory:", |
310
|
|
|
e, |
311
|
|
|
format_result(result), |
312
|
|
|
pretty=True, |
313
|
|
|
lvl=error, |
314
|
|
|
) |
315
|
|
|
success, result = run_process( |
316
|
|
|
"/home/" + login_config["username"], |
317
|
|
|
["/usr/bin/touch", "/home/" + username + "/.ssh/authorized_keys"], |
318
|
|
|
shell=shell, |
319
|
|
|
) |
320
|
|
|
if not success: |
321
|
|
|
log( |
322
|
|
|
"Error creating authorized hosts file:", |
323
|
|
|
e, |
324
|
|
|
format_result(result).output, |
325
|
|
|
lvl=error, |
326
|
|
|
) |
327
|
|
|
result = "" |
328
|
|
|
|
329
|
|
|
if key not in result: |
|
|
|
|
330
|
|
|
log("Key not yet authorized - adding") |
331
|
|
|
with shell.open("/home/" + username + "/.ssh/authorized_keys", "a") as f: |
332
|
|
|
f.write(key) |
333
|
|
|
else: |
334
|
|
|
log("Key is already authorized.", lvl=warn) |
335
|
|
|
|
336
|
|
|
log("Uploaded key") |
337
|
|
|
|
338
|
|
|
|
339
|
|
|
@remote.command(name="set", short_help="Set a parameter of a remote") |
340
|
|
|
@click.option( |
341
|
|
|
"--login", "-l", help="Modify login settings", is_flag=True, default=False |
342
|
|
|
) |
343
|
|
|
@click.argument("parameter") |
344
|
|
|
@click.argument("value") |
345
|
|
|
@click.pass_context |
346
|
|
|
def set_parameter(ctx, login, parameter, value): |
347
|
|
|
"""Set a configuration parameter of an instance""" |
348
|
|
|
|
349
|
|
|
log("Setting %s to %s" % (parameter, value)) |
350
|
|
|
remote_config = ctx.obj["host_config"] |
351
|
|
|
defaults = remote_template |
352
|
|
|
|
353
|
|
|
converted_value = None |
354
|
|
|
|
355
|
|
|
try: |
356
|
|
|
if login: |
357
|
|
|
parameter_type = type(defaults["login"][parameter]) |
358
|
|
|
else: |
359
|
|
|
parameter_type = type(defaults[parameter]) |
360
|
|
|
|
361
|
|
|
log(parameter_type, pretty=True, lvl=verbose) |
362
|
|
|
|
363
|
|
|
if parameter_type == tomlkit.api.Integer: |
364
|
|
|
converted_value = int(value) |
365
|
|
|
else: |
366
|
|
|
converted_value = value |
367
|
|
|
except KeyError: |
368
|
|
|
log( |
369
|
|
|
"Invalid parameter specified. Available parameters:", |
370
|
|
|
sorted(list(defaults.keys())), |
371
|
|
|
lvl=warn, |
372
|
|
|
) |
373
|
|
|
abort(EXIT_INVALID_PARAMETER) |
374
|
|
|
|
375
|
|
|
if converted_value is None: |
376
|
|
|
abort(EXIT_INVALID_VALUE) |
377
|
|
|
|
378
|
|
|
if login: |
379
|
|
|
remote_config["login"][parameter] = converted_value |
380
|
|
|
else: |
381
|
|
|
remote_config[parameter] = converted_value |
382
|
|
|
|
383
|
|
|
log("New config:", remote_config, pretty=True, lvl=debug) |
384
|
|
|
|
385
|
|
|
ctx.obj["remotes"][ctx.obj["remote"]] = remote_config |
386
|
|
|
|
387
|
|
|
write_remote(remote_config) |
388
|
|
|
|
389
|
|
|
|
390
|
|
|
@remote.command(name="install") |
391
|
|
|
@click.option( |
392
|
|
|
"--archive", "-a", is_flag=True, default=False, help="Archive existing Isomer first" |
393
|
|
|
) |
394
|
|
|
@click.option( |
395
|
|
|
"--setup", |
396
|
|
|
"-s", |
397
|
|
|
is_flag=True, |
398
|
|
|
default=False, |
399
|
|
|
help="Setup basic Isomer user/directories", |
400
|
|
|
) |
401
|
|
|
@click.pass_context |
402
|
|
|
def install_remote(ctx, archive, setup): |
403
|
|
|
"""Installs Isomer (Management) on a remote host""" |
404
|
|
|
|
405
|
|
|
shell = ctx.obj["shell"] |
406
|
|
|
platform = ctx.obj["platform"] |
407
|
|
|
host_config = ctx.obj["host_config"] |
408
|
|
|
use_sudo = host_config["use_sudo"] |
409
|
|
|
username = host_config["login"]["username"] |
410
|
|
|
existing = ctx.obj["existing"] |
411
|
|
|
remote_home = get_remote_home(username) |
412
|
|
|
target = os.path.join(remote_home, "isomer") |
413
|
|
|
|
414
|
|
|
log(remote_home) |
415
|
|
|
|
416
|
|
|
if shell is None: |
417
|
|
|
log("Remote was not configured properly.", lvl=warn) |
418
|
|
|
abort(5000) |
419
|
|
|
|
420
|
|
|
if archive: |
421
|
|
|
log("Renaming remote isomer copy") |
422
|
|
|
success, result = run_process( |
423
|
|
|
remote_home, |
424
|
|
|
["mv", target, os.path.join(remote_home, "isomer_" + std_now())], |
425
|
|
|
shell=shell, |
426
|
|
|
) |
427
|
|
|
if not success: |
428
|
|
|
log("Could not rename remote copy:", result, pretty=True, lvl=error) |
429
|
|
|
abort(5000) |
430
|
|
|
|
431
|
|
|
if existing is None: |
432
|
|
|
url = ctx.obj["url"] |
433
|
|
|
if url is None: |
434
|
|
|
url = host_config.get("url", None) |
435
|
|
|
|
436
|
|
|
source = ctx.obj["source"] |
437
|
|
|
if source is None: |
438
|
|
|
source = host_config.get("source", None) |
439
|
|
|
|
440
|
|
|
if url is None or source is None: |
441
|
|
|
log('Need a source and url to install. Try "iso remote --help".') |
442
|
|
|
abort(5000) |
443
|
|
|
|
444
|
|
|
get_isomer(source, url, target, upgrade=ctx.obj["upgrade"], shell=shell) |
445
|
|
|
destination = os.path.join(remote_home, "isomer") |
446
|
|
|
else: |
447
|
|
|
destination = existing |
448
|
|
|
|
449
|
|
|
install_isomer(platform, use_sudo, shell=shell, cwd=destination) |
450
|
|
|
|
451
|
|
|
if setup: |
452
|
|
|
log("Setting up system user and paths") |
453
|
|
|
success, result = run_process(remote_home, ["iso", "system", "all"]) |
454
|
|
|
if not success: |
455
|
|
|
log( |
456
|
|
|
"Setting up system failed:", |
457
|
|
|
format_result(result), |
458
|
|
|
pretty=True, |
459
|
|
|
lvl=error, |
460
|
|
|
) |
461
|
|
|
|
462
|
|
|
|
463
|
|
|
@remote.command() |
464
|
|
|
@click.pass_context |
465
|
|
|
def upgrade(ctx): |
466
|
|
|
"""Upgrade an existing remote""" |
467
|
|
|
|
468
|
|
|
ctx.obj["archive"] = True |
469
|
|
|
ctx.obj["setup"] = False |
470
|
|
|
ctx.obj["upgrade"] = True |
471
|
|
|
ctx.forward(install_remote) |
472
|
|
|
|
473
|
|
|
|
474
|
|
|
@remote.command() |
475
|
|
|
@click.pass_context |
476
|
|
|
def info(ctx): |
477
|
|
|
"""Shows information about the selected remote""" |
478
|
|
|
|
479
|
|
|
if ctx.obj["host_config"]["login"]["password"] != "": |
480
|
|
|
ctx.obj["host_config"]["login"]["password"] = "__OMITTED__" |
481
|
|
|
|
482
|
|
|
log("Remote %s:" % ctx.obj["remote"], ctx.obj["host_config"], pretty=True) |
483
|
|
|
|
484
|
|
|
|
485
|
|
|
@remote.command(name="list") |
486
|
|
|
@click.pass_context |
487
|
|
|
def list_remotes(ctx): |
488
|
|
|
"""Shows all configured remotes""" |
489
|
|
|
|
490
|
|
|
log("Remotes:", list(ctx.obj["remotes"].keys()), pretty=True) |
491
|
|
|
|
492
|
|
|
|
493
|
|
|
@remote.command(name="test") |
494
|
|
|
@click.pass_context |
495
|
|
|
def test(ctx): |
496
|
|
|
"""Run and return info command on a remote""" |
497
|
|
|
|
498
|
|
|
shell = ctx.obj["shell"] |
499
|
|
|
username = ctx.obj["host_config"]["login"]["username"] |
500
|
|
|
|
501
|
|
|
success, result = run_process( |
502
|
|
|
get_remote_home(username), ["iso", "-nc", "version"], shell=shell |
503
|
|
|
) |
504
|
|
|
log(success, "\n", format_result(result), pretty=True) |
505
|
|
|
|
506
|
|
|
|
507
|
|
|
@remote.command(name="command") |
508
|
|
|
@click.argument("commands", nargs=-1) |
509
|
|
|
@click.pass_context |
510
|
|
|
def command(ctx, commands): |
511
|
|
|
"""Execute a remote command""" |
512
|
|
|
|
513
|
|
|
log("Executing commands %s on remote %s" % (commands, ctx.obj["remote"])) |
514
|
|
|
|
515
|
|
|
shell = ctx.obj["shell"] |
516
|
|
|
|
517
|
|
|
args = ["iso"] + list(commands) |
518
|
|
|
|
519
|
|
|
log(args) |
520
|
|
|
|
521
|
|
|
success, result = run_process( |
522
|
|
|
get_remote_home(ctx.obj["host_config"]["login"]["username"]), args, shell=shell |
523
|
|
|
) |
524
|
|
|
|
525
|
|
|
if not success: |
526
|
|
|
log("Execution error:", format_result(result), pretty=True, lvl=error) |
527
|
|
|
else: |
528
|
|
|
log("Success:") |
529
|
|
|
log(format_result(result)) |
530
|
|
|
|
531
|
|
|
|
532
|
|
|
@remote.command(name="backup") |
533
|
|
|
@click.argument("backup-instance") |
534
|
|
|
@click.option( |
535
|
|
|
"--fetch", |
536
|
|
|
"-f", |
537
|
|
|
help="Fetch remote backup for local storage", |
538
|
|
|
is_flag=True, |
539
|
|
|
default=False, |
540
|
|
|
) |
541
|
|
|
@click.option( |
542
|
|
|
"--target", "-t", help="Fetch to specified target directory", metavar="target" |
543
|
|
|
) |
544
|
|
|
@click.pass_context |
545
|
|
|
def backup(ctx, backup_instance, fetch, target): |
546
|
|
|
"""Backup a remote""" |
547
|
|
|
|
548
|
|
|
log("Backing up %s on remote %s" % (backup_instance, ctx.obj["remote"])) |
549
|
|
|
|
550
|
|
|
shell = ctx.obj["shell"] |
551
|
|
|
|
552
|
|
|
args = [ |
553
|
|
|
"iso", |
554
|
|
|
"-nc", |
555
|
|
|
"--clog", |
556
|
|
|
"10", |
557
|
|
|
"-i", |
558
|
|
|
backup_instance, |
559
|
|
|
"-e", |
560
|
|
|
"current", |
561
|
|
|
"environment", |
562
|
|
|
"archive", |
563
|
|
|
] |
564
|
|
|
|
565
|
|
|
log(args) |
566
|
|
|
|
567
|
|
|
success, result = run_process( |
568
|
|
|
get_remote_home(ctx.obj["host_config"]["login"]["username"]), args, shell=shell |
569
|
|
|
) |
570
|
|
|
|
571
|
|
|
if not success or b"Archived to" not in result.output: |
572
|
|
|
log("Execution error:", format_result(result), pretty=True, lvl=error) |
573
|
|
|
else: |
574
|
|
|
log("Local backup created") |
575
|
|
|
|
576
|
|
|
if fetch: |
577
|
|
|
full_path = result.split("'")[1] |
578
|
|
|
filename = os.path.basename(full_path) |
579
|
|
|
|
580
|
|
|
with shell.open(full_path, "r") as input_file: |
581
|
|
|
with open(os.path.join(target, filename), "w") as output_file: |
582
|
|
|
output = input_file.read() |
583
|
|
|
output_file.write(output) |
584
|
|
|
|
585
|
|
|
log("Backup downloaded. Size:", len(output)) |
586
|
|
|
|