virt-back 0.2.5__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.
- virt_back-0.2.5.data/scripts/virt-back +665 -0
- virt_back-0.2.5.dist-info/METADATA +183 -0
- virt_back-0.2.5.dist-info/RECORD +6 -0
- virt_back-0.2.5.dist-info/WHEEL +5 -0
- virt_back-0.2.5.dist-info/top_level.txt +1 -0
- virtback.py +665 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
#!python
|
|
2
|
+
|
|
3
|
+
############################################
|
|
4
|
+
# Program: virt-back.py
|
|
5
|
+
# Author: russell.ballestrini.net
|
|
6
|
+
# Date: Fri Mar 18 15:56:59 EDT 2011
|
|
7
|
+
# License: Public Domain
|
|
8
|
+
############################################
|
|
9
|
+
|
|
10
|
+
DESCRIPTION = """A backup utility for QEMU, KVM, XEN, and Virtualbox guests.
|
|
11
|
+
Virt-back is a python application that uses the libvirt api to safely
|
|
12
|
+
shutdown, gzip, and restart guests. The backup process logs to syslog
|
|
13
|
+
for auditing and virt-back works great with cron for scheduling outages.
|
|
14
|
+
Virt-back has been placed in the public domain and
|
|
15
|
+
the latest version may be downloaded here:
|
|
16
|
+
https://git.unturf.com/python/virt-back
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
"""The variable doms represents a list of libvirt dom objects.
|
|
20
|
+
Use the Domfetcher class to acquire a list of dom objects."""
|
|
21
|
+
|
|
22
|
+
import libvirt
|
|
23
|
+
import tarfile
|
|
24
|
+
import syslog
|
|
25
|
+
import re
|
|
26
|
+
from time import sleep
|
|
27
|
+
from datetime import date
|
|
28
|
+
from sys import exit
|
|
29
|
+
import argparse
|
|
30
|
+
from os import path, remove
|
|
31
|
+
from shutil import move, copy2
|
|
32
|
+
import subprocess
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from operator import methodcaller
|
|
36
|
+
except ImportError:
|
|
37
|
+
|
|
38
|
+
def methodcaller(name, *args, **kwargs):
|
|
39
|
+
def caller(obj):
|
|
40
|
+
return getattr(obj, name)(*args, **kwargs)
|
|
41
|
+
|
|
42
|
+
return caller
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Domfetcher(object):
|
|
46
|
+
"""Abstract libvirt API, supply methods to return dom object lists"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, uri=None):
|
|
49
|
+
"""Connect to hypervisor uri with read write access"""
|
|
50
|
+
# register logit as the error handler
|
|
51
|
+
libvirt.registerErrorHandler(logit, "libvirt error")
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
self.c = libvirt.open(uri)
|
|
55
|
+
except:
|
|
56
|
+
self.c = None
|
|
57
|
+
logit("libvirt error", "Failed to open connection to the hypervisor")
|
|
58
|
+
|
|
59
|
+
def get_running_doms(self):
|
|
60
|
+
"""Return a list of running dom objects"""
|
|
61
|
+
doms = []
|
|
62
|
+
for id in self.c.listDomainsID(): # loop over the running ids
|
|
63
|
+
dom = self.c.lookupByID(id) # fetch dom object by id
|
|
64
|
+
if "Domain-" not in dom.name(): # prevent actions on the Xen hypervisor
|
|
65
|
+
doms.append(dom) # append dom object to doms
|
|
66
|
+
return doms
|
|
67
|
+
|
|
68
|
+
def get_shutoff_doms(self):
|
|
69
|
+
"""Return a list of all shutoff but defined dom objects"""
|
|
70
|
+
return [self.c.lookupByName(name) for name in self.c.listDefinedDomains()]
|
|
71
|
+
|
|
72
|
+
def get_doms_by_names(self, guest_names):
|
|
73
|
+
"""Accept a list of guest_names, return a list of related dom objects"""
|
|
74
|
+
doms = []
|
|
75
|
+
for name in guest_names:
|
|
76
|
+
try:
|
|
77
|
+
dom = self.c.lookupByName(name)
|
|
78
|
+
doms.append(dom)
|
|
79
|
+
except libvirt.libvirtError:
|
|
80
|
+
pass # logit reg'd as libvirt error handler
|
|
81
|
+
return doms
|
|
82
|
+
|
|
83
|
+
def get_all_doms(self):
|
|
84
|
+
"""Return a list of all dom objects"""
|
|
85
|
+
return self.get_running_doms() + self.get_shutoff_doms()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def invoke(doms, method):
|
|
89
|
+
"""Pattern to invoke shutdown, destroy, and start on a list of doms"""
|
|
90
|
+
f = methodcaller(method)
|
|
91
|
+
for dom in doms:
|
|
92
|
+
try:
|
|
93
|
+
logit(method, "invoking %s on %s" % (method, dom.name()))
|
|
94
|
+
retcode = f(dom)
|
|
95
|
+
if retcode: # log retcode
|
|
96
|
+
logit(
|
|
97
|
+
method,
|
|
98
|
+
"{0} returned {1} on {2}".format(method, retcode, dom.name()),
|
|
99
|
+
)
|
|
100
|
+
except libvirt.libvirtError:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def backup(doms):
|
|
105
|
+
"""Accept a list of dom objects, run backup procedure on each"""
|
|
106
|
+
for dom in doms:
|
|
107
|
+
recreate = dom.isActive()
|
|
108
|
+
|
|
109
|
+
if dom.isActive(): # if dom is active, shutdown
|
|
110
|
+
shutdown([dom])
|
|
111
|
+
|
|
112
|
+
if dom.isActive(): # if dom is active, error
|
|
113
|
+
logit(
|
|
114
|
+
"error",
|
|
115
|
+
"unable to shutdown or destroy %s and BACKUP FAILED!" % dom.name(),
|
|
116
|
+
)
|
|
117
|
+
continue # skip to the next dom
|
|
118
|
+
|
|
119
|
+
xml = dom.XMLDesc(0)
|
|
120
|
+
xmlfile = path.join(options.backpath, dom.name() + ".xml")
|
|
121
|
+
with open(xmlfile, "w") as f:
|
|
122
|
+
f.write(xml)
|
|
123
|
+
|
|
124
|
+
# Updated regular expression to match file= and dev= within <disk> elements
|
|
125
|
+
disklist = re.findall(
|
|
126
|
+
r"<disk.*?<source (?:file|dev)='(.*?)'.*?</disk>", xml, re.DOTALL
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
logit("backup", "invoking backup for " + dom.name())
|
|
130
|
+
|
|
131
|
+
for disk_source in disklist:
|
|
132
|
+
if disk_source.endswith(".iso"):
|
|
133
|
+
logit(
|
|
134
|
+
"backup", "skipping ISO file %s for %s" % (disk_source, dom.name())
|
|
135
|
+
)
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if is_zfs_dataset(disk_source):
|
|
139
|
+
# Handle ZFS dataset
|
|
140
|
+
zfs_dataset = (
|
|
141
|
+
disk_source[len("/dev/zvol/") :]
|
|
142
|
+
if disk_source.startswith("/dev/zvol/")
|
|
143
|
+
else disk_source
|
|
144
|
+
)
|
|
145
|
+
zfs_snapshot_base = f"{zfs_dataset}@backup-{TODAY}"
|
|
146
|
+
zfs_snapshot = zfs_snapshot_base
|
|
147
|
+
suffix = 1
|
|
148
|
+
|
|
149
|
+
# Increment snapshot name until an available name is found
|
|
150
|
+
while (
|
|
151
|
+
subprocess.run(
|
|
152
|
+
["zfs", "list", zfs_snapshot],
|
|
153
|
+
stdout=subprocess.PIPE,
|
|
154
|
+
stderr=subprocess.PIPE,
|
|
155
|
+
).returncode
|
|
156
|
+
== 0
|
|
157
|
+
):
|
|
158
|
+
zfs_snapshot = f"{zfs_snapshot_base}-{suffix}"
|
|
159
|
+
suffix += 1
|
|
160
|
+
|
|
161
|
+
zfs_file = path.join(options.backpath, f"{dom.name()}-{TODAY}.zfs")
|
|
162
|
+
if not options.nogzip:
|
|
163
|
+
zfs_file += ".gz"
|
|
164
|
+
|
|
165
|
+
# Create ZFS snapshot
|
|
166
|
+
logit(
|
|
167
|
+
"backup", f"creating ZFS snapshot {zfs_snapshot} for {dom.name()}"
|
|
168
|
+
)
|
|
169
|
+
try:
|
|
170
|
+
subprocess.run(["zfs", "snapshot", zfs_snapshot], check=True)
|
|
171
|
+
except subprocess.CalledProcessError as e:
|
|
172
|
+
logit("error", f"Failed to create ZFS snapshot {zfs_snapshot}: {e}")
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Send ZFS snapshot to file with optional compression
|
|
176
|
+
logit(
|
|
177
|
+
"backup",
|
|
178
|
+
f"sending ZFS snapshot {zfs_snapshot} to {zfs_file} for {dom.name()}",
|
|
179
|
+
)
|
|
180
|
+
try:
|
|
181
|
+
with open(zfs_file, "wb") as f:
|
|
182
|
+
if options.nogzip:
|
|
183
|
+
subprocess.run(
|
|
184
|
+
["zfs", "send", zfs_snapshot], stdout=f, check=True
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
send_proc = subprocess.Popen(
|
|
188
|
+
["zfs", "send", zfs_snapshot], stdout=subprocess.PIPE
|
|
189
|
+
)
|
|
190
|
+
gzip_proc = subprocess.Popen(
|
|
191
|
+
["gzip"], stdin=send_proc.stdout, stdout=f
|
|
192
|
+
)
|
|
193
|
+
send_proc.stdout.close() # Allow send_proc to receive a SIGPIPE if gzip_proc exits
|
|
194
|
+
gzip_proc.communicate()
|
|
195
|
+
except subprocess.CalledProcessError as e:
|
|
196
|
+
logit("error", f"Failed to send ZFS snapshot {zfs_snapshot}: {e}")
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Prune old @backup-* snapshots, keep newest `retention`
|
|
200
|
+
prune_zfs_snapshots(zfs_dataset, options.retention)
|
|
201
|
+
else:
|
|
202
|
+
# Handle QCOW2 or other file-based disk
|
|
203
|
+
logit("backup", f"{disk_source} is not a ZFS dataset")
|
|
204
|
+
disk_file = disk_source.split("/")[-1]
|
|
205
|
+
disk_dest = path.join(options.backpath, disk_file)
|
|
206
|
+
|
|
207
|
+
logit(
|
|
208
|
+
"backup",
|
|
209
|
+
"copying %s to %s for %s" % (disk_source, disk_dest, dom.name()),
|
|
210
|
+
)
|
|
211
|
+
copy2(disk_source, disk_dest)
|
|
212
|
+
|
|
213
|
+
if recreate: # if true, start guest after backup
|
|
214
|
+
create([dom]) # start dom
|
|
215
|
+
|
|
216
|
+
ext, tarmode = ".tar.gz", "w:gz"
|
|
217
|
+
if options.nogzip:
|
|
218
|
+
ext, tarmode = ".tar", "w"
|
|
219
|
+
|
|
220
|
+
tarfilename = dom.name() + ext
|
|
221
|
+
if options.tardate:
|
|
222
|
+
tarfilename = dom.name() + "-" + TODAY + ext
|
|
223
|
+
|
|
224
|
+
tarpath = path.join(options.backpath, tarfilename)
|
|
225
|
+
|
|
226
|
+
if path.isfile(tarpath): # if file exists, run rotate
|
|
227
|
+
logit("backup", "rotating backup files for " + dom.name())
|
|
228
|
+
rotate(tarpath, options.retention)
|
|
229
|
+
|
|
230
|
+
logit("backup", "archiving files for %s to %s" % (dom.name(), tarpath))
|
|
231
|
+
tar = tarfile.open(tarpath, tarmode)
|
|
232
|
+
|
|
233
|
+
logit("backup", "archiving %s for %s" % (xmlfile, dom.name()))
|
|
234
|
+
tar.add(xmlfile) # add xml to tar
|
|
235
|
+
remove(xmlfile) # cleanup tmp files
|
|
236
|
+
|
|
237
|
+
for disk_source in disklist:
|
|
238
|
+
if disk_source.endswith(".iso"):
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if is_zfs_dataset(disk_source):
|
|
242
|
+
zfs_file = path.join(options.backpath, f"{dom.name()}-{TODAY}.zfs")
|
|
243
|
+
if not options.nogzip:
|
|
244
|
+
zfs_file += ".gz"
|
|
245
|
+
if path.isfile(zfs_file):
|
|
246
|
+
logit("backup", "archiving %s for %s" % (zfs_file, dom.name()))
|
|
247
|
+
tar.add(zfs_file) # add zfs snapshot to tar
|
|
248
|
+
remove(zfs_file) # cleanup tmp files
|
|
249
|
+
else:
|
|
250
|
+
logit(
|
|
251
|
+
"error",
|
|
252
|
+
f"ZFS snapshot file {zfs_file} not found for {dom.name()}",
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
disk_file = disk_source.split("/")[-1]
|
|
256
|
+
disk_dest = path.join(options.backpath, disk_file)
|
|
257
|
+
logit("backup", "archiving %s for %s" % (disk_dest, dom.name()))
|
|
258
|
+
tar.add(disk_dest) # add img to tar
|
|
259
|
+
remove(disk_dest) # cleanup tmp files
|
|
260
|
+
|
|
261
|
+
tar.close()
|
|
262
|
+
|
|
263
|
+
logit("backup", "finished backup for " + dom.name())
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def is_zfs_dataset(disk_source):
|
|
267
|
+
"""Check if the disk source is a ZFS dataset"""
|
|
268
|
+
logit("backup", f"checking if {disk_source} is a ZFS dataset")
|
|
269
|
+
try:
|
|
270
|
+
# Extract the ZFS dataset name from the device path
|
|
271
|
+
if disk_source.startswith("/dev/zvol/"):
|
|
272
|
+
zfs_dataset = disk_source[len("/dev/zvol/") :]
|
|
273
|
+
else:
|
|
274
|
+
zfs_dataset = disk_source
|
|
275
|
+
|
|
276
|
+
result = subprocess.run(
|
|
277
|
+
["zfs", "list", zfs_dataset], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
278
|
+
)
|
|
279
|
+
is_zfs = result.returncode == 0
|
|
280
|
+
logit("backup", f"{zfs_dataset} is {'a' if is_zfs else 'not a'} ZFS dataset")
|
|
281
|
+
return is_zfs
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logit("error", f"Error checking ZFS dataset: {e}")
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def prune_zfs_snapshots(zfs_dataset, retention):
|
|
288
|
+
"""List @backup-* snapshots on dataset, destroy oldest until count <= retention.
|
|
289
|
+
|
|
290
|
+
Only touches snapshots virt-back created (@backup-* prefix). Never destroys
|
|
291
|
+
user or TrueNAS periodic snapshots. Never destroys more than necessary —
|
|
292
|
+
`retention` snapshots always survive as the recovery floor.
|
|
293
|
+
"""
|
|
294
|
+
if retention < 1:
|
|
295
|
+
logit("error", f"retention must be >= 1, got {retention} — skipping prune")
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
result = subprocess.run(
|
|
300
|
+
["zfs", "list", "-t", "snapshot", "-H", "-o", "name",
|
|
301
|
+
"-s", "creation", zfs_dataset],
|
|
302
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, text=True,
|
|
303
|
+
)
|
|
304
|
+
except subprocess.CalledProcessError as e:
|
|
305
|
+
logit("error", f"Failed to list snapshots for {zfs_dataset}: {e}")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
prefix = f"{zfs_dataset}@backup-"
|
|
309
|
+
snaps = [line for line in result.stdout.splitlines() if line.startswith(prefix)]
|
|
310
|
+
|
|
311
|
+
surplus = len(snaps) - retention
|
|
312
|
+
if surplus <= 0:
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
for snap in snaps[:surplus]:
|
|
316
|
+
logit("backup", f"pruning old ZFS snapshot {snap} (retention={retention})")
|
|
317
|
+
try:
|
|
318
|
+
subprocess.run(["zfs", "destroy", snap], check=True)
|
|
319
|
+
except subprocess.CalledProcessError as e:
|
|
320
|
+
logit("error", f"Failed to destroy snapshot {snap}: {e}")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def shutdown(doms, wait=180):
|
|
324
|
+
"""Accept a list of dom objects, attempt to shutdown the active ones"""
|
|
325
|
+
# get all running guests from list and invoke shutdown
|
|
326
|
+
invoke(get_all_running(doms), "shutdown")
|
|
327
|
+
|
|
328
|
+
"""loop until all guests are shut off or destroy
|
|
329
|
+
all active guests if wait timer is reached."""
|
|
330
|
+
|
|
331
|
+
secs = 10
|
|
332
|
+
wait /= secs # divide wait by secs
|
|
333
|
+
|
|
334
|
+
wait = int(wait)
|
|
335
|
+
|
|
336
|
+
for i in range(0, wait + 1):
|
|
337
|
+
|
|
338
|
+
# if all doms are shut off, leave loop
|
|
339
|
+
if check_all_shutoff(doms):
|
|
340
|
+
break
|
|
341
|
+
else:
|
|
342
|
+
logit(
|
|
343
|
+
"shutdown",
|
|
344
|
+
"waited "
|
|
345
|
+
+ str(i * secs)
|
|
346
|
+
+ " seconds for "
|
|
347
|
+
+ ", ".join(dom.name() for dom in get_all_running(doms))
|
|
348
|
+
+ " to shut off",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# if the wait time is reached, destroy all active doms
|
|
352
|
+
if i == wait:
|
|
353
|
+
invoke(get_all_running(doms), "destroy")
|
|
354
|
+
|
|
355
|
+
sleep(secs)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def create(doms):
|
|
359
|
+
"""Accept a list of dom objects, attempt to start the inactive ones"""
|
|
360
|
+
# get all shutoff guests from list and invoke create
|
|
361
|
+
invoke(get_all_shutoff(doms), "create")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def reboot(doms):
|
|
365
|
+
"""Accept a list of dom objects, attempt to shutdown then start"""
|
|
366
|
+
shutdown(doms)
|
|
367
|
+
create(doms)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def info(doms):
|
|
371
|
+
"""Accept a list of dom objects, attempt to display info for all"""
|
|
372
|
+
# invoke( doms, 'name' )
|
|
373
|
+
# invoke( doms, 'info')
|
|
374
|
+
if check_all_running(doms):
|
|
375
|
+
print("NOTE: All guests are running")
|
|
376
|
+
if check_all_shutoff(doms):
|
|
377
|
+
print("NOTE: All guests are shut off")
|
|
378
|
+
|
|
379
|
+
print("")
|
|
380
|
+
print("running guests: " + ", ".join([dom.name() for dom in get_all_running(doms)]))
|
|
381
|
+
print("shutoff guests: " + ", ".join([dom.name() for dom in get_all_shutoff(doms)]))
|
|
382
|
+
print("")
|
|
383
|
+
print(
|
|
384
|
+
"DomName".ljust(16)
|
|
385
|
+
+ "Memory MB".rjust(12)
|
|
386
|
+
+ "vCPUs".rjust(8)
|
|
387
|
+
+ "CPUtime ms".rjust(18)
|
|
388
|
+
)
|
|
389
|
+
print("======================================================")
|
|
390
|
+
for dom in doms:
|
|
391
|
+
name = dom.name()
|
|
392
|
+
rams = str(dom.info()[2] / 1024) + "/" + str(dom.info()[1] / 1024)
|
|
393
|
+
cpus = str(dom.info()[3])
|
|
394
|
+
time = str(dom.info()[4] / 1000000)
|
|
395
|
+
print(name.ljust(16) + rams.rjust(12) + cpus.rjust(8) + time.rjust(18))
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def check_all_running(doms):
|
|
399
|
+
"""Accept a list of dom objects, check if all guest dom are active"""
|
|
400
|
+
if sum([dom.isActive() for dom in doms]) == len(doms):
|
|
401
|
+
return True
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def check_all_shutoff(doms):
|
|
406
|
+
"""Accept a list of dom objects, check if all guest dom are shut off"""
|
|
407
|
+
if sum([dom.isActive() for dom in doms]):
|
|
408
|
+
return False
|
|
409
|
+
return True
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def get_all_running(doms):
|
|
413
|
+
"""Accept a list of dom objects, return a list of running dom objects"""
|
|
414
|
+
return [dom for dom in doms if dom.isActive()]
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def get_all_shutoff(doms):
|
|
418
|
+
"""Accept a list of dom objects, return a list of shutoff dom objects"""
|
|
419
|
+
return [dom for dom in doms if not dom.isActive()]
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def logit(context, message, quiet=False):
|
|
423
|
+
"""syslog and error handler"""
|
|
424
|
+
if type(message) is tuple:
|
|
425
|
+
message = message[2] # libvirt message is a tuple
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
quiet = options.quiet
|
|
429
|
+
except NameError:
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
if quiet:
|
|
433
|
+
pass
|
|
434
|
+
else:
|
|
435
|
+
print(context + ": " + message)
|
|
436
|
+
|
|
437
|
+
syslog.openlog("virt-back", 0, syslog.LOG_LOCAL3)
|
|
438
|
+
syslog.syslog(message)
|
|
439
|
+
syslog.closelog()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def rotate(target, retention=3):
|
|
443
|
+
"""file rotation routine"""
|
|
444
|
+
for i in range(retention - 2, 0, -1): # count backwards
|
|
445
|
+
old_name = "%s.%s" % (target, i)
|
|
446
|
+
new_name = "%s.%s" % (target, i + 1)
|
|
447
|
+
try:
|
|
448
|
+
move(old_name, new_name)
|
|
449
|
+
except IOError:
|
|
450
|
+
pass
|
|
451
|
+
move(target, target + ".1")
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def getoptions():
|
|
455
|
+
"""Fetch cli args, parse and map to python, test sanity"""
|
|
456
|
+
|
|
457
|
+
# create an argument parser object
|
|
458
|
+
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
459
|
+
|
|
460
|
+
parser.add_argument(
|
|
461
|
+
"-q",
|
|
462
|
+
"--quiet",
|
|
463
|
+
dest="quiet",
|
|
464
|
+
action="store_true",
|
|
465
|
+
default=False,
|
|
466
|
+
help="prevent output to stdout",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
parser.add_argument(
|
|
470
|
+
"-d",
|
|
471
|
+
"--date",
|
|
472
|
+
dest="tardate",
|
|
473
|
+
action="store_true",
|
|
474
|
+
default=False,
|
|
475
|
+
help="append date to tar filename [default: no date]",
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
parser.add_argument(
|
|
479
|
+
"-g",
|
|
480
|
+
"--no-gzip",
|
|
481
|
+
dest="nogzip",
|
|
482
|
+
action="store_true",
|
|
483
|
+
default=False,
|
|
484
|
+
help="do not gzip the resulting tar file",
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
parser.add_argument(
|
|
488
|
+
"-a",
|
|
489
|
+
"--retention",
|
|
490
|
+
dest="retention",
|
|
491
|
+
metavar="amount",
|
|
492
|
+
default=3,
|
|
493
|
+
type=int,
|
|
494
|
+
help="backups to retain [default: 3]",
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
parser.add_argument(
|
|
498
|
+
"-p",
|
|
499
|
+
"--path",
|
|
500
|
+
dest="backpath",
|
|
501
|
+
metavar="'PATH'",
|
|
502
|
+
default="/KVMBACK",
|
|
503
|
+
help="backup path [default: '/KVMBACK']",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
parser.add_argument(
|
|
507
|
+
"-u", "--uri", dest="uri", metavar="'URI'", help="optional hypervisor uri"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Actions for info testing: These options display info/ test a list of guests only.
|
|
511
|
+
info_group = parser.add_argument_group(
|
|
512
|
+
"Actions for info testing",
|
|
513
|
+
"These options display info or test a list of guests.",
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
info_group.add_argument(
|
|
517
|
+
"-i",
|
|
518
|
+
"--info",
|
|
519
|
+
dest="info",
|
|
520
|
+
action="store_true",
|
|
521
|
+
default=False,
|
|
522
|
+
help="info/test a list of guests (space delimited dom names)",
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
info_group.add_argument(
|
|
526
|
+
"--info-all",
|
|
527
|
+
dest="infoall",
|
|
528
|
+
action="store_true",
|
|
529
|
+
default=False,
|
|
530
|
+
help="attempt to show info on ALL guests",
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# WARNING: Dangerous options below, option grouping for scary actions
|
|
534
|
+
action_group = parser.add_argument_group(
|
|
535
|
+
"Actions for a list of dom names",
|
|
536
|
+
"WARNING: These options WILL bring down guests!",
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
action_group.add_argument(
|
|
540
|
+
"-b",
|
|
541
|
+
"--backup",
|
|
542
|
+
dest="backup",
|
|
543
|
+
action="store_true",
|
|
544
|
+
default=False,
|
|
545
|
+
help="backup a list of guests (space delimited dom names)",
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
action_group.add_argument(
|
|
549
|
+
"-r",
|
|
550
|
+
"--reboot",
|
|
551
|
+
dest="reboot",
|
|
552
|
+
action="store_true",
|
|
553
|
+
default=False,
|
|
554
|
+
help="reboot a list of guests (space delimited dom names)",
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
action_group.add_argument(
|
|
558
|
+
"-s",
|
|
559
|
+
"--shutdown",
|
|
560
|
+
dest="shutdown",
|
|
561
|
+
action="store_true",
|
|
562
|
+
default=False,
|
|
563
|
+
help="shutdown a list of guests (space delimited dom names)",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
action_group.add_argument(
|
|
567
|
+
"-c",
|
|
568
|
+
"--create",
|
|
569
|
+
dest="create",
|
|
570
|
+
action="store_true",
|
|
571
|
+
default=False,
|
|
572
|
+
help="start a list of guests (space delimited dom names)",
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
all_group = parser.add_argument_group(
|
|
576
|
+
"Actions for all doms", "WARNING: These options WILL bring down ALL guests!"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
all_group.add_argument(
|
|
580
|
+
"--backup-all",
|
|
581
|
+
dest="backupall",
|
|
582
|
+
action="store_true",
|
|
583
|
+
default=False,
|
|
584
|
+
help="attempt to shutdown, backup, and start ALL guests",
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
all_group.add_argument(
|
|
588
|
+
"--reboot-all",
|
|
589
|
+
dest="rebootall",
|
|
590
|
+
action="store_true",
|
|
591
|
+
default=False,
|
|
592
|
+
help="attempt to shutdown and then start ALL guests",
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
all_group.add_argument(
|
|
596
|
+
"--shutdown-all",
|
|
597
|
+
dest="shutdownall",
|
|
598
|
+
action="store_true",
|
|
599
|
+
default=False,
|
|
600
|
+
help="attempt to shutdown ALL guests",
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
all_group.add_argument(
|
|
604
|
+
"--create-all",
|
|
605
|
+
dest="createall",
|
|
606
|
+
action="store_true",
|
|
607
|
+
default=False,
|
|
608
|
+
help="attempt to start ALL guests",
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
# parse options and args
|
|
612
|
+
options, unknown_args = parser.parse_known_args()
|
|
613
|
+
|
|
614
|
+
# the actionsum should be 1 to continue, bool math ftw
|
|
615
|
+
actions = [
|
|
616
|
+
options.backup,
|
|
617
|
+
options.reboot,
|
|
618
|
+
options.shutdown,
|
|
619
|
+
options.create,
|
|
620
|
+
options.info,
|
|
621
|
+
options.backupall,
|
|
622
|
+
options.rebootall,
|
|
623
|
+
options.shutdownall,
|
|
624
|
+
options.createall,
|
|
625
|
+
options.infoall,
|
|
626
|
+
]
|
|
627
|
+
actionsum = sum(actions)
|
|
628
|
+
|
|
629
|
+
if actionsum == 1:
|
|
630
|
+
guest_names = unknown_args
|
|
631
|
+
return options, guest_names
|
|
632
|
+
else:
|
|
633
|
+
exit(
|
|
634
|
+
"\nYou must have 1 action, no more, no less.\n\nRun 'virt-back --help' for help.\n"
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
if __name__ == "__main__":
|
|
639
|
+
TODAY = str(date.today())
|
|
640
|
+
# Get the list of options and list of guest_names from cli
|
|
641
|
+
options, guest_names = getoptions()
|
|
642
|
+
# connect to hypervisor with Domfetcher (read-only)
|
|
643
|
+
domfetcher = Domfetcher(options.uri)
|
|
644
|
+
|
|
645
|
+
if (
|
|
646
|
+
options.backup
|
|
647
|
+
or options.reboot
|
|
648
|
+
or options.shutdown
|
|
649
|
+
or options.create
|
|
650
|
+
or options.info
|
|
651
|
+
):
|
|
652
|
+
doms = domfetcher.get_doms_by_names(guest_names)
|
|
653
|
+
else:
|
|
654
|
+
doms = domfetcher.get_all_doms()
|
|
655
|
+
|
|
656
|
+
if options.backup or options.backupall:
|
|
657
|
+
backup(doms)
|
|
658
|
+
if options.reboot or options.rebootall:
|
|
659
|
+
reboot(doms)
|
|
660
|
+
if options.shutdown or options.shutdownall:
|
|
661
|
+
shutdown(doms)
|
|
662
|
+
if options.create or options.createall:
|
|
663
|
+
create(doms)
|
|
664
|
+
if options.info or options.infoall:
|
|
665
|
+
info(doms)
|